Skip to content

Instantly share code, notes, and snippets.

@blessanm86
Last active December 19, 2021 14:47
Show Gist options
  • Save blessanm86/04034723852b004679c21fedd13ff93b to your computer and use it in GitHub Desktop.
Save blessanm86/04034723852b004679c21fedd13ff93b to your computer and use it in GitHub Desktop.
Some notes I can refer while I pick up rust

Installation

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

You may also need to install a linker via xcode

xcode-select --install

To update rust rustup update

To uninstall rust rustup self uninstall

Get the version rustc --version

Hello World

fn main() {
  println!("Hello World");
}

Run with the command rustc main.rs

Cargo

Cargo is the package manager and build system for rust.

  1. Create a new project - cargo new <name> Use --vcs=none to avoid git init.
  2. cargo.toml is the equivalent to package.json
  3. cargo build will create a cargo.lock file and create a executable binary
  4. cargo run will build and run the project
  5. cargo check will check if the code compiles without generating a build and is much faster
  6. Builds are stored in target/debug
  7. cargo build --release created the release binary which is more optimized and is stored in target/release

Variables

  1. Declare variable as let x = "Blessan";. This will be an immutable variable.

  2. To createa mutable variable let mut x = "Blessan";

  3. Constants are immutable, need to be typed and its value should be something that can be computed during build time. Its lifetime is throughout the run time of the prgram nad the scope its defined in.const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

  4. Rust allows variable shadowing

    fn main() {
         let x = 5;
    
         let x = x + 1;
    
         {
             let x = x * 2;
             println!("The value of x in the inner scope is: {}", x);
         }
    
         println!("The value of x is: {}", x);
     }
     
     The value of x in the inner scope is: 12
     The value of x is: 6
    
  5. Shadowing allows to change the type of a variable without changing its name.

Data Types

  1. Rust is a typed. It can infer types most of the type. For other cases you will have to annotate.
  2. Integer type - i8,i16,i32,i64,i128,u8,u16,u32,u64,u128, usize, isize. The number ranges are -(2n - 1) to 2n - 1 - 1
  3. You can use _ as a separator in numbers. Eg 1_000
  4. It has f32, f64 for floating numbers.
  5. Integer division leads to the floored integer value.
  6. The boolean type is bool.
  7. char type is written with single quotes. let x = '5';
  8. Tuples
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    println!("{}", tup.0);
    
    let (x, y, z) = tup;
    println!("The value of y is: {}", y);
    
  9. Unit type tuple is ()
  10. Expressions implicitly return the unit value if they don’t return any other value.
  11. Arrays should have a single type and fixed number of elements. Arrays are stored in the stack.
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    let a = [3; 5]; //same as [3,3,3,3,3]
    
    

Functions

  1. Rust code uses snake case as the conventional style for function and variable names.
  2. You must declare the type of each parameter
  3. Default return type is unit value or it should be specified
  4. Statements are instructions that perform some action and do not return a value.
    let y = 6;
    
  5. Expressions evaluate to a value
    let y = {
         let x = 3;
         x + 1
    };
    
  6. Expressions without semicolon in function will become the return value
    fn plus_one(x: i32) -> i32 {
        x + 1
    }
    

Control Flows

  1. If Statement. It needs to evaluate to a boolean value. Expression doesnt need braces around the expression. It can return an expression which can be stored in a variable. Similar to ternary operator in JS. But the types of the expression should be the same.

    let number = if condition { 5 } else { 6 };
    
  2. Loops - loop, while, for

    let mut remaining = 2;
    loop {
         if(remaining == 0) {
           break;
         }
         println!("again!");
         remaining -=1;
     }
    
  3. loop can also return an expression. break <expression>

  4. while

    let mut number = 3;
    
     while number != 0 {
         println!("{}!", number);
    
         number -= 1;
     }
    
  5. Use can use for loop with arrays, vectors

    let a = [10, 20, 30, 40, 50];
    
     for element in a {
         println!("the value is: {}", element);
     }
    
  6. You can do normal JS for loop using a number range

    for number in (1..4).rev() {
         println!("{}!", number);
     }
    

Ownership

Ownership is one of the mechnanisms used by rust to guarantee memory safeness. Values are stored in the stack or heap. All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.

When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.

Ownership Rules

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
{                      // 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 goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is called drop

let x = 5;
let y = x;

the value of x is copied to y as it is something that can be determined during compile time.

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

Here s1 is on the heap so s1's ownership is moved to s2. And s1 is invalid.

To create a copy of data on the heap, u will need to create a clone.

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

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

Rust has a special annotation called the Copy trait that we can place on types like integers that are stored on the stack (we’ll talk more about traits in Chapter 10). If a type implements the Copy trait, an older variable is still usable after assignment. Rust won’t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait. If the type needs something special to happen when the value goes out of scope and we add the Copy annotation to that type, we’ll get a compile-time error.

As a general rule, any group of simple scalar values can implement Copy, and nothing that requires allocation or is some form of resource can implement Copy. Here are some of the types that implement Copy:

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.

Ownership and Functions

The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

If we tried to use s after the call to takes_ownership, Rust would throw a compile-time error.

Return Values and Scope

Returning values can also transfer ownership.

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

References and Borrowing

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

We pass &s1 into calculate_length and, in its definition, we take &String rather than String. The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because it does not own it, the value it points to will not be dropped when the reference stops being used. the signature of the function uses & to indicate that the type of the parameter s is a reference. We cannot mutate the value with the above code.

Mutable References

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

First, we had to change s to be mut. Then we had to create a mutable reference with &mut s where we call the change function, and update the function signature to accept a mutable reference with some_string: &mut String. This makes it very clear that the change function will mutate the value it borrows.

But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data at a time.

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

The above code will fail with cannot borrow s as mutable more than once at a time

The benefit of having this restriction is that Rust can prevent data races at compile time.

let mut s = String::from("hello");

{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

You cannot mix both mutable and immutable references

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used.

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

The scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created. These scopes don’t overlap, so this code is allowed.

Dangling References

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter

this function's return type contains a borrowed value, but there is no value for it to be borrowed from. s will be dropped after function execution.

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

This works as the value is returned along with its ownership.

The Rules of References

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.

Slice Type

Another data type that does not have ownership is the slice. Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

This is similar to taking a reference to the whole String but with the extra [0..5] bit. Rather than a reference to the entire String, it’s a reference to a portion of the String.

We can create slices using a range within brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice. Internally, the slice data structure stores the starting position and the length of the slice, which corresponds to ending_index minus starting_index. So in the case of let world = &s[6..11];, world would be a slice that contains a pointer to the byte at index 6 of s with a length value of 5.

With Rust’s .. range syntax, if you want to start at index zero, you can drop the value before the two periods. In other words, these are equal:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

By the same token, if your slice includes the last byte of the String, you can drop the trailing number. That means these are equal:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

You can also drop both values to take a slice of the entire string. So these are equal:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

String Literals Are Slices

let s = "Hello, world!";

Knowing that you can take slices of literals and String values leads us to one more improvement on first_word, and that’s its signature:

fn first_word(s: &String) -> &str {

A more experienced Rustacean would write the signature shown in Listing 4-9 instead because it allows us to use the same function on both &String values and &str values.

fn first_word(s: &str) -> &str {

If we have a string slice, we can pass that directly. If we have a String, we can pass a slice of the String or a reference to the String. This flexibility takes advantage of deref coercions

Just as we might want to refer to a part of a string, we might want to refer to part of an array. We’d do so like this:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

This slice has the type &[i32]. It works the same way as string slices do, by storing a reference to the first element and a length.

Structs

A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Here, we’re creating a new instance of the User struct, which has a field named email. We want to set the email field’s value to the value in the email parameter of the build_user function. Because the email field and the email parameter have the same name, we only need to write email rather than email: email.

let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

The syntax .. specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance. The code in Listing 5-7 also creates an instance in user2 that has a different value for email but has the same values for the username, active, and sign_in_count fields from user1. The ..user1 must come last to specify that any remaining fields should get their values from the corresponding fields in user1, but we can choose to specify values for as many fields as we want in any order, regardless of the order of the fields in the struct’s definition.

Note that the struct update syntax is like assignment with = because it moves the data, just as we saw in the “Ways Variables and Data Interact: Move” section. In this example, we can no longer use user1 after creating user2 because the String in the username field of user1 was moved into user2. If we had given user2 new String values for both email and username, and thus only used the active and sign_in_count values from user1, then user1 would still be valid after creating user2. The types of active and sign_in_count are types that implement the Copy trait

Tuple structs.

Tuple structs have the added meaning the struct name provides but don’t have names associated with their fields; rather, they just have the types of the fields. Tuple structs are useful when you want to give the whole tuple a name and make the tuple be a different type from other tuples, and naming each field as in a regular struct would be verbose or redundant.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Unit-Like Structs Without Any Fields

You can also define structs that don’t have any fields! These are called unit-like structs because they behave similarly to (). Unit-like structs can be useful in situations in which you need to implement a trait on some type but don’t have any data that you want to store in the type itself.

struct AlwaysEqual;

let subject = AlwaysEqual;

It’s possible for structs to store references to data owned by something else, but to do so requires the use of lifetimes,

Putting the specifier :? inside the curly brackets tells println! we want to use an output format called Debug. The Debug trait enables us to print our struct in a way that is useful for developers so we can see its value while we’re debugging our code.

Rust does include functionality to print out debugging information, but we have to explicitly opt in to make that functionality available for our struct. To do that, we add the outer attribute #[derive(Debug)] just before the struct definition, as shown in Listing 5-12.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

When we have larger structs, it’s useful to have output that’s a bit easier to read; in those cases, we can use {:#?} instead of {:?} in the println! string. When we use the {:#?} style in the example, the output will look like this:

Another way to print out a value using the Debug format is by using the dbg! macro . The dbg! macro takes ownership of an expression, prints the file and line number of where that dbg! macro call occurs in your code along with the resulting value of that expression, and returns ownership of the value. Calling the dbg! macro prints to the standard error console stream (stderr), as opposed to println! which prints to the standard output console stream (stdout).

Define Methods on Structs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

In the signature for area, we use &self instead of rectangle: &Rectangle. The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for. Methods must have a parameter named self of type Self for their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot. Note that we still need to use the & in front of the self shorthand to indicate this method borrows the Self instance, just as we did in rectangle: &Rectangle. Methods can take ownership of self, borrow self immutably as we’ve done here, or borrow self mutably, just as they can any other parameter.

Methods can hold additonal params

```rust
let rect1 = Rectangle {
    width: 30,
    height: 50,
};
let rect2 = Rectangle {
    width: 10,
    height: 40,
};
let rect3 = Rectangle {
    width: 60,
    height: 45,
};

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

fn can_hold(&self, other: &Rectangle) -> bool {
    self.width > other.width && self.height > other.height
}

All functions defined within an impl block are called associated functions because they’re associated with the type named after the impl. We can define associated functions that don’t have self as their first parameter (and thus are not methods) because they don’t need an instance of the type to work with. We’ve already used one function like this, the String::from function, that’s defined on the String type.

Associated functions that aren’t methods 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,
        }
    }
}

let sq = Rectangle::square(3);

Enums

Enums allow you to define a type by enumerating its possible variants.

enum IpAddrKind {
    V4,
    V6,
}

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

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

Ip address can either be v4 or v6 but not both together.

We can put data inside enum.

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

This enum has four variants with different types:

  • Quit has no data associated with it at all.
  • Move has named fields like a struct does.
  • Write includes a single String.
  • ChangeColor includes three i32 values.

Defining an enum with variants such as the ones in Listing 6-2 is similar to defining different kinds of struct definitions, except the enum doesn’t use the struct keyword and all the variants are grouped together under the Message type. if we used the different structs, which each have their own type, we couldn’t as easily define a function to take any of these kinds of messages as we could with the Message enum.

There is one more similarity between enums and structs: just as we’re able to define methods on structs using impl, we’re also able to define methods on enums.

impl Message {
    fn call(&self) {
        // method body would be defined here
    }
}

let m = Message::Write(String::from("hello"));
m.call();

Option Enum

The Option type is used in many places because it encodes the very common scenario in which a value could be something or it could be nothing. Expressing this concept in terms of the type system means the compiler can check whether you’ve handled all the cases you should be handling; this functionality can prevent bugs that are extremely common in other programming languages.

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

The type of some_number is Option. The type of some_string is Option<&str>, which is a different type. Rust can infer these types because we’ve specified a value inside the Some variant. For absent_number, Rust requires us to annotate the overall Option type: the compiler can’t infer the type that the corresponding Some variant will hold by looking only at a None value. Here, we tell Rust that we mean for absent_number to be of type Option.

In other words, you have to convert an Option to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is.

match control flow

Rust has an extremely powerful control flow operator called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things

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

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Another useful feature of match arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.

enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}
  
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}
  • 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, 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 impossible. *
  let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

Here, we’re telling Rust explicitly that we aren’t going to use any other value that doesn’t match a pattern in an earlier arm, and we don’t want to run any code in this case.

Concise Control Flow with if let

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.

  let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
  
  same as
  
  let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }
  

The syntax if let takes a pattern and an expression separated by an equal sign. It works the same way as a match, where the expression is given to the match and the pattern is its first arm. In this case, the pattern is Some(max), and the max binds to the value inside the Some. We can then use max in the body of the if let block in the same way as we used max in the corresp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment