9 min read

Rust101

Table of Contents

Ownership

  1. Each value must always have one and only one variable pointing to it, that is its owner.
  2. At the end of scope of the owner, the value is dropped (i.e.) memory is deallocated.
  3. Ownership is moved by default, but can be copied using Copy.

Moving and Copying

  1. Primitive values like int, bool are copied by default.

Borrowing

  1. Borrowing allows temporary references (&T or &mut T) without transferring ownership.
  2. At any given time there can only be either a mutable or an unmutable reference.
  3. A reference must always be valid.

!(Mut + UnMut)

  1. Cannot have mutable and unmutable ref for the same value at the same time.
  2. Cannot have multiple mutable ref, to avoid race mutations to same value, if both ref are used to modify the same value. Avoids corrupting data.
  3. Can have multiple unmutable ref.

Invalid reference

Following fails because β€œa” is out of scope at the end of dangling fn and will be cleared. So a reference to a non existing value is not possible. (i.e.) cannot borrow from dropped value.

fn main() {
    let aref = dangling();
}

fn dangling() -> &String {
    let a = String:from("Hello");

    &a;
}

Slice

fn main() {
    let s = String::from("Hello world");
    let fw = first_word(&s);
    println!("{}", fw);
}

fn first_word(s: &String) -> &str {
    for (i, &item) in s.as_bytes().iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }

    return &s[..];
}

Why &s[..] instead of s[..]?

  • s[..] means β€œslice from 0 to end”

  • s[..] derefs s: &String to String, then calls - Deref<Target=str>

  • So s[..] gives a str (unsized)

  • Wrapping with &: &s[..] is a &str (sized, usable)

  • String owns heap memory

  • &String borrows the whole struct

  • &str borrows just the actual text (slice of bytes)

  • You need &s[..] to convert a &String to a &str

ExpressionTypeNotes
s&StringReference to the whole String
*sStringDereferenced String
(*s)[..]strUnderlying string slice
&s[..]&strReference to string slice βœ…

Modules

Scenario 1: Seprate modul.rs file

  • DO NOT wrap it in mod modul { ... }
  • Instead, declare it in main.rs using mod modul;
  • Then use it with use modul::… or modul::…

Layout

With this example layout

src/
β”œβ”€β”€ main.rs
└── modul.rs
mod modul; // loads src/modul.rs

use modul::Breakfast;

fn main() {
    let summer_bf = Breakfast::summer_menu();
    println!("{:?}", summer_bf);
}
#[derive(Debug)]
pub struct Breakfast {
    pub food: String,
    pub drink: String,
}

impl Breakfast {
    pub fn summer_menu() -> Breakfast {
        Breakfast {
            food: "Toast".into(),
            drink: "Orange juice".into(),
        }
    }
}

mod modul; tells the compiler to load the external file, and the file must not declare mod modul again β€” that would be redundant and cause a nested path like modul::modul::Breakfast.

Scenario 2: You define the module inline inside main.rs

  • DO use mod modul { ... } directly
  • No need for mod modul; or separate file
  • You can still use modul::… if you like
mod modul {
    #[derive(Debug)]
    pub struct Breakfast {
        pub food: String,
        pub drink: String,
    }

    impl Breakfast {
        pub fn summer_menu() -> Breakfast {
            Breakfast {
                food: "Toast".into(),
                drink: "Orange juice".into(),
            }
        }
    }
}

use modul::Breakfast;

fn main() {
    let summer_bf = Breakfast::summer_menu();
    println!("{:?}", summer_bf);
}

Seprate folder sub modules

src/ β”œβ”€β”€outermodul/ | └── innermodul.rs β”œβ”€β”€ outermodul.rs β€”> use innermodul; └── main.rs β€”> use outermodul;

If there is an inner modul, that needs to present in a folder with same name as the outermodul. This can then be access by outermodul.

If adding a bunch of utils to same folder

rust expects mod.rs file in directory.

src/
└── utils/
    β”œβ”€β”€ mod.rs
    β”œβ”€β”€ math.rs
    β”œβ”€β”€ stringconcat.rs
    └── extracttoken.rs

Other files that we want to import into other files must be brought into scope in the mod.rs file.

pub mod math;
pub mod stringconcat;
pub mod extracttoken;

In main.rs

mod utils;  // loads utils/mod.rs and submodules

fn main() {
    // To access math and concat functions:
    let sum = utils::math::add(1, 2);
    let combined = utils::stringconcat::concat_str("foo", "bar");
}
ScenarioHow to declare submodules in mod.rsHow to declare in main.rsHow to use modules
Folder with mod.rspub mod math; pub mod concat; in mod.rsmod utils; in main.rsutils::math::func()
Flat files no mod.rs and no foderN/Amod math; mod concat; in main.rsmath::func(), concat::func()

Copy and move

In the following example, both

  1. println!("{}", &v[0]);
  2. println!("{}", v[0]);
    will work.
pub fn collections() {
    let mut v: Vec<i32> = Vec::new();
    let v2 = vec![1, 2, 3, 4];

    v.push(1);

    println!("{}", &v[0]);
}

When using v[0] or &v[0]

  1. i32 by default β€œcopies” to another variable when it is assigned.

  2. String and structs when assign to another var need to be passed as ref, else they will be β€œmoved” on default. Can be cloned with .clone(). Compile err when trying to access the same after move, if not passed as ref initially.

  3. println accepts ref of variable. println!("{}", x); will auto-borrow x if it implements Display by reference. E.g. String, Vec, custom structs, if v[0] is used. If not, it will accept a clone (internally), If String is passed, will auto deref to &String which implements Display interface.

  4. println!("{}", v[0]); will work even if vector had a string at index 0, because by default [] indexing fn returns the reference not the owner and the auto deref will automatically use the &String from String.

  5. But let s = vStr[0]; will not compile if idx 0 is string

    1. We get a reference like before, but String type cant copy by default
    2. But moving from a vector isnt allowerd.
  6. This is why let a = vStr[0]; doesnt work but both println!("{}", &v[0]) and println!("{}", v[0]) work.

From ChatGPT:

CodeBorrowed?Moved?Why it works
println!("{}", v[0])βœ… yes❌ nov[0] returns reference; auto-borrowed
println!("{}", &v[0])βœ… yes❌ noExplicitly borrowed
println!("{}", v[0].clone())❌ noβœ… yesClones and moves the cloned value
let x = v[0];❌ no❌ no❌ Fails for non-Copy types like String

Generics With more Ownership understandings

Functions accessible based on their traits

Below is an example of a generic scenario where based on the type/ trait of the incoming type, the object gets access to different methods.

#[derive(Debug)]
struct Point<T,U>  {
    x: T,
    y: U,
}

impl<T: Copy, U> Point<T,U> { // get_x is accessible only for variables with copy trait
    fn get_x(&self) -> T {
        self.x
    }
}

impl<T> Point<T,f64> {
    fn get_floaty(&self) -> f64 {
        self.y
    }
}

fn enumgenr() {
    let flotPt = Point { x: 5, y: 3.5 };
    flotPt.get_floaty();
    flotPt.get_x();
    let intPt = Point { x: 5, y: 3 };
    // intPt.get_floaty(); // wont work
    intPt.get_x();
}

Complex Generic usecase

We try to combine two different generics and create an output combining their types.

Calling function to give context

  1. All points contains x and y
  2. There are two examples here one with a method for copy trait and another with clone trait.
  3. Both examples have two points each, and combine x from pt1 and y from pt2.
fn mixup() {
    // Copy trait
    let a = Point {x: 1, y: 3.0};
    let b = Point {x: "Hello", y: 'c'};

    let c = a.mix(b);
    println!("{:?}", c);
    println!("{:?}", a);

    // Clone trait
    let d = Point {x: String::from("tasd"), y: 3.0};
    let e = Point {x: String::from("Hello"), y: 'c'};

    let f = a.mix(b);
    println!("{:?}", f);
    println!("{:?}", d);
}

Copy Trait

  1. Supports T with copy trait, e.g. int, char, static str (stored on stack, fixed size).
  2. &self is a ref. Always copies. If β€œself” instead of β€œ&self” was used, it means that struct β€œa” is moved into the method.
  3. Which means a is no longer usable after this fn call. This is not because of the copy itself but simply how ownership is transferred if reference isn’t passed.
impl <T: Copy, U> Point<T, U> {
    fn mix<V, W>(&self, pt: Point<V, W>) -> Point<T, W> {
        Point {x: self.x, y: pt.y }
    }
}

Clone Trait

  1. Support T with clone trait. e.g. String, heap allocated, can grow.
  2. Here the value needs to be explicitly cloned since its not allowed to be moved out of a reference.
  3. Since &self is a shared self reference.
impl <T: Clone, U> Point<T, U> {
    fn mix<V, W>(&self, pt: Point<V, W>) -> Point<T, W> {
        Point {x: self.x.clone(), y: pt.y }
    }
}

Clone trait without &self

  1. Support T with Clone trait, e.g. String (heap-allocated, not Copy).
  2. This uses self instead of &self, meaning the method takes ownership of the whole struct.
  3. Because self is owned (not a reference), we are allowed to move x out of it directly.
  4. No need to clone here, even though T isn’t Copy, because the method owns the value and value is moved.
  5. Any method trying to access the struct after passing to method will cause a compiler error
impl <T: Clone, U> Point<T, U> {
    fn mix<V, W>(self, pt: Point<V, W>) -> Point<T, W> {
        Point {x: self.x, y: pt.y }
    }
}

&x.y always means β€œtake a reference to x.y”, regardless of whether x is already a reference or not.

Source