Rust CheatSheet
Table of Contents
- 1. Hello World!
- 2. Formatted text:
format!, println!, dbg! - 3. The “::” Notation: Types as Namespaces and General Imports
- 4. A First look at Ownership: Why
let x = ⋯; f(x); f(x)crashes - 5. Types
- 6. Tuples and Arrays
- 7. Slices —and
str - 8. Structures —“Tuples with named and unordered components”
- 9. Enumerations ~ Tagged Payloads
Table of Contents
- 1. Hello World!
- 2. Formatted text:
format!, println!, dbg! - 3. The “::” Notation: Types as Namespaces and General Imports
- 4. A First look at Ownership: Why
let x = ⋯; f(x); f(x)crashes - 5. Types
- 6. Tuples and Arrays
- 7. Slices —and
str - 8. Structures —“Tuples with named and unordered components”
- 9. Enumerations ~ Tagged Payloads
1. Hello World!
fn main() { println!("Hello, world!"); }
Compile and run this with
rustc hello_world.rs; ./hello_world
The main function is special: It is always the first code that runs in
every program. For now, know that names ending in ! are “macros”
—special functions that transform code.
1.1. cargo new
Packages of code are referred to as crates —which are shipped with cargo, the build tool and package manager. Cargo is the most convenient way to create a new Rust project.
Running cargo new my-first-program creates a folder my-first-program with a
bunch of stuff in it. Importantly, it has a src/main.rs file which is our entry
point (which prints ‘Hello, world!’) and Cargo.toml which is a configurations
file —e.g., to keep track of what external packages our program requires. We
can now do cd my-first-program; cargo run to compile and run our program in one
go: Hello, world! is printed.
To install a new crate, simply add it to the end of the [dependencies] list in
Cargo.toml. For example, open Cargo.toml and at the very end, add rand =
"0.8.5". Next time you do cargo run it will install the rng randomness package.
1.2. Let's see a full program viz a Number Guessing Game
use std::io; // Use the standard input-output library use rand::Rng; // Use Random Number Generator trait, needs “rand” crate use std::cmp::Ordering; // Enumeration to denote result of comparisons fn main() { println!("Guess the number!"); // Ranges “m..n” are all integers i with m ≤ i < n. let secret_number = rand::thread_rng().gen_range(1..101); // The “loop” keyword creates an infinite loop. loop { println!("Please input your guess."); // “mut”able variable, bound to an empty String object let mut guess = String::new(); // Get a handle to the standard input for your terminal. // Then, stick user input into (a mutable reference to) “guess”. // If something goes wrong, show an informative message. io::stdin() .read_line(&mut guess) .expect("Failed to read line"); // Instead of “expect”, we can use “match” to move from // crashing on an error to instead handling the error... /* Shadow the “guess” variable to avoid using a new variable name. Handle the possibility of parse errors using Pattern Matching: We try to make an unsigned integer out of the user's string input; if no number is parsed, we ask the user to guess again. */ let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue // Restarts the loop }; // Alternatively: guess.trim().parse().unwrap() tries to get “num”; otherwise it crashes. // “unwrap” and “expect” are useful for quick development, but otherwise dangerous to use. match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break }, // Exit the loop } } }
Shadowing (overriding) let's us reuse names: Mutable variables can change their
value only, whereas shadowing means we can change the value and the type.
Shadowing is done with let, whereas mutable variables can be assigned to
directly.
- With shadowing,
let x = f(x), we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.
The parse function returns a value of type Option, which might contain
something or it might not contain anything. (Akin to type-safe nullables).
Exercise! Instead of the continue on parse errors, use std:process library
to quit the program with process::exit(1) after providing a useful message to
the user.
Exercise! Instead of the continue on parse errors, quit the program with
panic!("Not a number") —this violently stops execution with an error message,
source file name, and line number. (Likewise, unwrap panics where there is no
Option value to retrieve.)
Exercise! Notice that the first guess variable had its string type
inferred. If we remove the explicit numeric type annotation from the second
guess then parse wouldn't know what kind
of value is being parsed and we get a compile error. Remove the explicit type
annotation by specialising parse to get numeric u32 values using the
so-called “turbofixh syntax”: parse::<u32>() .
2. Formatted text: format!, println!, dbg!
We can write formatted text to a String object using
format! —or print it to the console instead using
println!; both work the same and stringify their arguments.
Let's print 4 weeks and 31 days a few different ways:
fn main() { // Placeholders `{}` will be replaced with arguments. println!("{} weeks and {} days", 4, 31); // Or using “format!” first. let it = format!("{} weeks and {} days", 4, 31); println!("{}", it); // Positional placeholders can be used. println!("{1} weeks and {0} days", 31, 4); // As can named arguments. println!("{all} weeks and {every} days", every = 31, all = 4); // We can mix and match. println!("{:?} weeks {1:?} {all} days", 4, "and", all = 31); // We can also “pretty print” expressions; useful for lots of data println!("{:#?}", ["4", "weeks", "and", "31", "days"]); // We can see an expression, its value, and its location in code. dbg!(it); // Prints: [src/main.rs:19] it = "4 weeks and 31 days" let n = dbg!(1 * 2 + 3) - 1; // Prints: [src/main.rs:20] 1 * 2 + 3 = 5 assert!(n == 4); // Sometimes it may be useful to see the memory address of a variable. println!("{:p}", &n); }
Notice that the {:?} placeholder prints text for debugging purposes; e.g.,
strings keep their quotes. Alternatively, the dbg!
macro takes ownership of an expression, prints the file and line number of where
that dbg! call occurs along with the resulting value of that expression, and
returns ownership of the value —this means that almost anywhere there is an
expression 𝒳, we can just replace it with dbg!(𝒳) to get some useful debugging
information about it!
The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!
- If you're doing lots of
println!just to see values of variables/expressions, just usedbg!instead:println!("x = {}, y = {}", x, y)can be replaced bydbg!(x, y).
3. The “::” Notation: Types as Namespaces and General Imports
Dots are generally used to get field from an object such as p.0 for pairs; the
double-colon is used similarly but for namespaces as in library::file::function.
fn main() { assert!(std::cmp::min(3, 8) == 3); }
usedirectives can be used to “bring into scope” names from other namespaces.// use std::cmp::min; // Single import // use std::cmp::{max, min}; // Multiple imports use std::cmp::*; // Imports everything fn main() { assert!(min(5, 5) == max(5, 5)); }
Types are namespaces too, and methods can be called as regular functions:
fn main() { assert!("hola".len() == str::len("hola")); }
In-general,
object.method(args) ≡ type::method(object, args).
4. A First look at Ownership: Why let x = ⋯; f(x); f(x) crashes
Many languages distinguish between ‘literals’ and ‘objects’, Rust does so with a feature called ownership, which aims to prevent us from writing memory unsafe code.
The following program looks like it should work without any problems…
1: fn main() { 2: let mut hi = "hello".to_string(); // We want an object; not a literal primitive. 3: 4: fn speak(words: String) { 5: println!("{}", words) 6: } 7: 8: speak(hi); 9: // speak(hi); // Whoops, the *value* of “me” has moved out of this *variable*! 10: 11: hi = "yup".to_string(); 12: speak(hi) 13: }
Ownership means variables own their values:
- Line 2: The variable
hiowns the string value"hello." - Line 8: The value
"hello"is passed into the functionspeak, and so ownership of the value has moved. In-particular,hino longer owns a value and so cannot be accessed after this line. - Line 9: We try to call the function
speakwith a variable that has no value and so we get a compile-time error. - Line 11: We can fix this by letting the variable
hiown a value, then we can use it again.
If we do need access to a variable's value in multiple function calls, we will have to pass it by reference instead of by value.
1: fn main() { 2: let mut hi = "hello".to_string(); 3: 4: fn speak(words: &String) { 5: println!("{}", words) 6: } 7: 8: speak(&hi); // Works first time. 9: speak(&hi); // Works second time! 10: }
Borrowing means temporarily owning something: Line 4 says this function
wants a reference to a string value; then line 8 calls the function by
making a reference using the “borrow operator” &.
- A reference is a type that references another value in memory.
They're useful when we don't want to pass “ownership” to control structures (such as for-loops) or functions.
A variable binding can be immutably borrowed multiple times:
- 🥳
let x = 1; let ref1 = &x; let ref2 = &x.
- 🥳
While borrowed, a variable binding cannot be mutated:
- 😦
let mut x = 1; let ref = &x; x = 2
- 😦
While immutably borrowed, a variable cannot be mutably borrowed:
- 😦
let mut x = 1; let ref1 = &x; let ref2 = &mut x.
- 😦
Summary: Rust provides two variants of most types, those that can be owned and those that cannot. Unowned types are passed-by-value, or copied. Loosely put, this is the difference between literals and objects.
fn main() { // Literals have no ownership, so the following works fine. let x: &str = "hello"; let y = x; dbg!(x, y); // ⇒ "hello", "hello" // Objects have a single owner, so the following crashes. let x = String::from("hello"); let y = x; // Now ‘y’ owns the string // dbg!(x, y); // Cannot use ‘x’ since it owns nothing! }
This is important: When we say let mut x = thing; let mut y = x, are updates to
y reflected in x or not? Yes, if thing is a literal; otherwise it doesn't matter
in Rust since x can no-longer be used —multiple mutable references cause
trouble!
Exercise! Confirm that references cannot be owned, by making a similar x, y
setup as above.
(For the curious, String is just a structure consisting of a vector of integers.)
Incidentally, another difference between literals and objects is their lifetimes. (Objects are heap-allocated.)
fn main() { // This works since literals are “static”; live forever. let x = { let y = &"hi"; y }; // let x = { let y = &"hi".to_string(); y }; // Error: The string object does not live long enough to be used anywhere else! // Note: Consider using a `let` binding to create a longer lived value. dbg!(x); }
Exercise! By making similar x, y setups as above, confirm that arrays [T; n] are
unowned and have static lifetimes, whereas vectors Vec<T> are owned and are
only have dedicated lifetimes.
5. Types
Rust is a statically & strongly typed language: It knows the types of all variables at compile time —it can infer them if you don't write them— and it does no implicit type coercions.
| Type | Explanation ; Example value |
|---|---|
Integers i32, u32 |
Number without decimals ; 123 |
Floats f32, f64 |
Numbers with decimals ; -1.23, 1e4 |
Booleans bool |
Truth values ; true, false, !p, p && q, p ∥ q |
Characters char |
Unicode characters ; 't', '😻' |
Tuples (τ₀, τ₁, …, τₙ₋₁) |
\(n\) things of different types ; (1, 'a', 2.3) |
Unit () |
A “zero-tuple”; denotes “done”; value is written () |
Arrays [τ; n] |
\(n\) things of the same type ; [1, 2, 3] |
Vectors Vec<τ> |
Like arrays, but can increase in size ; vec![1, 2, 3] |
Slices [τ] |
Slices are similar to arrays, but their length is not known at compile time. |
| Structures | A record, hashmap, lightweight class; key-value pairs |
| Enums | Constants; algebraic data-types |
| Ranges | \(m..n\) is the sequence of integers \(i\) with \(m ≤ i < n\). ; 3..7 |
Functions (τ₁, …, τₙ) → τ |
Transform τᵢ into a τ ; fn first<A, B>(a:A, b:B) -> A { a } |
[Read-only] References &τ |
Refer to an object, without owning it ; let s = ⋯; &s ;; let x = &4 |
Mutable references &mut τ |
Borrow an object and change it ; let mut s = ⋯; &mut s ;; let x = &mut 4 |
String Slices, &str |
A string literal that can only be borrowed, but not owned/mutated; let five = "5" |
[Growable] Strings, String |
Strings that can grow in size; essentially Vec<&str>;; let five = "5".to_string() |
- Slices are the answer to “What is the input type of a function that consumes arrays but doesn't know their length?” Slices act like temporary views into an array or a vector.
- Rust vectors are array-lists: They're arrays that double in size whenever more space is needed.
- Range Example:
(1..5).for_each(|i| println!("{}", i));
- The unit type,
(), is often used as the return type of functions that have side-effects, such as printing to the screen. Everything has to have a type, so()(which can be read “done”) can be that type. The default return type of functions is(), unless explicitly indicated otherwise. For 𝓃 in {8, 16, 32, 64}, a number of bits, there are signed integers
i𝓃which can store numbers from \(-2^{n}\) to \(2^{n - 1} - 1\) inclusive, and unsigned integersu𝓃which store numbers from \(0\) to \(2^{n}\).- These types provide bounds on numbers; e.g.,
let it: u8 = 256results in a compile error since 256 is out of range foru8. - By default, numeric variables without an explicit type are assigned type
i32, if possible. - Numbers have the expected arithmetic operations
+, -, /, %(remainder). - Underscores can be inserted in numeric literals to improve readability,
e.g.
1_000is the same as1000, and0.000_001is the same as0.000001.
- These types provide bounds on numbers; e.g.,
6. Tuples and Arrays
Both tuples and arrays collect multiple values and have fixed (non-growable) sizes; the only difference is that the values in a tuple can be of a different type whereas an array requires them to all have the same type.
fn main() { // Create: Tuples, different types allowed let p = ('a', 1, 2.3, "bye"); let q: (char, i32) = ('a', 12); // Explicit type annotation // Create: Arrays, can only be same type let a = [1, 2, 3]; let b : [char; 2] = ['x', 'y']; // Explicit type annotation // Read: Tuples, with “ .𝒾 ” notation // Read: Arrays, with “ [𝒾] ” notation println!("{0}, {1}, {2}", q.0, p.3, a[1]); // p.4, a[4] ⇒ Error! // Both can be *destructured*, but must match in length let (_, me, _, you) = p; // ≈ let me = p.1; let you = p.3; let [_, them, _] = a; // ≈ let them = a[1]; // Example: A super simple way to swap let x = 1; let y = 2; assert!(x == 1 && y == 2); let (y, x) = (x, y); // Shadowing assert!(x == 2 && y == 1); // Shorthand for constant arrays assert!([2, 2, 2, 2] == [2; 4]); }
[τ; n] is the type of arrays of length \(n\) with elements from type τ.
If τ is a value, then there is only one possible array: [τ, τ, ..., τ] (n-many
times).
We can loop over an array as follows.
fn main() { let names = ["me", "you", "them"]; for name in names { println!("{}", name) } }
Exercise! Use a vector, let names = vec!["me", "you", "them"], and try to (debug-)print names after the
loop. Notice that name lost ownership of its value to
the for⋯in loop. Fix this by giving the loop a
reference, &names.
7. Slices —and str
If we have an array/vector/string/collection, how do we reference a slice of their memory; i.e., narrow our view to a sub-part of the entire whole? The answer is “slice types”.
Slices are similar to arrays, but their length is not known at compile time. Essentially, slices are “views into segments of data” and so consists of two-parts: a pointer to the start of the data, and the length of the data. Below, with a bit of pointer arithmetic, we can use slices as these two-parts —but this is not recommended!
- A slice,
[τ], is a view into a block of memory. Whether mutable or not, a slice always borrows and that is why it is always behind a pointer, &. Thus, sometimes people say “slices are &[τ]”. - Since references are unowned, so too slices
&[τ]are unowned.
fn main() { let array: [i32; 5] = [11, 22, 33, 44, 55]; let slice: &[i32] = &array; // Decompose a slice into its two main parts let length = slice.len(); // ⇒ 3 let pointer = slice.as_ptr(); // ⇒ Some memory address // The 𝓃th element is 𝓃-steps away from the starting pointer. // (This is why elements start at index 0.) dbg!(unsafe { *pointer.offset(1) }); // ⇒ 22 // If you do a pointer offset out of bounds, like 4, you've just accessed more memory; // possibly by accident and not the memory you want! // Safe way to do indexing; which checks for bounds at run-time. let element = slice[2]; // ⇒ 33 // let element = slice[4]; // Index out of bounds error! // An even safer way, that does not crash at run-time. let element = slice.get(4); // ⇒ None // Get a sub-slice start at index 2 up to, and including, index 3. let sub: &[i32] = &slice[2..=3]; // ⇒ [33, 44] // You can (mutably) borrow a slice of a (mutable) vector, or string, etc. let mut v = vec![1, 2, 3, 4, 5]; let s = &mut v[2..]; // ⇒ [3, 4, 5] s[0] = 333; assert!(s == [333, 4, 5]); assert!(v == vec![1, 2, 333, 4, 5]); }
We borrow slices of x : τ using &x[..] provided the type τ implements the Index
and IndexMut traits, which let us using the indexing operator. The “..” syntax
is for range literals: .., 0.., ..20, ..=20, 3..6 are all example ranges with
m..n denoting all integers \(i\) with \(m ≤ i < n\) —and the right-bound can be
made inclusive using ‘=’. More concretely, (..=20).contains(&13) is true since
the range ..=20 is all integers at-most 20.
- References are implicitly converted into slices, when possible, by treating
&xas if it were&x[..].
[Mutable Slices] Notice that changing the value of a slice index also changes the value in the underlying array/vector/string —which actually owns the values that the slice is letting us look at.
Since slices can be formed from vectors and arrays, if we want a function to
work on both kinds of values, we make our function consume a slice: Values of
&Vec<T> and &[T; n] are implicitly transformed into values of &[T].
- Likewise, for
&strandString, the more generic approach is functions consuming&str.
Rust's strings are all Unicode and so characters differ in size, as such string
slices cannot be implemented with a pointer and accessed with a uniform offset,
like [T]. As such, there is a special type str, “string slices”, which fullfills
this role. str really acts like slice types; e.g., "hello" has type str and
&"hello"[1..] has type &str; yet assert!(&"hello"[1..] == "ello") is true.
8. Structures —“Tuples with named and unordered components”
Structures provide ways to group several values into a single value. Conceptually, this allows us to put a bunch of related things in a bag and run around with the bag, instead of wrapping our arms around all of the individual things and trying to hold on to them separately. ---Eloquent JavaScript
A structure is also known as a record, hashmap, association list,
lightweight-class, JSON object. Struct types are formed as name-type pairs, and
their values are constructed with ‘struct literals’: Name-value pairs preceded
by the struct name.
Tuples quickly group related variables together, but structs give us meaning by naming the variables —instead of using numeric indices.
// ⟨0⟩ Forming a new type #[derive(Debug)] // Simple debug printing struct Person { name : String, job : String, age : u32 } // ⟨1⟩ Introducing a value of that type let mut job = String::from("farmer"); let mut jay = Person { name: String::from("jason"), age: 72, job // Shorthand for same name, but *cloned* value: job: String::form("farmer") }; // Mutability of “job” and “jay” are not related at all. job = String::from("labourer"); // Has no effect on “jay” assert!(jay.job == "farmer"); // ⟨2⟩ Eliminating a value of that type by destructuring // [Ownership moves: If “jay.field” is an object, it has moved to “field”] // [Namely, after this line, /read access/ of “jay.name” is invalid.] let Person {name, age, ..} = jay; // The “..” are to ignore the rest // ⟨3⟩ Updating a field [Give “jay.name” a value and so it is valid to read.] jay.name = String::from("jasim"); assert!(jay.name == "jasim" && name == "jason"); // Using the automatic debug printing println!("Jay is {:?}", jay); // Pretty print with: {:#?} // ⟨4⟩ Let's attach a method to our struct instances impl Person { fn speak(&self) { println!("{}: I am a {} years old.", self.name, self.age); } } // Let's use this method. jay.speak(); // ⟨1⟩' Creating instances from instances; “prototyping”! // [Read: kathy is like jay but has a different name.] // [This “struct update syntax” can only happen in the last position.] // [In-particular, “Person {..jay}” is a clone of the value “jay”.] let kathy = Person {name: String::from("Kalthum"), ..jay}; // ⟨4⟩′ We can call methods using type-namespace syntax. Person::speak(&kathy); // ⟨1⟩″ We can also make unnamed fields, and use tuple indexing // These “tuple structs” are basically named tuples. struct Mine(String, u32); let mut it = Mine(String::from("hola"), 23); it.1 = 12;
// ⟨4⟩
The function fn greet(p: &Person) { println!("{}: I am a {}.", p.name, p.job) }
is very specific: It only works on Person values. As such, it's helpful to tie
this behaviour more closely to the Person struct, which can be done by moving it
into an “impl”ementation block. All methods in an impl block must have the first
argument named self. As such, we could have written fn speak(self: &Person) {⋯}.
However, within an impl block, the type Self is an alias for the type that the
impl block is for. As such, we could have written fn speak(self: &Self) {⋯}.
Since the first argument of methods must be named self and typed Self, we can
just abbreviate it to self: Self to self —however, in our case we use the
abbreviation &self because we want the value to be borrowed, not owned.
Exercise! Change speak above to consume a mutable reference, &mut self, that
increments the age. Two other non-mutable things will now break in the above
program; make them mutable. Make kathy speak twice and assert! her age has
changed.
Exercise! Struct methods can have the same name as struct fields —whereas
arbitrary functions cannot share the name of variables. Change the method name speak to be age.
8.1. Structs can be generic too!
struct Pair<A, B> { first: A, second: B, } fn main() { let p = Pair { first: 3, second: true, }; // : Pair<i32, bool> }
9. Enumerations ~ Tagged Payloads
The enum keyword allows the creation of a type which may be one of a few
different variants.
Let's make an generic enum to indicate where something can be found, amongst two kinds of types.
// An enum is defined by a collection of structs. #[derive(Debug)] enum Location<A, B> { // An `enum` variant may either be unit-like, NoWhere, // or tuple structs, Here(A), There(B), // or named structures. Both { first: A, second: B }, // Besides being generic, enums can also be recursive (via indirection) // Swap(Box<Location<A, B>>), } // To work on enums, we use the match clause fn toInt<A, B>(l: &Location<A, B>) -> i32 { match l { Location::NoWhere => 0, Location::Here(a) => -1, Location::There(b) => 1, Location::Both { first, second } => 2, } } fn main() { let both = Location::Both { first: 3, second: false, }; let nope = Location::NoWhere::<i32, i32>; // Use names without qualifiers use crate::Location::*; let no: Location<i32, i32> = NoWhere; dbg!(&no, toInt(&no)); // ⇒ &no = NoWhere; toInt(&no) = 0 }
Exercise! Insert a use clause within toInt so that you can use the constructor
names without qualifying them.