rust

Local Documentation

  • rustup doc brings up all the local documentation including
    • The Rust book
    • Rust by Example
    • Cargo book
  • Inside your project, cargo doc --open opens your project's documentation
    • Without --open will just generate the documentation, which can be potentially shared with others as required.

Data Types

  • Arrays : Fixed size
    • When defining say days of the week, months of the year etc.
    • Initialize as :
      • let a = [3; 5]; is same as let a = [3, 3, 3, 3, 3];
  • Vector : Like array but can shrink and/or grow
  • If unsure, use Vector
  • Use new() or vec! to create
let mut numbers: Vec<i32> = Vec::new();
let mut magic_numbers = vec![7i32, 42, 47, 45, 54];
  • push to add at the end, pop remove from the end

Enum

  • Similar to Enum in python
  • But variants can contain data like struct
  • Data fields can be named/labeled (like title: String) or unlabeled
    • labeled ones improve readability though

Control Flow

  • rust does not have truthy values. e.g. if x is 1, then if x does not compile. x must be boolean

  • loop can have optional label. See the documentation

'outer: loop {
.   ...
  loop {  // inner, unlabeled loop
    ...
    if some_condition {
      break; // Will break from inner/unlabeled loop
    }
    ...
    ...
    if some_other_condition {
      break 'outer'; // will break out of the outer loop
    }
    ...
  }
}

Statements Vs Expressions

Statements do not return a value, expressions do.

C and Ruby, where the assignment returns the value of the assignment.
In those languages, you can write x = y = 6 and have both x and y have
the value 6; that is not the case in Rust.

In a function, last expression is also a return value.

If you add a semicolon to the end of an expression, you turn it into
a statement, and it will then not return a value.

Three types of iterators

  • iter() : Gives read-only reference inside the loop
    • Collection available for reuse after the loop.
  • into_iter() : Gives ownership of the element inside the loop
    • Collection has been consumed it is no longer available for reuse as it has
      been 'moved' within the loop.
  • iter_mut : Gives mutable reference inside the loop
    • Collection can be modified in place (while being iterated over)

See the reference

Iterator consumer

  • for loop
  • elements.iter().for_each( | element | println!("{}", element));
  • collect()
    • collect() automagically decides what data structure to output to, based on
      • The return type.
      • return type of the function if collect is the last statement
      • explicit instruction like collect::<Vec<String>>()
  • find : Calls next till it finds an element that returns truthy value
    • So the loop may not complete, if any of the element matches the condition
    • If not found, returns None
    • else Some(value)

Iterator Adaptors

  • .map(|element| <do something to the element)
    • Modifies element but does not call next
    • So after .map, .for_each must be called
    • output of map is then passed to for_each
  • map_or : Takes 2 args
    • First is returned if it gets None (From say find)
    • Second is used if it gets Some(value)

Ownership

  • Cloning avoids Moves
  • Useful when we need to reference the "original" data after it the ownership is moved
  • .clone create a copy (which is used to move the oownership,leaving the original intact)

Slice

We 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.

&str is called a String Slice

  • There is also Vector slice. Same as String slice.
  • We can point to just portion of the Vector
  • So when passing a Vector, prefer to use &[<data type of contents of a vector>]
    e.g. &[String]

Implementations

  • impl keyword is used to assign methods (mostly) to struct
  • return type Self indicates, it will return the same type
    for which the implementation is being added.
  • Associated function : Directly associated with struct (Mostly new or constructor)
    • Called using Struct::function()
  • Method: Associated with instance of struct
    • Called using instance.method()
    • First arg is &self

Mutability

  • Indicate mutability using mut keyword for the variable.
  • Mutable references :
    • use &mut type_goes_here in the function definition to indicate function expects mutable reference.
    • Call using &mut variable_goes_here

Sample code:

fn change_num(number: &mut usize) {
    *number = 1;
}
fn main() {
    let mut num: usize = 10;
    println!("{:#?}", num);
    change_num(&mut num);
    println!("{:#?}", num);
}

Rules

  • You can make a mutable reference to a value only if there are no read-only references currently in use. One mutable ref to a value can exist at a time
  • You can't mutate a value through the owner when any ref (mutable or immutable) to the value exists

Error Handling

When return value is Option (or Result), preferred ways to deal with it are :

  • match for Some and None explicitly. (Or Ok and Err)

    • We can deal with the error in a meaningful way (like having fallback value)
  • if let and else

  • unwrap : By itself, Avoid using unless temporary code or debugging.

  • expect : When you expect the value to be present, like environment variable

  • unwrap_or : If there can be a fallback value (like say missing config)

  • ? when we can not handle the error in the current function (Caller may have more information to deal with the error, so no need to panic in current function)

  • Error::other creates new instance of Error (like new())

? operator

  • Error is propagated back to the caller.
  • Our function does not need to handle the error. It assumes happy path, but if there is an error, using ? will short circuit the execution, and will return from that point to the caller.
  • Also called as Try operator

Generics

struct State<T> {
    data: T,
}

let state_int: State<i32> = State { data: 5 };  // State holding an integer
let state_str: State<&str> = State { data: "hello" };  // State holding a string

Here State is a generic type. Defining it as State<i32> makes it specific

Resources

Strings

  • Use String when
    • Want ownership
    • Want text that can grow or shrink
  • &String
    • rarely used
    • Rust automatically converts it to &str (String slice)
  • Use &str
    • No ownership needed
    • Need part/slice of the string