Skip to content

Instantly share code, notes, and snippets.

@anican
Created September 28, 2021 20:17
Show Gist options
  • Select an option

  • Save anican/4b2bfaf25132c005fb62ead2b07ec663 to your computer and use it in GitHub Desktop.

Select an option

Save anican/4b2bfaf25132c005fb62ead2b07ec663 to your computer and use it in GitHub Desktop.
Rust for cse453

A Quick Introduction to Rust (for CS undergraduates)

Why Rust?

Rust is a systems programming language that is designed to catch at compile time many types of type confusion, memory allocation, and concurrency race condition bugs that are difficult to find solely through runtime testing and debugging. Once mastered, it is a productivity tool - allowing you to spend more time on improving the design or functionality of your systems, and less time on debugging. Another important aspect of Rust is that it incurs little overhead compared to native C programming. Like Java and Go, Rust is type safe - the compiler or runtime will catch and prevent misuse of memory. However, unlike these other languages, there is no background garbage collector. Rather, the programmer adds information to the program to allow the compiler to know exactly when data can be reclaimed. In addition, data structures are formatted in memory in a C-like style. Of course, Rust also comes with a suite of generic data structures like maps. The combination of no garbage collection and visible memory formatting means that it is reasonable to use Rust for systems code where predictable, high performance is essential.
Rust achieves this through an idea called “ownership safety”. The idea is that every byte of allocated memory (on the heap or the stack) must have a compiler-checkable lifetime -- when it is live and when it can be reclaimed. This includes anytime we take a reference or pointer to an allocated item, anytime we store a reference of one data structure in another data structure, or anytime we pass a reference to a procedure. The Rust compiler understands both mutable and immutable data structures, as well as read-only versus read-write references. This does require some extra care when programming. In practice however, if you want your C code to be free of memory and concurrency errors, that extra care is essential. All the Rust compiler is doing is providing an automated check that you are using a coding discipline that will eliminate whole classes of bugs. There are two flavors of Rust: safe and unsafe. Safe Rust enforces type and ownership safety. Unsafe Rust allows the programmer to evade some of Rust’s restrictions. We’re purists, so we’ll be using Safe Rust.

What to expect

Rust is in many ways similar to C and Java, but Rust is not an object-oriented language. We will assume you are already familiar with those languages. With the exception of ownership safety, most things in Rust have direct analogues to C and Java, and so we’ll start by giving you a mapping of concepts between the languages. However, this is meant as only a very brief introduction to allow you to get started. We are not going to try to cover every topic, and where appropriate, we’ll just include pointers to an explanation. If you would like to explore more, we recommend Rust’s official tutorial. As a warning, it is long, in part because it does not assume any specific prior programming experience (e.g., so it needs to explain an enum to those who haven’t seen one before). However, it is the single best resource for rigorous Rust learning. Other resources are also available, like Rust by Example” (recommend,) “Rust for Java/C++ programmers,” or “30min Rust.”

The majority of the time, the Rust compiler will be your best teacher. That is, try things out, and the compiler will tell you when you need to add clarity. With Rust's super powerful compiler and some patience with the verboseness of Rust compile-time error messages, you will solve most of the problems and learn a lot from the compiler!

Note that all “FYI” parts are beyond the scope. They are only fun to know :)

Get started – installation & hello world

Follow Chapter 1 in Rust’s official guide: Getting Started.

Old friends from Java/C in a demo

Let’s go through concepts with a demo.

Java:

import java.util.HashSet;
import java.util.Set;

class Student {
  public String name;
  private int idNumber;
  private Set<String> classTaken;

  public Student(String name, int idNumber) {
    this.name = name;
    this.idNumber = idNumber;
    this.classTaken = new HashSet<>();
  }

  public void sayHello() {
    System.out.println("My name is " + name + " and my student id number is " + idNumber);
    System.out.println("I have taken " + String.join(", ", classTaken));
  }

  public void takeClass(String className) {
    classTaken.add(className);
  }
  
  public static void main(String[] args) {
    Student student = new Student("haha", 1234);
    student.takeClass("CSE 143");
    student.takeClass("CSE 453");
    student.sayHello();
  }
}

Rust:

// note “use” instead of “import”
use itertools::Itertools;
use std::collections::HashSet;

// Rust has C-like structures rather than Java-like classes, and structs can be public or private
// type information comes after the variable name, commas as separators between struct fields
pub struct Student {
    pub name: String, // public field
    id_number: i32,		// private field by default
    class_taken: HashSet<String>,
}

// this defines a set of methods on the object
impl Student {
    fn new(name: String, id: i32) -> Student { // the constructor with two arguments
        Student { //allocates a Student with fields and returns the result
            name,	// this is a shortcut for `name: name`
            id_number: id,
            class_taken: HashSet::new(),
        }
    }

    fn say_hello(&self) {
        println!(
            "My name is {} and my student id number is {}",
            self.name, self.id_number
        );
        println!("I have taken {}", self.class_taken.iter().join(", "));
    }

    fn take_class(&mut self, class_name: String) {
        self.class_taken.insert(class_name);
    }
}

// like C, Rust expects a main function in every program
fn main() {
    // or explicitly declare the type: `let mut student: Student = ...`
    let mut student = Student::new(String::from("haha"), 1234); 
    student.take_class(String::from("CSE 143"));
    student.take_class(String::from("CSE 453"));
    student.say_hello();
}

output:

My name is haha and my student id number is 1234
I have taken CSE 453, CSE 143

The small demo covers many key concepts in Rust, let’s go over them one by one.

  • Structs vs. Objects

    Rust is not an object-oriented language but, as with C, you can use it in an object-oriented manner. Rust does not have objects. Instead, Rust has structs similar to those in C. However, in Rust, you can define methods for structs.

  • use vs. import

    These are similar, except that Rust uses :: to separate namespaces.

  • Variable Declaration

Both languages are statically typed – every value has a type that is checked at compile time. Java places types before variable names while Rust puts them afterwards. However, the most significant difference is that Rust does not require you to declare the type if the compiler can infer the type instead (e.g., from how the variable is used). You may find examples of mandatory and omittable type declarations in the demo. Types are always needed for struct and function definitions.

  • Mutable vs. Immutable

    As you may notice, there is mut as part of some declarations. In Rust, variables are immutable by default. If you want to be able to modify a struct, like adding a new element to a list, you must declare it as mut. (FYI, the above “immutable” is indeed “exteriorly immutable”. A type has interior mutability if its internal state can be changed through a shared reference to it. We’ll talk about it later.) Mutability is important for ownership safety - an immutable object can be silently copied by the compiler when needed for a function call, but (obviously) there’s an important difference between a mutable object and its copy.

  • Public/Private

    Anything that can be either public or private (e.g. structs, enums, functions, modules, etc.) is private by default. E.g., struct Haha is private but pub struct Haha is public.

  • Return Statement

    Java mandates the keyword return but Rust does not. Rather, Rust takes the final expression of the function as its return value. As an expression (x), and not a statement (return x;), you do not append a semicolon. You can use an explicit return to return from the middle (or end) of a function; that makes it a statement, and so it takes a semicolon.

  • Function Declaration

    Rust’s function declaration looks very similar to that of Python.

    • Parameter declarations requires types.
    • Return type is after ->
    • self / mut self / &self / &mut self For functions defined on structs (similar to object methods, though “object method” is not a Rust terminology), self must be the first argument in the definition, similar to Python. The & is similar a pointer in C. Rust uses * to dereference too, though there are cases you don’t need to explicitly dereference (FYI, like when passing a pointer to a function call. I.e., Deref Coercions.) If you’re not familiar with Python, all these four self’s are the instance itself.
  • println! – the most frequently used macro Rust’s println is similar to Python’s .format(). If you’re not familiar with python, each {} is like %s, %d, or so in C, except Rust knows the type, so {} is for any type. Also, Rust’s macro is very similar to C’s macro, and macros have a exclamation mark as a suffix.

  • String vs. String Literal

    String in Rust is a struct, yet string literals are not. String literals have type &str and, like in C, are stored in the static region of the program executable, while String’s are dynamically allocated on the heap. To convert a &str to a String, it’s recommended to use String::from("a literal”). (FYI, there are other ways like .to_string() or .to_owned.) Note that Strings and string literals are stored as unicode, so it much differs from C: strings can't be indexed and you need to use an explicit iterator (e.g. .chars(), .bytes()) to walk through a String.

  • Object Constructor vs. Struct Initialization

    Rust does not officially have constructors, even though you may create a method new() as if it is a constructor. The struct initialization is like that in C.

Beyond Java/C

The following is mostly a digest of Rust by Example with some of our words. It serves as a brief introduction (or a refresher when you want to check back) to relatively unique topics in Rust that cannot be found in Java/C. To learn more, follow the links under each topic.

No GC, No malloc/free – Ownership, Borrowing & Lifetime

The memory management system in Rust is the one thing that most differentiates Rust from Java or C. Memory is governed by a new concept (FYI, well, not so new if you know RAII) — ownership. Ownership is also leveraged for concurrency safety which we discuss below. Unlike Java’s automated memory management, there is no garbage collector - the programmer is in full control as to when allocated data can be reclaimed. Unlike C, however, the compiler does the work - the programmer never calls malloc or free. Thus, the ownership system is like malloc/free with a memory safeguard done by the compiler. This is key to Rust being “blazingly fast.”

There are two ways to learn this topic:

  1. If you want a comprehensive understanding of ownership and lifetime (recommended), please check out the official book Ownership and Lifetime.
  2. If you are familiar with RAII or want to get hands-on faster, check out Rust by Example: Scoping Rules.
Trait

Rust traits are similar to interfaces in Java. Say we have a trait

pub trait Summary {
    fn summarize(&self) -> String;
}

We may impl it for structs, like we can implements an interface for a class in Java.

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Derive

The compiler is capable of providing basic implementations for some traits via the #[derive] attribute. These traits can still be manually implemented if more complex behavior is required. For example,

#[derive(Debug)]
struct Inches(i32);

Debug is a trait to format a value using the {:?} formatter.

Iterator

The Iterator trait is used to implement iterators over collections such as arrays, vectors, hash maps, and so on.

The trait only requires a single method called next which returns the next element if it exists. Iterator is implemented for arrays, ranges, and most collections in the standard library. For custom data types, it must be defined manually in an impl block.

As a point of convenience for common situations, the for construct turns some collections into iterators using the .into_iter() method.

struct Fibonacci {
    curr: u32,
    next: u32,
}

// Implement `Iterator` for `Fibonacci`.
// The `Iterator` trait only requires a method to be defined for the `next` element.
impl Iterator for Fibonacci {
    // We can refer to this type using Self::Item
    type Item = u32;
    
    // Here, we define the sequence using `.curr` and `.next`.
    // The return type is `Option<T>`:
    //     * When the `Iterator` is finished, `None` is returned.
    //     * Otherwise, the next value is wrapped in `Some` and returned.
    // We use Self::Item in the return type, so we can change
    // the type without having to update the function signatures.
    fn next(&mut self) -> Option<Self::Item> {
        let new_next = self.curr + self.next;

        self.curr = self.next;
        self.next = new_next;

        // Since there's no endpoint to a Fibonacci sequence, the `Iterator` 
        // will never return `None`, and `Some` is always returned.
        Some(self.curr)
    }
}

// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
    Fibonacci { curr: 0, next: 1 }
}

fn main() {
    // `0..3` is an `Iterator` that generates: 0, 1, and 2.
    let mut sequence = 0..3;

    println!("Four consecutive `next` calls on 0..3");
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());

    // `for` works through an `Iterator` until it returns `None`.
    // Each `Some` value is unwrapped and bound to a variable (here, `i`).
    println!("Iterate through 0..3 using `for`");
    for i in 0..3 {
        println!("> {}", i);
    }

    // The `take(n)` method reduces an `Iterator` to its first `n` terms.
    println!("The first four terms of the Fibonacci sequence are: ");
    for i in fibonacci().take(4) {
        println!("> {}", i);
    }

    // The `skip(n)` method shortens an `Iterator` by dropping its first `n` terms.
    println!("The next four terms of the Fibonacci sequence are: ");
    for i in fibonacci().skip(4).take(4) {
        println!("> {}", i);
    }

    let array = [1u32, 3, 3, 7];

    // The `iter` method produces an `Iterator` over an array/slice.
    println!("Iterate the following array {:?}", &array);
    for i in array.iter() {
        println!("> {}", i);
    }
}
Clone

When dealing with resources, the default behavior is to move them during assignments or function calls. However, sometimes we need to make a copy of the resource as well.

The Clone trait helps us do exactly this. Most commonly, we use the .clone() method defined by the Clone trait.

// A unit struct without resources
#[derive(Debug, Clone, Copy)]
struct Unit;

// A tuple struct with resources that implements the `Clone` trait
#[derive(Clone, Debug)]
struct Pair(Box<i32>, Box<i32>);
More…

To learn more, check out Rust By Example: Trait.

Enums & Pattern Matching

Enums in Rust are like enums in Java

enum Work {
    Civilian,
    Soldier,
}

and we use them like in Java, though a match must cover all possible cases in Rust.

let work = Work::Civilian;

match work {
  Work::Civilian => println!("Civilians work!"),
  Work::Soldier  => println!("Soldiers fight!"),
}

Notice that enums may store values.

// Create an `enum` to classify a web event. Note how both
// names and type information together specify the variant:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Each is different and independent.
enum WebEvent {
    // An `enum` may either be `unit-like`,
    PageLoad,
    PageUnload,
    // like tuple structs,
    KeyPress(char),
    Paste(String),
    // or c-like structures.
    Click { x: i64, y: i64 },
}

// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("page loaded"),
        WebEvent::PageUnload => println!("page unloaded"),
        // Destructure `c` from inside the `enum`.
        WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
        WebEvent::Paste(s) => println!("pasted \"{}\".", s),
        // Destructure `Click` into `x` and `y`.
        WebEvent::Click { x, y } => {
            println!("clicked at x={}, y={}.", x, y);
        },
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('x');
    // `to_owned()` creates an owned `String` from a string slice.
    let pasted  = WebEvent::Paste("my text".to_owned());
    let click   = WebEvent::Click { x: 20, y: 80 };
    let load    = WebEvent::PageLoad;
    let unload  = WebEvent::PageUnload;

    inspect(pressed);
    inspect(pasted);
    inspect(click);
    inspect(load);
    inspect(unload);
}

For more information, see Rust by Examples: Enums.

Crates

A crate is a compilation unit in Rust. Whenever rustc some_file.rs is called, some_file.rs is treated as the crate file. Like in C, the crate file is linked/inserted to where mods inside the file is used.

For examples, check out the short chapter Rust by Example: Crates.

Error Handling
Panic

Calling panic() prints an error message and terminates the program. This is the simplest way of handling an error.

fn drink(beverage: &str) {
    // You shouldn't drink too much sugary beverages.
    if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("Some refreshing {} is all I need.", beverage);
}

fn main() {
    drink("water");
    drink("lemonade");
}
Option & unwrap

Rust doesn’t allow unhandled null values. To enforce handling null values, an enum called Option<T> in the std library is used when absence is a possibility. It manifests itself as one of two "options":

  • Some(T): An element of type T was found.
  • None: No element was found.

unwrap, when applied to an Option, returns the real value in Some if it’s a Some and panics if it’s a None.

// The adult has seen it all, and can handle any drink well.
// All drinks are handled explicitly using `match`.
fn give_adult(drink: Option<&str>) {
    // Specify a course of action for each case.
    match drink {
        Some("lemonade") => println!("Yuck! Too sugary."),
        Some(inner)   => println!("{}? How nice.", inner),
        None          => println!("No drink? Oh well."),
    }
}

// Others will `panic` before drinking sugary drinks.
// All drinks are handled implicitly using `unwrap`.
fn drink(drink: Option<&str>) {
    // `unwrap` returns a `panic` when it receives a `None`.
    let inside = drink.unwrap();
    if inside == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("I love {}s!!!!!", inside);
}
Result

Result<T, E> is similar to Option<T> except that an error of type E is in place of None. We can also apply unwrap to Result.

fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
    // Let's try using `unwrap()` to get the number out. Will it bite us?
    let first_number = first_number_str.parse::<i32>().unwrap();
    let second_number = second_number_str.parse::<i32>().unwrap();
    first_number * second_number
}

fn main() {
    let twenty = multiply("10", "2");
    println!("double is {}", twenty);

    let tt = multiply("t", "2");
    println!("double is {}", tt);
}
? Operator

? is almost exactly equivalent to an unwrap which returns instead of panicking on Errs. It’s a convenient way to fail-fast and reduce code verboseness.

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = first_number_str.parse::<i32>()?;
    let second_number = second_number_str.parse::<i32>()?;

    Ok(first_number * second_number)
}
More…

For more information, like iterating over Results, check out Rust by Example: Error Handling.

Testing
Unit Tests

Most unit tests go into a tests mod with the #[cfg(test)] attribute. Test functions are marked with the #[test] attribute. Unit tests usually live in the same file as the mod to be tested.

Tests fail when something in the test function panics. There are some helper macros:

  • assert!(expression) - panics if expression evaluates to false.
  • assert_eq!(left, right) and assert_ne!(left, right) - testing left and right expressions for equality and inequality respectively.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// This is a really bad adding function, its purpose is to fail in this
// example.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        // This assert would fire and test will fail.
        // Please note, that private functions can be tested too!
        assert_eq!(bad_add(1, 2), 3);
    }
}

To run all test, type cargo test in your terminal. To run a particular test, for example test_add, use cargo test test_add.

Integration Tests

Integration tests usually live in a separate file like tests/integration_test.ts.

Cargo looks for integration tests in the tests directory next to src.

File src/lib.rs:

// Define this in a crate called `adder`.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

File with test: tests/integration_test.rs:

#[test]
fn test_add() {
    assert_eq!(adder::add(3, 2), 5);
}

Integration tests are also run by cargo test.

Closure

Closures are functions that can capture the enclosing environment. For example, a closure that captures the x variable:

|val| val + x

The syntax and capabilities of closures make them very convenient for on the fly usage. Calling a closure is exactly like calling a function. Some characteristics of closures include:

  • using || instead of () around input variables.
  • optional body delimination ({}) for a single expression (mandatory otherwise).
  • the ability to capture the outer environment variables.
fn main() {
    // Increment via closures and functions.
    fn function(i: i32) -> i32 { i + 1 }

    // Closures are anonymous, here we are binding them to references
    // Annotation is identical to function annotation but is optional
    // as are the `{}` wrapping the body. These nameless functions
    // are assigned to appropriately named variables.
    let closure_annotated = |i: i32| -> i32 { i + 1 };
    let closure_inferred  = |i     |          i + 1  ;

    let i = 1;
    // Call the function and closures.
    println!("function: {}", function(i));
    println!("closure_annotated: {}", closure_annotated(i));
    println!("closure_inferred: {}", closure_inferred(i));

    // A closure taking no arguments which returns an `i32`.
    // The return type is inferred.
    let one = || 1;
    println!("closure returning one: {}", one());

}
Smart Pointer
Box

All values in Rust are stack allocated by default. Values can be boxed (allocated on the heap) by creating a Box<T>. A box is a smart pointer to a heap allocated value of type T. When a box goes out of scope, its destructor is called, the inner object is destroyed, and the memory on the heap is freed.

Boxed values can be dereferenced using the * operator; this removes one layer of indirection.

#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

let boxed_point: Box<Point> = Box::new(Point { x: 0.0, y: 0.0 })
let unboxed_point: Point = *boxed_point;
Rc

Like reference counted smart pointers in C++, when multiple ownership is needed, Rc(Reference Counting) can be used. Rc keeps track of the number of the references which means the number of owners of the value wrapped inside an Rc.

Reference count of an Rc increases by 1 whenever an Rc is cloned, and decreases by 1 whenever one cloned Rc is dropped out of the scope. When an Rc's reference count becomes zero, which means there are no owners remaining, both the Rc and the value are all dropped.

Cloning an Rc never performs a deep copy. Cloning creates just another pointer to the wrapped value, and increments the count.

use std::rc::Rc;

fn main() {
    let rc_examples = "Rc examples".to_string();
    {
        println!("--- rc_a is created ---");
        
        let rc_a: Rc<String> = Rc::new(rc_examples);
        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
        
        {
            println!("--- rc_a is cloned to rc_b ---");
            
            let rc_b: Rc<String> = Rc::clone(&rc_a);
            println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));
            println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
            
            // Two `Rc`s are equal if their inner values are equal
            println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));
            
            // We can use methods of a value directly
            println!("Length of the value inside rc_a: {}", rc_a.len());
            println!("Value of rc_b: {}", rc_b);
            
            println!("--- rc_b is dropped out of scope ---");
        }
        
        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
        
        println!("--- rc_a is dropped out of scope ---");
    }
    
    // Error! `rc_examples` already moved into `rc_a`
    // And when `rc_a` is dropped, `rc_examples` is dropped together
    // println!("rc_examples: {}", rc_examples);
    // TODO ^ Try uncommenting this line
}

Output

Reference Count of rc_a: 1
--- rc_a is cloned to rc_b ---
Reference Count of rc_b: 2
Reference Count of rc_a: 2
rc_a and rc_b are equal: true
Length of the value inside rc_a: 11
Value of rc_b: Rc examples
--- rc_b is dropped out of scope ---
Reference Count of rc_a: 1
--- rc_a is dropped out of scope ---
Arc

When shared ownership between threads is needed, Arc(Atomic Reference Counted) can be used. This struct, via the Clone implementation can create a reference pointer for the location of a value in the memory heap while increasing the reference counter. As it shares ownership between threads, when the last reference pointer to a value is out of scope, the variable is dropped.

fn main() {
    use std::sync::Arc;
    use std::thread;
    
    // This variable declaration is where its value is specified.
    let apple = Arc::new("the same apple");
    
    for _ in 0..10 {
        // Here there is no value specification as it is a pointer to a reference
        // in the memory heap.
        let apple = Arc::clone(&apple);
    
        thread::spawn(move || { // spawn a thread and move (transfer ownsership) the closure to the new thread
            // As Arc was used, threads can be spawned using the value allocated
            // in the Arc variable pointer's location.
            println!("{:?}", apple);
        });
    }
}
RefCell

RefCell<T> is another way to “wrap” a value. Rc<T>is similar to RefCell<T>, but Rc<T> is checked at compile time while RefCell<T> is checked at runtime. That means the former raises compile errors while the latter panics during runtime if errors occur.

The common ways to get value stored in RefCell<T> are via .borrow() or .borrow_mut() where the former one provides an immutable reference to the value inside while the latter provides a mutable reference.

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
      
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

We highly recommend going through the Rust Book: RefCell as RefCell<T> is important yet relatively hard to grasp with a short intro.

Comparison between Box<T>, Rc<T>, or RefCell<T>
  • Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.
  • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.
  • Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.
Generics

Generics in Rust are like those in Java. There are many places where generics may play a role. For a comprehensive list, please check Rust By Example: Generics. Here lists some basic use cases:

  • Function

    fn foo<T>(arg: T) { ... }

    so arg is of type T

  • impl

    struct S; // Concrete type `S`
    struct GenericVal<T>(T); // Generic type `GenericVal`
    
    // impl of GenericVal where we explicitly specify type parameters:
    impl GenericVal<f32> {} // Specify `f32`
    impl GenericVal<S> {} // Specify `S` as defined above
    
    // `<T>` Must precede the type to remain generic
    impl<T> GenericVal<T> {}
  • Trait

    Of course traits can also be generic. Here we define one which reimplements the Drop trait as a generic method to drop itself and an input.

    // Non-copyable types.
    struct Empty;
    struct Null;
    
    // A trait generic over `T`.
    trait DoubleDrop<T> {
        // Define a method on the caller type which takes an
        // additional single parameter `T` and does nothing with it.
        fn double_drop(self, _: T);
    }
    
    // Implement `DoubleDrop<T>` for any generic parameter `T` and
    // caller `U`.
    impl<T, U> DoubleDrop<T> for U {
        // This method takes ownership of both passed arguments,
        // deallocating both.
        fn double_drop(self, _: T) {}
    }
    
    fn main() {
        let empty = Empty;
        let null  = Null;
    
        // Deallocate `empty` and `null`.
        empty.double_drop(null);
    
        //empty;
        //null;
        // ^ TODO: Try uncommenting these lines.
    }
Cargo

cargo is the official Rust package management tool. You don’t need to be a master of cargo for this course. Following lab specs should suffice, but if you are curious, Rust by Example: Cargo gives a concise introduction, so please read it. There are also great third-party libraries that may help implementations. Here is a list of some popular ones.

Error handling
  • anyhow (apps)
  • thiserror (libs)
Serialization/deserialization
  • serde
CLI
  • clap
  • structopt
HTTP
  • http-types
Concurrency

Concurrency is a major topic for 453 and we don’t want you to miss any detail. Please check out Rust Book: Concurrency.

5. FAQ

Why I cannot impl certain traits? It’s subject to the restriction that you can't implement a foreign trait on a foreign type.

  1. self trait on foreign type (legal)
  2. foreign trait on self type (legal)
  3. foreign trait on foreign type (ILLEGAL)

6. Acknowledgement

Some examples and descriptions are from Rust by Example and Rust Book.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment