Skip to content

Latest commit

 

History

History
1256 lines (1008 loc) · 37.2 KB

README.md

File metadata and controls

1256 lines (1008 loc) · 37.2 KB

rust-ballistics

Personal notes on my journey to mastering Rust.

Daily Notes

Dec 4

  1. default types: i32, f64
  2. usize is big enough to store ANY pointer / offset in a data structure
  3. assert!(0.1+0.2==0.3); fails coz 0.1, 0.2’s default type is f64 so 0.1 = 0.100000000000002 etc, but (0.1_f32 + 0.2+f32 == 0.3) passes
  4. for i in -3..2 { sum += i } => sum = -5
  5. char size = 4 bytes and can hold ANY unicode char
  6. bool size = 1 byte
  7. fn not returning any value returns ‘unit’ type implicitly: (). Its size is 0 bytes
  8. let x = 5u32; let z = { x = 2 * x; } => z = () (unit type, since x = 2 * x is a statement, hence returns nothing)
  9. let z = { 2 * x } => z = 10_u32 (since 2 * x is an expression, so returns its value)
  10. REMEMBER, ‘;’ makes an expression, a statement. Expressions might also end in ‘;’, making them a statement:
    1. fn main(x: i32, y: i32) -> () { x+y;} => main’s return type is () as x+y; is a statement
  11. fn parameters MUST define their types
  12. diverging fn’s return type is ‘!’ implying it’ll never return to the caller
    1. fn main() -> ! { }
  13. unimplemented!(), todo!(), panic!() are ways to implement diverging fn
  14. Ownership
    1. a value can only have 1 owner at all times, be it a variable or a function
    2. once ownership is ‘moved’ from a variable, accessing this variable after will throw error
    3. mutability cab be changed while ‘moving’ ownership
    4. once ownership is ‘referenced’ from a variable, accessing this variable after will NOT throw error
    5. Move: transfers ownership of the value, Reference: creates a reference to owner of value (aka borrowing), Copy: sends copy of value
    6. Only 1 mutable reference possible OR many immutable references possible, but NOT both, in a given scope. Lemma: But possible in the same scope if immutable reference is not being accessed after definition of mutable reference and vice versa. Lemma is true also for 2 mutable references, not just 1 mut and 1 immut reference
    7. * - dereference, & - reference, ref - reference
  15. let s: Box<str> = "hello, world".into(); into() - converts static type to heap here (similar to ‘as’)
  16. &str: pronounced string slice
  17. Can ONLY append literals to a string, not another string
  18. (to_string(), String::from()): &str to String, (as_str(), &): String to &str
  19. r"Escapes don't work here: \x3F \u{211D}" prints Escapes don't work here: \x3F \u{211D}
  20. String slice (i.e. &str) is a view into a heap-allocated string
  21. let s1 = String::from("hi,中国"); let h1 = &s1[3..6]; => h1 is , as is 3 bytes long
  22. &String -> &str is implicitly convertible by compiler in rust
  23. tuples with size > 12 cannot be printed directly with println!()

Dec 5

  1. All fields of a struct have to be initialised at the same time. Enum variants can be initialised separately in any order

  2. Accessing

    1. Struct: struct_instance_name.var_name
    2. Enum: EnumName::EnumVariable
  3. Struct fields are name: value vs Enum variants are mostly only name, but can be name = value too

  4. #[derive(Debug)] trait and println!(“{:?}”,struct_instance_name OR EnumName::EnumVariable); to print compound types

  5. names being accessed after the loop below will throw error coz ownership of its elements was taken by name

        let names = [String::from("liming"),String::from("hanmeimei")];
        for name in names {
            // Do something with name...
        }
  6. Iterate the indexing and value in a

    let a = [4, 3, 2, 1];
    for (i, v) in a.iter().enumerate() {
        println!("The {}th element is {}", i + 1, v);
    }
  7. Expression is supposed to return a value, statement is not.

  8. fn main() {
        let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y'];
    
        // Fill the blank with `matches!` to make the code work
        for ab in alphabets {
            assert!(matches!(ab, 'A'..='Z' | 'a'..='z' | '0'..='9'))
        }
    
        println!("Success!");
    } 
  9. Use matches! to compare compound types

        enum MyEnum {
            Foo,
            Bar
        }
    
        fn main() {
            let mut count = 0;
    
            let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
            for e in v {
                if matches!(e, MyEnum::Foo) { // note
                    count += 1;
                }
            }
    
            assert_eq!(count, 2);
    
            println!("Success!");
        }
  10. if let > match when comparing compound types (enums etc) that have max 1 value. match otherwise.

  11. match and if let can introduce shadowed variables

  12. Note the partial destructurings in match arms (x, y, x and y respectively)

    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        // Fill in the blank to let p match the second arm
        let p = Point { x: 4, y: 10 };
    
        match p {
            Point { x, y: 0 } => println!("On the x axis at {}", x),
            // Second arm
            Point { x: 0..=5, y: y@ (10 | 20 | 30) } => println!("On the y axis at {}", y),
            Point { x, y } => println!("On neither axis: ({}, {})", x, y),
        }
    }
  13. if x < split is called a 'match guard'

    // Fill in the blank to make the code work, `split` MUST be used
    fn main() {
        let num = Some(4);
        let split = 5;
        match num {
            Some(x) if x < split => assert!(x < split),
            Some(x) => assert!(x >= split),
            None => (),
        }
    
        println!("Success!");
    }
  14. On comparing mutable reference in a match, the value in the match arm is dereferenced before comparison

    // FIX the error with least changing
    // DON'T remove any code line
    fn main() {
        let mut v = String::from("hello,");
        let r = &mut v;
    
        match r {
            value => value.push_str(" world!") 
        }
    }
  15. Methods of structs are called on the instance of the struct. Functions are called on the type.

    struct Circle {
        x: f64,
        y: f64,
        radius: f64
    }
    
    impl Circle {
        fn area(&self) -> f64 {
            std::f64::consts::PI * (self.radius * self.radius)
        }
    
        fn diameter() -> f64 {
            2 * 10.0
        }
    }
    
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area()); // => Method
    println!("{}", Circle::diameter()); // => Function
  16. self will take the ownership of current struct instance, however, &self will only borrow a reference from the instance.

    // Only fill in the blanks, DON'T remove any line!
    #[derive(Debug)]
    struct TrafficLight {
        color: String,
    }
    
    impl TrafficLight {
        pub fn show_state(&self)  {
            println!("the current state is {}", self.color);
        }
    }
    fn main() {
        let light = TrafficLight{
            color: "red".to_owned(),
        };
        // Don't take the ownership of `light` here.
        light.show_state();
        // ... Otherwise, there will be an error below
        println!("{:?}", light);
    }

Dec 6

  1. how generic impl for generic sruct is defined
    // Add generic for Val to make the code work, DON'T modify the code in `main`.
    struct Val<T> {
        val: T,
    }
    
    impl<T> Val<T> {
        fn value(&self) -> &T {
            &self.val
        }
    }
    
    fn main() {
        let x = Val{ val: 3.0 };
        let y = Val{ val: "hello".to_string()};
        println!("{}, {}", x.value(), y.value());
    }
  2. method (mixup()) of a generic struct (Point<T, U>) receiving a generic parameter (Point<V, W>) returning a generic struct (Point<T, W>)
    struct Point<T, U> {
        x: T,
        y: U,
    }
    
    impl<T, U> Point<T, U> {
        // Implement mixup to make it work, DON'T modify other code.
        fn mixup<V, W>(self, z: Point<V, W>) -> Point<T, W> {
            Point {
                x: self.x,
                y: z.y
            }
        }
    }
    
    fn main() {
        let p1 = Point { x: 5, y: 10 };
        let p2 = Point { x: "Hello", y: '中'};
    
        let p3 = p1.mixup(p2);
    
        assert_eq!(p3.x, 5);
        assert_eq!(p3.y, '中');
    
        println!("Success!");
    }
  3. a trait is like a class for struct, but not exactly. Mainly used when you want some structs to share some functionalities. Notice how say_hi() is reimplemented for Teacher
    // Fill in the two impl blocks to make the code work.
    // DON'T modify the code in `main`.
    trait Hello {
        fn say_hi(&self) -> String {
            String::from("hi")
        }
    
        fn say_something(&self) -> String;
    }
    
    struct Student {}
    impl Hello for Student {
        fn say_something(&self) -> String {
            String::from("I'm a good student")
        }
    }
    struct Teacher {}
    impl Hello for Teacher {
        fn say_hi(&self) -> String {
            String::from("Hi, I'm your new teacher")
        }
    
        fn say_something(&self) -> String {
            String::from("I'm not a bad teacher")
        }
    }
    
    fn main() {
        let s = Student {};
        assert_eq!(s.say_hi(), "hi");
        assert_eq!(s.say_something(), "I'm a good student");
    
        let t = Teacher {};
        assert_eq!(t.say_hi(), "Hi, I'm your new teacher");
        assert_eq!(t.say_something(), "I'm not a bad teacher");
    
        println!("Success!");
    }
  4. Weird destructuring of struct value (let &Inches(inches) = self;), PartialEq to allow equality comparison, PartialOrd to allow greater / lesser comparison
    // `Centimeters`, a tuple struct that can be compared
    #[derive(PartialEq, PartialOrd)]
    struct Centimeters(f64);
    
    // `Inches`, a tuple struct that can be printed
    #[derive(Debug)]
    struct Inches(i32);
    
    impl Inches {
        fn to_centimeters(&self) -> Centimeters {
            let &Inches(inches) = self;
    
            Centimeters(inches as f64 * 2.54)
        }
    }
    
    // ADD some attributes to make the code work!
    // DON'T modify other code!
    #[derive(Debug, PartialEq, PartialOrd)]
    struct Seconds(i32);
    
    fn main() {
        let _one_second = Seconds(1);
    
        println!("One second looks like: {:?}", _one_second);
        let _this_is_true = (_one_second == _one_second);
        let _this_is_false = (_one_second > _one_second);
    
        let foot = Inches(12);
    
        println!("One foot equals {:?}", foot);
    
        let meter = Centimeters(100.0);
    
        let cmp =
            if foot.to_centimeters() < meter {
                "smaller"
            } else {
                "bigger"
            };
    
        println!("One foot is {} than one meter.", cmp);
    }
  5. Operator overloading: overloading * operator to use a.mul(b) from standard library, for any type that implements Mul trait
    use std::ops;
    
    // Implement fn multiply to make the code work.
    // As mentioned above, `+` needs `T` to implement `std::ops::Add` Trait.
    // So, what about `*`?  You can find the answer here: https://doc.rust-lang.org/core/ops/
    fn multiply<T: std::ops::Mul<Output = T>>(a:T, b:T) -> T {
        a * b // a.mul(b)
    }
    
    fn main() {
        assert_eq!(6, multiply(2u8, 3u8));
        assert_eq!(5.0, multiply(1.0, 5.0));
    
        println!("Success!");
    }
  6. Overloading + operator to funny-add 2 structs (Foo + Bar = FooBar)
    // Fix the errors, DON'T modify the code in `main`.
    use std::ops;
    
    struct Foo;
    struct Bar;
    
    #[derive(PartialEq, Debug)]
    struct FooBar;
    
    #[derive(PartialEq, Debug)]
    struct BarFoo;
    
    // The `std::ops::Add` trait is used to specify the functionality of `+`.
    // Here, we make `Add<Bar>` - the trait for addition with a RHS of type `Bar`.
    // The following block implements the operation: Foo + Bar = FooBar
    impl ops::Add<Bar> for Foo {
        type Output = FooBar;
    
        fn add(self, _rhs: Bar) -> FooBar {
            FooBar
        }
    }
    
    impl ops::Sub<Bar> for Foo {
        type Output = BarFoo;
    
        fn sub(self, _rhs: Bar) -> BarFoo {
            BarFoo
        }
    }
    
    fn main() {
        // DON'T modify the code below.
        // You need to derive some trait for FooBar to make it comparable.
        assert_eq!(Foo + Bar, FooBar);
        assert_eq!(Foo - Bar, BarFoo);
    
        println!("Success!");
    }

Dec 7

  1. All functions of a trait must be implemented by the struct that uses it.

  2. trait as a fn parameter. Used to restrict parameter types to those that implement the trait. Note both implementations of summary()

    // Implement `fn summary` to make the code work.
    // Fix the errors without removing any code line
    trait Summary {
        fn summarize(&self) -> String;
    }
    
    #[derive(Debug)]
    struct Post {
        title: String,
        author: String,
        content: String,
    }
    
    impl Summary for Post {
        fn summarize(&self) -> String {
            format!("The author of post {} is {}", self.title, self.author)
        }
    }
    
    #[derive(Debug)]
    struct Weibo {
        username: String,
        content: String,
    }
    
    impl Summary for Weibo {
        fn summarize(&self) -> String {
            format!("{} published a weibo {}", self.username, self.content)
        }
    }
    
    fn main() {
        let post = Post {
            title: "Popular Rust".to_string(),
            author: "Sunface".to_string(),
            content: "Rust is awesome!".to_string(),
        };
        let weibo = Weibo {
            username: "sunface".to_string(),
            content: "Weibo seems to be worse than Tweet".to_string(),
        };
    
        summary(&post);
        summary(&weibo);
    
        println!("{:?}", post);
        println!("{:?}", weibo);
    }
    
    // Implement `fn summary` below.
    fn summary(a: &impl Summary) {
        let output: String = a.summarize();
        
        println!("{}", output);
    }
    
    //// alternate implementation ////
    fn summary<T: Summary>(a: &T) {
        let output: String = a.summarize();
        
        println!("{}", output);
    }
  3. General compiler rule: sizes of return types must be known at compile time.

  4. Box is smart pointer that allocates memory in heap. Used for types whose size is not known at compile time. It allocates and owns the memory in heap. Also deallocates memory when out of scope. & on the other hand only points to existing memory. Box can be passed across scopes, but & has limited lifetime. Box can be cloned, but & cannot be. Box can be used in pattern matching.

  5. Dynamic Dispatch Screenshot 2024-12-07 at 9 41 55 AM

  6. For functions wanting to return different types that implement a given trait, we cannot use impl Trait directly as size of return type is unknown at compile time, so need to dynamic dispatch the return value, using Box<dyn Trait>

    struct Sheep {}
    struct Cow {}
    
    trait Animal {
        fn noise(&self) -> String;
    }
    
    impl Animal for Sheep {
        fn noise(&self) -> String {
            "baaaaah!".to_string()
        }
    }
    
    impl Animal for Cow {
        fn noise(&self) -> String {
            "moooooo!".to_string()
        }
    }
    
    // Returns some struct that implements Animal, but we don't know which one at compile time.
    // FIX the errors here, you can make a fake random, or you can use trait object.
    fn random_animal(random_number: f64) -> Box<dyn Animal> {
        if random_number < 0.5 {
            Box::new(Sheep {})
        } else {
            Box::new(Cow {})
        }
    }
    
    fn main() {
        let random_number = 0.234;
        let animal = random_animal(random_number);
        println!("You've randomly chosen an animal, and it says {}", animal.noise());
    }
  7. impl Trait is a specific trait bound. More generally, when working with generics, the type parameters often use traits as bounds to stipulate what functionality a type implements, in various other ways. Below, T is bound to any type that implements std::ops::Add trait.

    fn main() {
        assert_eq!(sum(1, 2), 3);
    }
    
    // Implement `fn sum` with trait bound in two ways.
    fn sum<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
        x + y
    }
  8. More trait bounds. Observe the alternate implementation of creating a struct. Also note Self == Pair<T>

    struct Pair<T> {
        x: T,
        y: T,
    }
    
    impl<T> Pair<T> {
        // Self == Pair<T>
        fn new(x: T, y: T) -> Self {
            Self {
                x,
                y,
            }
        }
    }
    
    impl<T: std::fmt::Debug + PartialOrd + PartialEq> Pair<T> {
        fn cmp_display(&self) {
            if self.x >= self.y {
                println!("The largest member is x = {:?}", self.x);
            } else {
                println!("The largest member is y = {:?}", self.y);
            }
        }
    }
    
    #[derive(Debug, PartialOrd, PartialEq)]
    struct Unit(i32);
    
    fn main() {
        let pair = Pair {
            x: Unit(1),
            y: Unit(3)
        };
        
        // alternate version of creating a struct
        let pair = Pair::new(Unit(1), Unit(3));
    
        pair.cmp_display();
    }

Dec 9

  1. hatch_a_bird() returns a Box<dyn Bird>, which is a trait object, meaning it can return any type that implements the Bird trait, without knowing the exact type at compile time (also note that return types for all arms in match must be same)

    trait Bird {
        fn quack(&self) -> String;
    }
    
    struct Duck;
    impl Duck {
        fn swim(&self) {
            println!("Look, the duck is swimming")
        }
    }
    struct Swan;
    impl Swan {
        fn fly(&self) {
            println!("Look, the duck.. oh sorry, the swan is flying")
        }
    }
    
    impl Bird for Duck {
        fn quack(&self) -> String{
            "duck duck".to_string()
        }
    }
    
    impl Bird for Swan {
        fn quack(&self) -> String{
            "swan swan".to_string()
        }
    }
    
    fn main() {
        // FILL in the blank.
        let duck: Duck = Duck;
        duck.swim();
    
        let bird = hatch_a_bird(2);
        // This bird has forgotten how to swim, so below line will cause an error.
        // bird.swim();
        // But it can quak.
        assert_eq!(bird.quack(), "duck duck");
    
        let bird = hatch_a_bird(1);
        // This bird has forgotten how to fly, so below line will cause an error.
        // bird.fly();
        // But it can quak too.
        assert_eq!(bird.quack(), "swan swan");
    
        println!("Success!");
    }   
    
    // IMPLEMENT this function.
    fn hatch_a_bird(species: u8) -> Box<dyn Bird> {
        match species {
            1 => Box::new(Swan),
            2 => Box::new(Duck),
            _ => panic!()
        }
    }
  2. Array of trait objects. Note how every element of birds is a pointer with size of usize, coz we can't specify Duck or Swan whose size is unknown at compile time. Also &dyn Bird == Box<dyn Bird>

    trait Bird {
        fn quack(&self);
    }
    
    struct Duck;
    impl Duck {
        fn fly(&self) {
            println!("Look, the duck is flying")
        }
    }
    struct Swan;
    impl Swan {
        fn fly(&self) {
            println!("Look, the duck.. oh sorry, the swan is flying")
        }
    }
    
    impl Bird for Duck {
        fn quack(&self) {
            println!("{}", "duck duck");
        }
    }
    
    impl Bird for Swan {
        fn quack(&self) {
            println!("{}", "swan swan");
        }
    }
    
    fn main() {
        // FILL in the blank to make the code work.
        let birds: [&dyn Bird; 2] = [&Swan, &Duck]; // usize
    
        for bird in birds {
            bird.quack();
            // When duck and swan turn into Birds, they all forgot how to fly, only remember how to quack.
            // So, the code below will cause an error.
            // bird.fly();
        }
    }
  3. Creating a trait for a standard lib type

    // FILL in the blanks.
    trait Draw {
        fn draw(&self) -> String;
    }
    
    impl Draw for u8 {
        fn draw(&self) -> String {
            format!("u8: {}", self)
        }
    }
    
    impl Draw for f64 {
        fn draw(&self) -> String {
            format!("f64: {}", self)
        }
    }
    
    fn main() {
        let x = 1.1f64;
        let y = 8u8;
    
        // Draw x.
        draw_with_box(Box::new(x));
    
        // Draw y.
        draw_with_ref(&y);
    
        println!("Success!");
    }
    
    fn draw_with_box(x: Box<dyn Draw>) {
        x.draw();
    }
    
    fn draw_with_ref(x: &dyn Draw) {
        x.draw();
    }
  4. Note: Box<dyn Foo> doesn't work in dynamic_dispatch(a: &dyn Foo)

    trait Foo {
        fn method(&self) -> String;
    }
    
    impl Foo for u8 {
        fn method(&self) -> String { format!("u8: {}", *self) }
    }
    
    impl Foo for String {
        fn method(&self) -> String { format!("string: {}", *self) }
    }
    
    // IMPLEMENT below with generics.
    fn static_dispatch<T: Foo>(a: T) {
        a.method();
    }
    
    // Implement below with trait objects.
    fn dynamic_dispatch(a: &dyn Foo) {
        a.method();
    }
    
    fn main() {
        let x = 5u8;
        let y = "Hello".to_string();
    
        static_dispatch(x);
        dynamic_dispatch(&y);
    
        println!("Success!");
    }
  5. A trait is object-safe when (1) its methods DO NOT return Self (2) there are NO generic parameters. Note return type of f() is Box<dyn MyTrait>, so that its size is known at compile time.

    trait MyTrait {
        fn f(&self) -> Box<dyn MyTrait>;
    }
    
    impl MyTrait for u32 {
        fn f(&self) -> Box<dyn MyTrait> { Box::new(42) }
    }
    
    impl MyTrait for String {
        fn f(&self) -> Box<dyn MyTrait> { Box::new(self.clone()) }
    }
    
    fn my_function(x: Box<dyn MyTrait>) -> Box<dyn MyTrait>  {
        x.f()
    }
    
    fn main() {
        my_function(Box::new(13_u32));
        my_function(Box::new(String::from("abc")));
    
        println!("Success!");
    }

Strings

  1. String is a heap-allocated data structure that can be mutated. &str is a view into a string. String::from(), push_str() take literals, push() takes chars.

    fn main() {
        let mut s: String = String::from("hello, ");
        s.push_str("world");
        s.push('!');
    
        move_ownership(&s);
    
        assert_eq!(s, "hello, world!");
    
        println!("Success!");
    }
    
    fn move_ownership(s: &String) {
        println!("ownership of \"{}\" is moved here!", s)
    }
  2. mutable reference to a string, slices by byte count

    // FILL in the blanks
    fn main() {  
    let mut s = String::from("hello, world");
    
    let slice1: &str = s.as_str(); // other way: &s[..]
    assert_eq!(slice1, "hello, world");
    
    let slice2 = &s[..=4];
    assert_eq!(slice2, "hello");
    
    let slice3: &mut String = &mut s; 
    slice3.push('!');
    assert_eq!(slice3, "hello, world!");
    
    println!("Success!");
    }
  3. you cannot index into a String

  4. utf8_slice crate to slice a string by character count

    use utf8_slice;
    fn main() {
    let s = "The 🚀 goes to the 🌑!";
    
    let rocket = utf8_slice::slice(s, 4, 5);
    // Will equal "🚀"
    }
  5. s.chars() returns characters in s one by one, s.chars().enumerate() returns characters with their index

    fn main() {
        let s = String::from("hello, 世界");
        let slice1 = &s[..1]; //tips: `h` only takes 1 byte in UTF8 format
        assert_eq!(slice1, "h");
    
        let slice2 = &s[7..10]; // Tips: `中`  takes 3 bytes in UTF8 format
        assert_eq!(slice2, "世");
        
        // Iterate through all chars in s
        for (i, c) in s.chars().enumerate() {
            if i == 7 {
                assert_eq!(c, '世')
            }
        }
    
        println!("Success!");
    }
  6. note from_utf8(&v).unwrap())

    use std::str::from_utf8;
    fn main() {
        let mut s = String::new();
        s.push_str("hello");
    
        // Some bytes, in a vector
        let v = vec![104, 101, 108, 108, 111];
        // Turn a byte's vector into a String
        let s1 = from_utf8(&v).unwrap();
        
        assert_eq!(s, s1);
        println!("Success!");
    }
  7. compiler increases string's capacity by 2 times, so better to do it ourself

    // Output: 
    // 25
    // 25
    // 25
    // Here, there’s no need to allocate more memory inside the loop.
    fn main() {
        let mut s = String::with_capacity(25);
    
        println!("{}", s.capacity());
    
        for _ in 0..2 {
            s.push_str("hello");
            println!("{}", s.capacity());
        }
    
        println!("Success!");
    }

Dec 12

  1. String is a vector that holds utf-8 encoded bytes.

  2. vec![1, 2, 3] == vec!(1, 2, 3), Vec::from(arr): array to vector

    fn main() {
        let arr: [u8; 3] = [1, 2, 3];
        
        let v = Vec::from(arr);
        is_vec(&v);
    
        let v = vec![1, 2, 3];
        is_vec(&v);
    
        // vec!(..) and vec![..] are same macros, so
        let v = vec!(1, 2, 3);
        is_vec(&v);
        
        // In code below, v is Vec<[u8; 3]> , not Vec<u8>
        // USE Vec::new and `for` to rewrite the below code 
        let mut v1 = Vec::new();
        is_vec(&v1);
    
        for i in &v {
            v1.push(*i);
        }
    
        assert_eq!(v, v1);
    
        println!("Success!");
    }
    
    fn is_vec(v: &Vec<u8>) {}
  3. v2.extend(&v1): appends all elements of v1 to v2

  4. Vec::from(arr) == arr.into() == string_var.into() == string_var.into_bytes() == [0; 10].into_iter().collect() (iter().collect() also works for hashmaps)

  5. v.get(i) returns None if i is out of bounds without panicking

    fn main() {
        let mut v = Vec::from([1, 2, 3]);
        for i in 0..5 {
            println!("{:?}", v.get(i)) // get returns Option<&i32> i.e. Some, None
        }
    
        for i in 0..5 {
            match v.get(i) {
                Some(e) => v[i] = e + 1,
                None => v.push(i + 2)
            }
        }
        
        assert_eq!(v, vec![2, 3, 4, 5, 6]);
    
        println!("Success!");
    }
  6. when a vector and string are reallocated, their capacity is doubled.

  7. Hashmaps are an unordered collection of key-value pairs. Hashmap:remove() to delete a key-value pair. Allows insert, delete, and lookup in O(1) time (and O(n) in worst case)

  8. hashmap insertion convenience functions

    use std::collections::HashMap;
    fn main() {
        // Type inference lets us omit an explicit type signature (which
        // would be `HashMap<&str, u8>` in this example).
        let mut player_stats = HashMap::new();
    
        // Insert a key only if it doesn't already exist
        player_stats.entry("health").or_insert(100);
    
        assert_eq!(player_stats["health"], 100);
    
        // Insert a key using a function that provides a new value only if it
        // doesn't already exist
        player_stats.entry("health").or_insert_with(random_stat_buff);
        assert_eq!(player_stats["health"], 100);
    
        // Ensures a value is in the entry by inserting the default if empty, and returns
        // a mutable reference to the value in the entry.
        let health = player_stats.entry("health").or_insert(50);
        assert_eq!(health, &100);
        *health -= 50;
        assert_eq!(*health, 50);
    
        println!("Success!");
    }
    
    fn random_stat_buff() -> u8 {
        // Could actually return some random value here - let's just return
        // some fixed value for now
        42
    }
  9. only types that implement Hash and Eq can be used as keys in a hashmap.

  10. ways to approximately reduce the size of a hashmap. Note that the approximation here is due to some internal compiler stuff.

    use std::collections::HashMap;
    fn main() {
        let mut map: HashMap<i32, i32> = HashMap::with_capacity(100);
        map.insert(1, 2);
        map.insert(3, 4);
        // Indeed ,the capacity of HashMap is not 100, so we can't compare the equality here.
        assert!(map.capacity() >= 100);
    
        // Shrinks the capacity of the map with a lower limit. It will drop
        // down no lower than the supplied limit while maintaining the internal rules
        // and possibly leaving some space in accordance with the resize policy.
    
        map.shrink_to(50);
        assert!(map.capacity() >= 50);
    
        // Shrinks the capacity of the map as much as possible. It will drop
        // down as much as possible while maintaining the internal rules
        // and possibly leaving some space in accordance with the resize policy.
        map.shrink_to_fit();
        assert!(map.capacity() >= 2);
        println!("Success!");
    }
  11. hashmap takes ownersip of owned variables and copies static types during insertion, unless their reference is passed in

  12. #[allow(overflowing_literals)] to silence error in 1000 as u8 which returns 232, and other anomalies

    #[allow(overflowing_literals)]
    fn main() {
        assert_eq!(1000 as u16, 1000);
    
        assert_eq!(1000 as u8, 232);
    
        // For positive numbers, this is the same as the modulus
        println!("1000 mod 256 is : {}", 1000 % 256);
    
        assert_eq!(-1_i8 as u8, 255);
        
        // Since Rust 1.45, the `as` keyword performs a *saturating cast* 
        // when casting from float to int. If the floating point value exceeds 
        // the upper bound or is less than the lower bound, the returned value 
        // will be equal to the bound crossed.
        assert_eq!(300.1_f32 as u8, 255);
        assert_eq!(-100.1_f32 as u8, 0);
        
    
        // This behavior incurs a small runtime cost and can be avoided 
        // with unsafe methods, however the results might overflow and 
        // return **unsound values**. Use these methods wisely:
        unsafe {
            // 300.0 is 44
            println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>());
            // -100.0 as u8 is 156
            println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>());
            // nan as u8 is 0
            println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>());
        }
    }
  13. implementing from trait auto-implements into() method. Implementing fmt::Display trait auto-implements to_string() method.

  14. String to i32: "5".parse().unwrap() == i32::from_str("5").unwrap()

Dec 13

  1. 221_u8 * 135_u8 panics due to overflow on u8.

  2. v.get(1).unwrap() returns a reference to the value, not the value itself.

  3. RUST_BACKTRACE=1 cargo run to see backtrace.

  4. Result is an enum that returns Ok(val) or Err(msg).

  5. unwrap() takes Result as input and returns the value inside Ok(val), or panics.

  6. ? is basically unwrap() that returns error instead of panicking.

Dec 14

  1. A package (project) can have multiple binary (executables) crates but only one library (reusable code) crate.

  2. crate::module_name::function_name() to call a function in a library, package_name::module_name::function_name() to call a function in a binary.

  3. {}: display notation, {:?}: debug notation. Custom types need to implement fmt::Display and fmt::Debug traits to be used with {} and {:?}, in println!(), format!(), etc to be displayed. Static types have these implemented in std lib.

  4. beautify struct displays using {:#?} (with #[derive(Debug)] ofc)

  5. manually modify existing Debug trait implementations for a struct. Making it print 7 instead of Deep(Structure(7))

    use std::fmt;
    
    struct Structure(i32);
    
    struct Deep(Structure);
    
    // this snippet is from the docs
    impl fmt::Debug for Deep {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{}", self.0.0)
        }
    }
    
    fn main() {    
        // The problem with `derive` is there is no control over how
        // the results look. What if I want this to just show a `7`?
    
        /* Make it print: Now 7 will print! */
        println!("Now {:?} will print!", Deep(Structure(7)));
    }
  6. Dangling references: reference to a variable that has gone out of scope. Avoid it by ensuring that variables ALWAYS outlive their references, and not vice versa.

Dec 15

  1. Functions accepting references as arguments, need to know their lifetimes to ensure that the reference is valid for the duration of the function call, otherwise the compiler will throw an error.

    /* Make it work by adding proper lifetime annotation */
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    
    fn main() {
        let x = "long";
        let y = "longer";
        
        println!("{}", longest(x, y));
    }
  2. an example of when the reference does not outlive the function, hence does not compile.

    // `'a` must live longer than the function.
    // Here, `&String::from("foo")` would create a `String`, followed by a
    // reference. Then the data is dropped upon exiting the scope, leaving
    // a reference to invalid data to be returned.
    
    /* Fix the error in three ways  */
    fn invalid_output<'a>() -> &'a String { 
        &String::from("foo") 
    }
    
    fn main() {
        let x = invalid_output();
        println!("{}", x);
    }

Dec 20

  1. Variable must outlive its reference, and the reference (of parameters, return type) must outlive the function. This won't compile coz the reference y does not outlive failed_borrow() (read comment). But compiles if 'a is removed from both places.

    // A function which takes no arguments, but has a lifetime parameter `'a`.
    fn failed_borrow<'a>() {
        let _x = 12;
    
        // ERROR: `_x` does not live long enough
        let y: &'a i32 = &_x;
        // Attempting to use the lifetime `'a` as an explicit type annotation 
        // inside the function will fail because the lifetime of `&_x` is shorter
        // than `'a` . A short lifetime cannot be coerced into a longer one.
    }
  2. Lifetime elision: automatic lifetime inference of references by compiler. Rules:

    • each reference is assigned a lifetime
    • if only 1 input reference, its lifetime is assigned to all output references (and if multiple input references, then need to manually assign output reference lifetime)
    • if multiple references, but one of them is &self or &mut self, the lifetime of self is assigned to all output references
    /* The following is a minimum compilable example! */
    
    fn input(x: &i32) {
        println!("`annotated_input`: {}", x);
    }
    
    fn pass(x: &i32) -> &i32 { x }
    
    fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
        x
    }
    
    struct Owner(i32);
    
    impl Owner {
        // Annotate lifetimes as in a standalone function.
        fn add_one(&mut self) { self.0 += 1; }
        fn print(&self) {
            println!("`print`: {}", self.0);
        }
    }
    
    struct Person<'a> {
        age: u8,
        name: &'a str,
    }
    
    enum Either {
        Num(i32),
        Ref(&'static i32),
    }
    
    fn main() {}
  3. 'static lifetime: lifetime of the entire program. &'static only indicates that the data can live forever, not the reference, which will be constrained by its scope. 'static can be coerced into a shorter lifetime, but not vice versa.

    // Make a constant with `'static` lifetime.
    static NUM: i32 = 18;
    
    // Returns a reference to `NUM` where its `'static`
    // lifetime is coerced to that of the input argument.
    fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
        &NUM
    }
    
    fn main() {
        {
            // Make an integer to use for `coerce_static`:
            let lifetime_num = 9;
    
            // Coerce `NUM` to lifetime of `lifetime_num`:
            let coerced_static = coerce_static(&lifetime_num);
    
            println!("coerced_static: {}", coerced_static);
        }
    
        println!("NUM: {} stays accessible!", NUM);
    }
  4. i has 'static lifetime but its reference's lifetime is limited to main(). (const i or static i)'s reference is 'static though (while their lifetime is 'static ofc).

    use std::fmt::Debug;
    
    fn print_it<T: Debug + 'static>( input: T) {
        println!( "'static value passed in is: {:?}", input );
    }
    
    // same as above
    fn print_it1( input: impl Debug + 'static ) {
        println!( "'static value passed in is: {:?}", input );
    }
    
    
    fn print_it2<T: Debug + 'static>( input: &T) {
        println!( "'static value passed in is: {:?}", input );
    }
    
    fn main() {
        // i is owned and contains no references, thus it's 'static:
        let i = 5;
        print_it(i);
    
        // oops, &i only has the lifetime defined by the scope of
        // main(), so it's not 'static:
        print_it(&i);
    
        print_it1(&i);
    
        // but this one WORKS !
        print_it2(&i);
    }