Skip to content

jhaemin/rust

Repository files navigation

Rust

This repository demonstrates my learning process of Rust programming language from the ground up, following the official guide book step by step.

Statements vs Expressions

Rust is an expression-based language, so it is important to understand its definition correctly.

  • Statements are instructions that perform some actions and do not return a value.
  • Expressions evaluate to a resulting value.
fn main() {
  let y = 6; // statement
  y + 1 // expression: resolves to 7
  y + 1; // statement: does not return any value
}

Unlike other languages, such as C and Ruby, where the assignment returns the value of the assignment, Rust does not.

No ; at the end of an expression

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value.

Functions with Return Values

In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.

fn ten() -> i32 {
  10
}

fn plus_one(x: i32) -> i32 {
  x + 1
}

fn main() {
  let x = plus_one(2);

  println!("{}", x); // "3"
}

fn minus_one(x: i32) -> i32 {
  x - 1; // compile error: because it is a statement
}

Blocks of code evaluate to the last expression in them, and numbers by themselves are also expressions.

Comments

All programmers strive to make their code easy to understand, but sometimes extra explanation is warranted.

if Expressions

if is an expression

arms

Blocks of code associated with the conditions in if expressions are sometimes called arms, just like the arms in match expressions.

fn main() {
  let number = 3;

  if number < 5 {
    println!("condition was true");
  }
}

It's also worth nothing that the condition in this code must be a bool. If the condition isn't a bool, we'll get an error.

fn main() {
  let number = 3;

  if number { // Error
    println!("number was three");
  }
}

Unlike languages such as Ruby and JavaScript, Rust will not automatically try to convert non-Boolean types to a Boolean. You must be explicit and always provide if with a Boolean as its condition.

The values that have the potential to be results from each arm of the if must be the same type

// Compile error

fn main() {
  let condition = true;

  let number = if condition {
    5 // Integer
  } else {
    "six" // String
  };
}

loop

fn main() {
  loop {
    println("again");
  }
}

Ownership

Ownership Rules

First, let’s take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Example

{                      // s is not valid here, it’s not yet declared
    let s = "hello";   // s is valid from this point forward

    // do stuff with s
}                      // this scope is now over, and s is no longer valid
  • When s comes into scope, it is valid.
  • It remains valid until it goes out of scope.

Ways Variables and Data Interact: Move

let x = 5;
let y = x;

x and y are both valid.

let s1 = String::from("hello");
let s2 = s1;

s1 was moved into s2. So s1 is invalid.

Ways Variables and Data Interact: Clone

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

Stack-Only Data: Copy

❗️Slice Type

Need more investigation

Structs

#[derive(Debug)] // For debug print (optional)
struct Rectangle {
  width: u32,
  height: u32,
}

Methods

Similar to functions but implemented in structs themselves. Their first parameter is always self, which represents the instance of the struct the method is being called on.

impl Rectangle {
  fn area(&self) -> u32 {
    self.width * self.height
  }
}

Associated Functions

Another useful feature of impl blocks is that we’re allowed to define functions within impl blocks that don’t take self as a parameter. Associated functions are often used for constructors that will return a new instance of the struct.

impl Rectangle {
  fn square(size: u32) -> Rectangle {
    Rectangle {
      width: size,
      height: size,
    }
  }
}

// Inside main
let rect = Rectangle::square(311); // Create a 311 x 311 rectangle

Enums

enum IpAddrKind {
  V4,
  V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

fn route(ip_kind: IpAddrKind) {}

route(IpAddrKind::V4);
route(IpAddrKind::V6);
enum IpAddr {
  V4(String),
  V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
enum IpAddr {
  V4(u8, u8, u8, u8),
  V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

Option

Rust doesn’t have the null feature that many other languages have. Null is a value that means there is no value there. In languages with null, variables can always be in one of two states: null or not-null.

In his 2009 presentation “Null References: The Billion Dollar Mistake,” Tony Hoare, the inventor of null, has this to say:

I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

enum Option<T> {
    Some(T),
    None,
}
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;
  • Some: We know that a value is present and the value is held within the Some.
  • None: In some sense, it means the same thing as null: we don't have a valid value.
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y; // Error

When we have a value of a type like i8 in Rust, the compiler will ensure that we always have a valid value. We can proceed confidently without having to check for null before using that value.

Only when we have an Option<i8> (or whatever type of value we're working with) do we have to worry about possibly not having a value, and the compiler will make sure we handle that case before using the value.

match

enum Coin {
  Penny,
  Nickel,
  Dime,
  Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  match coin {
    Coin::Penny => {
      println!("Lucky penny!");
      1
    }
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
  }
}

Matches are exhaustive

Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option<T>, when Rust prevents us from forgetting to explicitly handle the None case, it protects us from assuming that we have a value when we might have null, thus making the billion-dollar mistake discussed earlier.

The _ placeholder

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

if let

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}
if let Some(3) = some_u8_value {
    println!("three");
}

Two behave the same.

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

Both are equivalent.

Macros

Macros look like functions, except that their name ends with a bang !, but instead of generating a function call, macros are expanded into source code that gets compiled with the rest of the program. However, unlike macros in C and other languages, Rust macros are expanded into abstract syntax trees, rather than string preprocessing, so you don't get unexpected precedence bugs.

// This is a simple macro named `say_hello`.
macro_rules! say_hello {
    // `()` indicates that the macro takes no argument.
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello!");
    };
}

fn main() {
    // This call will expand into `println!("Hello");`
    say_hello!()
}

Packages and Crates

A crate is a binary or library. The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate.

A package is one or more crates that provide a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.

Several rules determine what a package can contain. A package must contain zero or one library crates, and no more. It can contain as many binary crates a you'd like, but it must contain at leat one crate (either library or binary).

Cheatsheet

Commands

rustup update
cargo --version
cargo build
cargo build --release
cargo check
cargo run
cargo doc --open

Keywords

  • let
  • mut
  • const
  • match
  • loop

About

My learning process of Rust, a modern and blazingly fast programming language

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages