Published on

The Rust Learning Notes

Authors

1. variables

const CONST_VALUE: u32 = 10;
let number: f32 = 10;
  • number is the variable name, f32 is type, 10 is value.
  • All variable default is immutable, for safty, concurrency and speed.
  • Use let mut number: f32 = 10; to change number to a mutable value that can be changed later.
  • const defines const type, const type is immutable forever, and it must indicates the type.

2. function

fun do_stuff(a: f32, b: f32) -> f32 {
    {
        let x = 2;
        println!("{}", x);
    }
    // x is not available outside the scope
    a*b
}
  • a:f32, b:f32 define the function arguments, ->f32 indicate the return type.
  • a*b is the return value, same as return a*b;, remember if don't use returnkeyward, don't use; in the end.
  • variable only available in the scope, x is assigned inside scope, it will drop automatically when leaving the .

3. shadowing

fn main() {
    let x = 5;
    let x = x + 6;
    {
        let x = x * 7; //x is 77
    }
    //x here is 11
}
  • declare a new variable with the same name as previous is first variable shadowed by the second.
  • compile only sees the latest one until it out of scope

4. Data Types

Integer Type:

u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, 0xff, 0b1111_0000, b'A'

  • u is unsigned value, i is signed, u/isize is the value depends on 32bit or 64bit system
  • type suffix is allowed like 20u8
  • default integer type is i32
  • 0xff is hex, 0b is binary, b is byte
  • can use _ to make large number easy read like 1_000_000

Float type

f32, f64

  • default is f64

Bollean Type

bool

Char Type

char

  • char type is four bytes in size in rust

Tuple type

    let tup: (i32, f32) = (3, 8.8);
    let first_elemnt = tup.0;
    let (first, second) = tup;
  • tuple element can be different type
  • access the tuple value by use .index like tup.0 above
  • (first, second) destructures the tup and get the value

Array Type

let array: [u32, 5] = [1,2,3,4,5];
let first = array[0];
let inital_array = [0; 5] // equal to [0, 0, 0, 0, 0]
  • array must have same type and fixed length
  • [u32, 5], first one defines type, second is length
  • [0; 5] means inital array value to all 0 and array length is 5
  • use array_name[index] to access the array member

5. Control Flow

if..else

if x<0 {5} else if x<6 {6} else {7}
  • if condition doesn't need () after if like if(x<0) in rust
  • if can assign value like let value = if x<0 {5}, value is 5 here

loops :

let count  = 0;
 'first_loop': loop{
    let value = loop {
        if count == 2
        {
            break count;
        }
        count += 1;
        if count == 3
        {
            break 'first_loop'
        }
    }
    count += 1;
 }
  • first_loop is the defined name to distinguish different loops
  • break count can quit the loop and return the value of count
  • break first_loop can quit the loop named first_loop

while

while number !== 0 {
    number -= 1
}

for

let array = [1,2,3,4,5]
for element in array {
    println!("the value is : {element}")
}

6. Ownership

fn main(){
    let s1 = String::from("Owner");
    let s2 = s1;
    //s1 is not available anymore because s1 is move ownership to s2 for the string
    let x1 = 5;
    let x2 = x1;
    //here we can access both x1 and x2, because x1 is on stack, and it int32 applys copy trait to copy the value to x2
}
  • Rust use ownership to manage memory, there is only one owner for the value in rust, once the value move out of scope, it calls drop to free the memory
  • if you want to keep s2 and s1 above both available, use s2 = s1.clone() to copy the value, copy trait is not allowed for the value implement the Drop trait
fn main(){
    let s1 = String::from("Owner");
    get_length(&s1);
}

fn get_length(s: &String) -> usize {
    s.len()
}
  • we use & to borrow the reference to the string and the ownership now still belong to s1, so when function returns, the s1 is not dropped
  • only one mutable browwer can exist during the same scope, but we allowed multiple immutable references.

7. Slice

let s = String::from("Hello World");
let hello = &s[0..5];

let a = [0,1,2,3,4,5];
let a1_2 = &a[1, 3];
assert_eq!(a1_2, &[1,2]);
  • slice return a reference with pointer and length to the original data
  • above &s[..5] give the same result, and s[..]returs "Hello World"

8. Struct

#[derive(Debug)]
struct Shape {
   isRound : bool,
   Box: i32
}

impl Shape{
   fn new() -> Self{
     Self{
         isRound: false,
         Box: 32,
       }
  }

   fn some_function() {} // use Shape::some_function to access, shape instance can not access like shape.some_function
   fn another_function(&self) {} // use self will allowed instance shape to use shape.another_function
}

let s = Shape::new();
  • #[derive(Debug)] allowed use println!("{:?}", shape) or println!("{:#?}", shape) to print Shape structure
  • also can use dbg! in the line to output debug info like dbg!(&shape)

9. Enum

enum TestObject {
   Empty,
   Square(u32, u32),
   Round(f32),
   Position {x: f32, y: f32}
}

impl TestObject {
   fn display(&self) {}
}

fn get_result(testobj: TestObject) {
    match testobj {
        TestObject::Square(x,y) => println!("square width {}, height {}", x, y),
        TestObject::Round(x) => println!("round radius {}", x),
        TestObject::Position{x, y}=> println!("position is x: {} y: {}", x, y),
        _ => println!("Empty"),
    }
}

let shape = TestObject::Square(6, 5);
let pos = TestObject::Position{x: 3.0, y: 3.0};

get_result(shape);

if let TestObject::Square(x, y) = shape {
	println!("it match the result square width {} height {}", x, y);
}
else
{
	println!("it doesn't match")
}
  • Enum can have values in it's element like Square(u32, u32)
  • Enum can implement function to it
  • use _ to match all other case if don't need the value, otherwise can use other => instead
  • use if let to compare if the same pattern of the enum and get the value of enum
enum Option<T> {
  Some(T),
  None,
}
let mut x: Option<i32> = None;
x = Some(5);
x.is_some(); //true
x.is_none(); //false
for i in x {
 println!("{}", i); //print 5
}

enum Result<T, E> {
	Ok(T),
    Err(E),
}
  • Rust doesn't allowed null value, so if a value need to be null, can set to a Option type, Option can be None, value can be Some(value)

10. Module and Path

--market
    |___fruit
    |     |____apple
    |___meat
          |____pork
          |____chicken
          |____beef
                |___lean

For above structure, market consider as project we created。 so market is the root, we can use crate to represent root. fruit, meat, apple, pork...etc are just different modules under the project.

  • if we are in market, to access apple, we can use absolute path which is crate::fruit::apple, or relative path fruit::apple
  • if we are in chicken, to access pork, we can use absolute path like crate::meat::pork or relative path super::pork, super means parent of current path
  • there is no difference between absolute path or relative path, it depends on the project needs. Most time it suggest to use absolute path
  • use use key word to import the module you need once like use meat::beef , then you can use beef::lean to access in the same content without call meat::beef::lean everywhere
  • you can group import module by use use meat::{pork, chicken, beef}
  • use meat::* allows you import everything under meat
two ways to build this structure in file system:
src/main.rs
src/fruit.rs
src/meat.rs
src/fruit/apple.rs
src/meat/pork.rs

or

src/main.rs
src/fruit/mod.rs
src/meat/mod.rs
src/fruit/apple/mod.rs
src/meat/pork/mod.rs

11.Collections

Vector

//create new vector
let mut v1:Vec<i32> = Vec::new();
//another way to create new vector by using macro
let v2 = vec![1,2,3];
//insert vector
v.push(2);
//get value form vector by using index
let value& i32 = &v1[0];
//get value from vector by using get function
let value: Option<&i32> = v1.get(2)
//iterate vector
for i in &v1 { println!{"{i}"};}
//use Enum to store different type in vector
enum Shape{
    Round(f32),
    Box(u32,u32),
}
let shape = vec![Shape::Round(3.14), Shape::Box(3,4)];
  • use get or index to retrive data from vector depends if the program need to panic when index out of bounds, like v1[1] will panic because no value in index 1, but v1.get(1) will return Option None that you can do something for it.

String

//create empty string
let mut s = String::new();
//create string from value by using from
let s1 = String::from("string 1");
//create string from value by using to_string
let s2 = "string 2".to_string();
//append value to string
s.push_str("append s");
//append char to string
s.push('s');
//add strings by using +
let s3 = s1 + &s2; //output: string 1 string 2
//add strings by using format!
format!("{s2}-{s3}"); //output: string 2 - string 1 string 2
//slice string
let ss = &s2[0..6];
//iterating string by char
for c in s3.chars()
//iterating string by byte
for b in s3.bytes()
  • when use add, s1 is move to s3, s1 will not validate after moving, but s2 is borrowed, so you still can use s2.
  • format borrows both s2 and s3, so both value is valid after
  • string is not allowed to use index to access, because some word may take more then one byte to store, like "你好" will use 4 bytes, index 0 will not return the first full character. By using slice you will take your own risk to get the correct character

HashMap

//create hashmap
let mut count = HashMap::new();
//add hashmap
let cat = String::from("cat");
count.insert(cat, 3);
//read value
count.get("cat").copied().unwrap_or(0);
//iterate hashmap
for (key, value) in &count
//only update value when key is not exist, and it will return a pointer to value 5
count.entry(String::from("cat")).or_insert(5);
  • cat is not valid after insert into HashMap because cat ownership is move to the count HashMap
  • HashMap use SipHash hash function, it is not the fastest but provide more security.

12. Generic Types

//function generic type parameters
add(a: T, b: T)->T{
    a+b
}
let result = add(3, 4);
let result = add(0.3, 0.4);

//generic type in struct definitions
struct Point<T> {
    x: T,
    y: T,
}

//generic type in method
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }

//generic type in enum
enum Option(T) {
    Some(T),
    None,
}
}
  • generic type doesn't affect performance, because during the compile time each generic type will be replace with specific definitions for each instance.

13. Trait

//create a trait
trait DisplayShape {
  fn display (&self)  fn defaultDisplay (&self) {
   println!("defined here don't need to be implment again")
 }
}

//implement trait
impl DisplayShape for Shape {
  // here must implement all the function that not defined in the DisplayShape trait
  fn display (&self) {
  println!{ {"{}"}, self.Box}
 }
}

//impl makes the compiler determine type during the compile period
//it will create different version of the function based on how many trait been implemented.
fn display_shape(val: imp DisplayShape) {
   val.display();
}

// dynamic decission of which function to call at runtime, so compile file is smaller
fn display_shape(val: &dyn DisplayShape) {
   val.display();
}

//using where case for trait pass to function to make it clear reading
fn display_shape<T, U>(t: &T, u: &U)->i32
where T: Display + Clone,
      U: Clone + Debug,
  • not field allowed in trait defining
  • function in trait can have default implementation
  • + in above T, U means tha item must implement both trait

14. Lifetime

//reference with lifetime
&'a i32
//mutable reference with lifetime
&'a mut i32

//life time in function
fn display_shape<'a>(val: &'a str, val: &'a str) -> &'a str

//life time will use smaller life time of the two arguments
//following will show error during compile
let string1 = String::from("this is a outer box");
let result;
{
let string2 = String::from("this is a inner box");
result = display_shape(string1.as_str(), string2.as_str());
}
println!("result is {}", result);

//life time in struct
struct Shape<'a> {
    name: &'a str
}

//life time in method definitions
impl<'a> Shape<'a> {
    fn box(&self) -> (f32,f32{
        (3.1,3.1)
    }
}

//life time with generic type, trait
fn shape_area<'a, T>(s: &'a shape, show: T) -> f32
where T: Display
  • Life time is to make sure the borrowing reference will always valid.
  • Rust has 3 rules allows don't always need put lifetime for the parameters to make it simple. Check the rust book.

15. Error Handling

//unrecoverable error
panic!("crash unrecoverable");

//recoverable error
enum Result<T, E>{
    Ok(T),
    Err(E)
}

//for recoverable error, unwrap will give a panic if error and return result if not panic
File::open("file.txt").unwrap();

//expect will set the panic message
File::open("file.txt").expect("error: file can not open");

//? mark to return the error, if success will continue
File::open("file.txt")?; // equal to return Err(e); when file not open

16. Closure

fn add(x: i32, y: i32) -> i32 {x + y}
//above function is equal to
let add = |x,y| x+y;
//closure can only accept one type
//below will give error
let i = add(3, 4);
let f = add(3.0, 4.0)

//normally in closure, it will borrow the reference
//if want to use move keyword
move || println!("move list here {:?}", list);

17. Iterator

let v = vec![1,2,3];
let v_iter = v.iter();
//loop the iterator
for val in v_iter { println!("got {}", val);}

//next
let mut v1_iter = v.iter();
v1_iter.next() // result is Some(&1)

//sum
let v2_iter = v.iter();
let total = v2_iter.sum(); //result is 1+2+3 = 6, after sum v2_iter is not valid anymore.

//map
let v1:Vec<_> = v.iter().map(|x|x+1).collect(); // result is vec![2,3,4]

//filter
let v2:Vec<_> = v.iter().filter(|x| x>1).collect(); //result is vec![2,3]

18. Smart Pointers

1. Box

//using box to store value in the heap
let b = Box::new(5);
println!("value is {}",b); //print out value is 5

struct Round {
    radius: f32,
}
struct Shape {
    pub round: Box<Round>
}
let r = Box::new(Round{radius: 3.14});
let shape = Shape{round: r,};
println!{"value is {:?}", shape.round.radius}; //print out valu is 3.14

2. Rc

use std::rc::Rc;
struct Round {
    radius: f32,
}
struct Shape {
    pub round: Rc<Round>
}
struct Area {
    pub round: Rc<Round>
}
//create Rc<T>
let r = Rc::new(Round{radius: 3.14});
//using Rc::clone(&r) to pass shared value, this will lead to strong_count + 1
let shape = Shape{round: Rc::clone(&r)};
let area = Area{round: Rc::clone(&r)};
println!("shape radius is {:?}", shape.round.radius);
println!("area radius is {:?}", area.round.radius);
  • Rc allowed multiple ownership for the same value. Rc stands for Reference Count which means each one reference ownership will add one more count, when each value out of scope, the count will reduce by 1, once count is 0, the memory will be freed.
  • Rc is only allowed in single thread
  • Rc only allowed immutable borrows

3. RefCell

//following imutable function will throw error
let m = Vec::new();
m.push(1); // error here becaues m is immutable, can't modify by pushing value into it
//compile way to do is change m to mut m
//also can use RefCell<T>
let m = RefCell::new(Vec::new());
m.borrow_mut().push(1);
println!("m first value is {}", m.borrow()[0]); //output m first value is 1
  • RefCell only allowed using in single thread like Rc

Summary

  • Rc<T> enables multiple owners of same data; Box<T> and RefCell<T> only allowed single owner;
  • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checkd at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime
  • RefCell<T> can mutate the value inside the RefCell<T> even RefCell<T> is immutabl.
  • We can combine Rc::new(RefCell::new) to do muti ownership with mutable value inside.

19. Concurrency

//create a new thread
use std::tread
let t1 = thread::spawn(||{
    for i in 1..100 {
        println!("I'm in another thread")
    }
});

//wait thread to finish
t1.join().unwrap();

//if want to use value in main thread, one way is move the value into thread
let v = vec![1,2,3];
let t2 = thread::spawn(move||{
    println!("vector move in thread {:?}", v);
});

Thread share information by using message system

//create a message chanenel
use std::sync::mpsc;

let (tx, rx) = mpsc::channel();
//can clone tx for another thread
let tx1 = tx.clone();
//comuncation
 let t1 = thread::spawn(move || {
    let val = String::from("hi");
    tx.send(val).unwrap();
 })

 let received = rx.recv().unwrap(); //recevied hi
 t1.join().unwrap();

 thread::spawn(move || {
    let vals = vec![1, 2, 3];
    for val in vals {
        tx.send(val).unwrap();
    }
 });
// rx here didn't call recv function, use iter instead
 for received in rx {
    println!("got : {}", received);
 }
  • mpsc stands for multiple producer, single consumer, that means multiple sending but only one receving end
  • tx, rx stands for transmitter and receiver.
  • tx needs to move to thread, so thread own it to send the message
  • recv and send function both return Result<T, E> type, if receiver is droped or send closed, will return error.

Thread share information by using shared data

//create mutex to protect shared data, only one thread can using the data
use std::sync::Mutex;
use std::thread;

let m = Mutex::new(5);

//read the mutex data
let mut num = m.lock().unwrap();
*num = 6; //number 5 become 6 here now.

//share mutex with more threads by using Arc
//Arc is similar to Rc but can be used in muti thread
use std::sync::Arc

let counter = Arc::new(Mutex::new(0));

let counter1 = Arc::clone(&counter);
let counter2 = Arc::clone(&counter);

let t1 = thread::spawn(move || {
    let mut num = counter1.lock().unwrap();
    *num += 1;
});

t1.join().unwrap();

let t2 = thread::spawn(move || {
    let mut num = counter2.lock().unwrap();
    *num += 1;
});
t2.join().unwrap();
//num here is 2 now