Skip to content

Instantly share code, notes, and snippets.

@umutseven92
Created September 28, 2021 15:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save umutseven92/c5e814dc7c3982c34ff7a2527d86dc3f to your computer and use it in GitHub Desktop.
Save umutseven92/c5e814dc7c3982c34ff7a2527d86dc3f to your computer and use it in GitHub Desktop.
Rust Cliff Notes

Rust

Data Types

  • The default type is f64 because on modern CPUs it’s roughly the same speed as f32 but is capable of more precision.

  • Declaring arrays: let a: [i32; 5] = [1, 2, 3, 4, 5];

  • isize and usize types depend on the kind of computer your program is running on: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.

  • Unlike languages such as Ruby and JavaScript, Rust will not automatically try to convert non-Boolean types to a Boolean.

  • Rust does not have nulls, but it has Option:

     fn plus_one(x: Option<i32>) -> Option<i32> {
          match x {
              None => None,
              Some(i) => Some(i + 1),
          }
      }

Syntax

  • Return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.

  • If in a let statement: let number = if condition { 5 } else { 6 };

  • Returning from loops:

    let result = loop {
          counter += 1;
    
          if counter == 10 {
              break counter * 2;
          }
      };
  • if let is syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values:

    if let Coin::Quarter(state) = coin {
          println!("State quarter from {:?}!", state);
      } else {
          count += 1;
      }

Memory Management & Ownership

  • Stack & Heap:
    • All data stored on the stack must have a known, fixed size.
    • Data with an unknown size (like String) at compile time or a size that might change must be stored on the heap instead.
    • The heap is less organized: when you put data on the heap, you request a certain amount of space.
    • The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location. This process is called allocating on the heap and is sometimes abbreviated as just allocating.
    • Pushing values onto the stack is not considered allocating. Because the pointer is a known, fixed size, you can store the pointer on the stack, but when you want the actual data, you must follow the pointer.
    • Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data; that location is always at the top of the stack. Comparatively, allocating space on the heap requires more work, because the allocator must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.
    • Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory.
    • 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.
  • let s = String::from("hello");
    • The double colon (::) is an operator that allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.
  • When a variable goes out of scope, Rust calls a special function for us. This function is called drop, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.
    • In C++, this pattern of deallocating resources at the end of an item’s lifetime is sometimes called Resource Acquisition Is Initialization (RAII).
  • Rust will never automatically create “deep” copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.
  • If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone.
  • 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 (e.g. Stack only types can be copied).
  • Passing a value to a function & returning a value from a function transfers ownership.
    • To pass a value without transfering ownership, we can use references.
  • Having references as function parameters is called borrowing.
  • Just as variables are immutable by default, so are references.
    • You can mark references as mutable, just like variables.
    • You can have only one mutable reference to a particular piece of data in a particular scope; this prevents data races.
    • Same goes for combining mutable and immutable references.
  • String slice: let hello = &s[..5];

 Structs

  • The entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.

  • Field init shorthand:

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

       let user2 = User {
          email: String::from("another@example.com"),
          username: String::from("anotherusername567"),
          ..user1
      };
  • Tuple structs: struct Color(i32, i32, i32);

  • Putting the specifier :? and :#? 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.
    • To do that, we add the annotation #[derive(Debug)] just before the struct definition.
  • Methods are different from functions in that they’re defined within the context of a struct.

  • Method syntax:

    impl Rectangle {
      fn area(&self) -> u32 {
          self.width * self.height
      }
    }
  • If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self as the first parameter.

    • Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.
  • We’re allowed to define functions within impl blocks that don’t take self as a parameter. These are called associated functions because they’re associated with the struct.

    • Associated functions are often used for constructors that will return a new instance of the struct.
    • To call this associated function, we use the :: syntax with the struct name.
  • We're allowed to have multiple impl blocks.

Enums

  • Enums can have values:

    enum IpAddr {
          V4(u8, u8),
          V6(String),
      }
  • We’re also able to define methods on enums, just like structs.

 Modules & Crates

  • To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub and use.
  • We can use nested paths to bring the same items into scope in one line: use std::{cmp::Ordering, io};

 Collections

 Vectors

  • Declare vector with initial values: let v = vec![1, 2, 3];
  • Getting values from vector:
    • let third: &i32 = &v[2];: Will panic if out of bounds,
    • v.get(2): Will return an Option.

 Strings

  • to_string method is available on any type that implements the Display trait, like string literals do.
  • let s = format!("{}-{}-{}", s1, s2, s3);

Error Handling

  • When the panic! macro executes, your program will print a failure message, unwind and clean up the stack, and then quit.

  • Matching on Result:

    let f = match f {
          Ok(file) => file,
          Err(error) => panic!("Problem opening the file: {:?}", error),
      };
  • Matching with multiple errors:

      let f = match f {
          Ok(file) => file,
          Err(error) => match error.kind() {
              ErrorKind::NotFound => match File::create("hello.txt") {
                  Ok(fc) => fc,
                  Err(e) => panic!("Problem creating the file: {:?}", e),
              },
              other_error => {
                  panic!("Problem opening the file: {:?}", other_error)
              }
          },
      };
  • If the Result value is the Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro for us.

  • expect, which is similar to unwrap, lets us also choose the panic! error message.

  • ? operator:

    • If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function as if we had used the return keyword so the error value gets propagated to the calling code.
    • Used when propogating errors.

Traits

  • Defining a trait (interface):

    pub trait Summary {
      fn summarize(&self) -> String;  
    }
  • Implementing a trait:

    impl Summary for NewsArticle {
      fn summarize(&self) -> String {
          format!("{}, by {} ({})", self.headline, self.author, self.location)
      }
    }
  • Passing the trait as a parameter:

    pub fn notify(item: &impl Summary) {
      println!("Breaking news! {}", item.summarize());
    }
  • Trait bounds:

    • Single: fn largest<T: PartialOrd>(list: &[T]) -> T,

    • Multiple:

      fn some_function<T, U>(t: &T, u: &U) -> i32
        where T: Display + Clone,
              U: Clone + Debug { }

Lifetimes

  • Every reference in Rust has a lifetime, which is the scope for which that reference is valid.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment