🦀
Â
Â
Â
# Install Rust using rustup (on Unix)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 1) Proceed with installation (default) <---
# 2) Customize installation
# 3) Cancel installation
// `println!` prints to the console
// The ! indicates it is a macro (metaprogramming - code that writes code)
println!("Hello, world!");
// println! supports format strings
let name = "Bob";
println!("Hello, {name}"); // These
println!("Hello, {}", name); // are
println!("Hello, {n}", n = name); // equivalent
let arr = [1, 2, 3];
println!("{arr:?}"); // The :? is sometimes needed when printing complex types
// Format strings can also be interpreted by the `format!` macro
let name = "Bob";
let greeting = format!("Hello, {name}!");
let num: i32 = 5; // With explicit type annotation
let num = 5; // Rust can usually infer the type
let nums = (0..10).collect(); // But not always
// error[E0282]: type annotations needed
let nums: Vec<i32> = (0..10).collect(); // With explicit type annotation
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
// Ints defaults to i32
let num = 42; // This is an i32
// If you want a different type:
let num_1: u8 = 42;
let num_2 = 42 as u8;
let num_3 = 42u8;
let num_4 = 42_u8;
assert!(num_1 == num_2 && num_2 == num_3 && num_3 == num_4);
// Be careful when narrowing!
let negative = -5_i8;
let positive = negative as u8;
println!("{positive}"); // Prints 251
// Floats only have two types: f32 and f64
// f64 is the default
let num = 42.0;
// A String is a growable, mutable, owned, UTF-8 encoded type.
let mut s1 = String::new();
s1.push_str("hello");
let s2 = String::from("hello");
let s3 = "hello".to_string();
assert_eq!(s1, s2);
assert_eq!(s2, s3);
let s = "hello".to_string();
let string_slice: &str = &s[0..4];
println!("{string_slice}");
// Prints "hell"
// Single and double quotes mean different things!
let this_is_a_string_slice = "x";
let this_is_a_char = 'x';
for char in this_is_a_string_slice.chars() {
println!("{char}",);
}
let data = Vec::new();
data.push(1);
// error[E0596]: cannot borrow `data` as mutable,
// as it is not declared as mutable
let mut data = Vec::new();
data.push(1);
// Why const, when we can create immutable variables with let?
// Constants are known at compile time, meaning the compiler can optimize.
const LOG_LEVEL: &str = "INFO";
const NUMBERS: [i32; 3] = [1, 2, 3];
let (first, second, third) = (1, 2, 3);
const MORE_NUMBERS: [i32; 3] = [first, second, third];
// error[E0435]: attempt to use a non-constant value in a constant
struct PowerStation {
name: String,
capacity: f64,
}
let kvildal = PowerStation {
name: "Kvildal".to_string(),
capacity: 1240.0,
};
let tonstad = PowerStation {
name: "Tonstad".to_string(),
capacity: 960.0,
};
struct PowerStation {
name: String,
capacity: f64,
}
// A tuple struct is a struct that has unnamed fields.
struct Point(f64, f64);
let origin = Point(0.0, 0.0);
let point = Point(3.0, 4.0);
// A unit struct is a struct that has no fields.
struct NothingToSeeHere;
let nope = NothingToSeeHere;
enum Color {
Red,
Green,
Blue,
}
let favorite_color = Color::Blue;
// Enums can hold data
enum Pet {
Goldfish,
Cat(String),
Dog { name: String, age: u8 }
}
let first_pet = Pet::Goldfish;
let second_pet = Pet::Cat("Misty".to_string());
let third_pet = Pet::Dog { name: "Rusty".to_string(), age: 8 };
I call it my billion-dollar mistake. It was the invention of the null reference in 1965.
[...]
This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
- Tony Hoare
// Instead of null or None, Rust has Option:
enum Option<T> {
Some(T),
None,
}
/// Try to find an even number in the given sequence of numbers.
fn find_even(input: Vec<i32>) -> Option<i32> {
for num in input {
if num % 2 == 0 {
return Some(num);
}
}
None
// This None is not a standalone object like None in Python,
// it is the None variant of the Option enum
}
let maybe_number = find_even(vec![1, 3, 5]);
maybe_number + 1; // error[E0369]: cannot add `{integer}` to `Option<i32>`
// Just like there's no null or None,
// there's also no exceptions in Rust.
// Instead, we have Result:
enum Result<T, E> {
Ok(T),
Err(E),
}
/// Try to divide two numbers. Return Err if the divisor is zero.
fn divide(dividend: f64, divisor: f64) -> Result<f64, String> {
if divisor == 0.0 {
Err("Cannot divide by zero!".to_string())
} else {
Ok(dividend / divisor)
}
}
let result = divide(2.0, 3.0);
Result
s// Get the value out of the Ok variant,
// or panic if the result is Err
let result = divide(2.0, 3.0).unwrap();
// Get the value out of the Ok variant,
// or panic with a custom message if the result is Err
let result = divide(2.0, 3.0).expect("Failed to divide");
// Get the value out of the Ok variant,
// or a default value if the result is Err
let result = divide(2.0, 3.0).unwrap_or(f64::INFINITY);
// Get the value out of the Ok variant,
// or return the Err to the caller
let result = divide(2.0, 3.0)?;
// The ? operator above is equivalent to:
let result = match divide(2.0, 3.0) {
Ok(value) => value,
Err(e) => return Err(e),
};
// PS: Try to avoid the panicking methods in production!
let tuple = (1, 2, 3);
let arr = [1, 2, 3];
let vec = vec![1, 2, 3];
println!("tuple: {tuple:?}");
println!("arr: {arr:?}");
println!("vec: {vec:?}");
// Prints:
// tuple: (1, 2, 3)
// arr: [1, 2, 3]
// vec: [1, 2, 3]
// Tuples can store mixed types, arrays and vectors cannot
let tuple = (1, "hello", 4.2);
let arr = [1, "hello", 4.2]; // error[E0308]: mismatched types
let vec = vec![1, "hello", 4.2]; // error[E0308]: mismatched types
// Indexing
let tuple = (1, 2, 3);
let arr = [1, 2, 3];
let vec = vec![1, 2, 3];
println!("First tuple element: {}", tuple.0);
println!("First array element: {}", arr[0]);
println!("First vector element: {}", vec[0]);
// Array length must be known at compile time,
// and the size can't change.
let mut arr = [1, 2, 3];
arr = [5, 6, 7]; // This is fine
arr = [1, 2]; // This is not
// error[E0308]: mismatched types
// expected an array with a fixed size of 3 elements,
// found one with 2 elements
// The structural type of a tuple can't change
let mut tup = (1, "cat");
tup = (2, "cats"); // This is fine
tup = ("many", "cats") // This is not
// error[E0308]: mismatched types
// 59 | tup = ("many", "cats") // This is not
// | ^^^^^^ expected integer, found `&str`
// Vector length DOES NOT have to be known at compile time,
// and vectors can freely grow or shrink
use rand::prelude::*;
let random_numbers = thread_rng().gen_range(1..10);
let unknown_number_of_elements = vec![0; random_numbers];
// HashSets contain unique elements
use std::collections::HashSet;
let mut set = HashSet::from([1, 2, 3]);
set.extend([2, 3, 4]);
println!("{set:?}");
// Prints: {1, 2, 3, 4}
let set_one = HashSet::from([1, 2, 3]);
let set_two = HashSet::from([3, 4, 5]);
let in_both = set_one.intersection(&set_two);
println!("{in_both:?}");
// Prints: [3]
let in_one_of_them = set_one.union(&set_two);
println!("{in_one_of_them:?}");
// Prints: [3, 2, 1, 5, 4]
let week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
for day in week {
println!("{}", day);
}
for number in 1..5 {
println!("{number}");
}
// Prints 1, 2, 3, 4
for number in 1..=5 {
println!("{number}");
}
// Prints 1, 2, 3, 4, 5
let mut user_input = get_user_input();
while user_input != "quit" {
user_input = get_user_input();
}
loop
loop// No need for `while true`
loop {
let user_input = get_user_input();
if user_input == "quit" {
break;
}
}
// Closures are anonymous functions that can be stored in a variable.
let add_one = |x| x + 1;
println!("{:?}", add_one(1)); // Prints "2"
// Closures can capture variables from the scope
// in which they are defined.
let x = 2;
// y is a parameter, x is a captured variable
let add_x = |y| x + y;
println!("{:?}", add_x(1)); // Prints "3"
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
let mut it = numbers.iter();
println!("The first element is {:?}", it.next());
println!("The second element is {:?}", it.next());
println!("The third element is {:?}", it.next());
// Prints:
// The first element is Some(1)
// The second element is Some(2)
// The third element is Some(3)
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
let negated_odd_numbers: Vec<i32> = numbers
.iter()
.filter(|&x| x % 2 == 1)
.map(|&x| -x)
.collect();
println!("{negated_odd_numbers:?}");
// Prints [-1, -3, -5]
// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.
let s: String = "hello".to_string();
let new_s: String = s;
println!("The string is {s}");
// error[E0382]: borrow of moved value: `s`
// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.
// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.
fn takes_ownership(new_string_owner: String) {
// The string now belongs to the variable new_string_owner.
// The string isn't returned, therefore it is simply dropped when
// new_string_owner goes out of scope at the end of this function.
}
let s = "hello".to_string();
takes_ownership(s);
println!("The string is {s}");
// error[E0382]: borrow of moved value: `s`
// Borrowing happens through references.
// References are immutable by default.
fn calculate_length(s: &String) -> usize {
s.len()
}
let mut greeting = "hello".to_string();
let len = calculate_length(&greeting);
println!("The length of {greeting} is {len}");
// Borrowing happens through references.
// References are immutable by default.
fn calculate_length(s: &String) -> usize {
s.push_str(", world");
// error[E0596]: cannot borrow `*s` as mutable,
// as it is behind a `&` reference
s.len()
}
let mut greeting = "hello".to_string();
let len = calculate_length(&greeting);
println!("The length of {greeting} is {len}");
// Mutable references
let mut greeting = "hello".to_string();
change(&mut greeting);
println!("The string is now {greeting}");
// The string is now hello, world
fn change(s: &mut String) {
s.push_str(", world");
}
// Borrowing rules:
// 1. At any given time, you can have either one mutable reference
// or any number of immutable references.
// 2. References must always be valid.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
// error[E0499]: cannot borrow `s` as mutable more than once at a time
println!("{r1}, {r2}");
// Ownership + borrowing rules rules prevent data races
// and allow "fearless concurrency"!
Initially, the Rust team thought that ensuring memory safety and preventing concurrency problems were two separate challenges to be solved with different methods. Over time, the team discovered that the ownership and type systems are a powerful set of tools to help manage memory safety and concurrency problems!
- The book
// Literal patterns
let number = 3;
match number {
1 => println!("All is one"),
2 => println!("Two's company"),
3 => println!("Three's a crowd"),
_ => println!("Undefined territory"),
}
// Patterns can be used with `let`, `if let`, `for` loops,
// function parameters, etc.
struct Color { r: u8, g: u8, b: u8 }
let colors = [
Color {r: 65, g: 250, b: 9},
Color {r: 8, g: 164, b: 18},
Color {r: 241, g: 9, b: 98},
];
for Color { r, g, b } in colors {
println!("{r}-{g}-{b}");
}
// Refutability: Patterns come in two forms, refutable and irrefutable.
// The pattern (x, y) is irrefutable, i.e. it will allways match:
let point = (4, 3);
let (x, y) = point;
// The pattern Some(x) is refutable, i.e. it might not match:
let maybe_a_number = Some(3);
if let Some(n) = maybe_a_number {
println!("It's something");
} else {
println!("There's nothing there");
};
enum Color {
Red,
Green,
Blue,
}
let color = Color::Red;
match color {
Color::Red => println!("Red"),
Color::Green => println!("Green"),
}
// error[E0004]: non-exhaustive patterns: `Color::Blue` not covered
enum Color {
Red,
Green,
Blue,
}
let color = Color::Red;
match color {
Color::Red => println!("Red"),
Color::Green => println!("Green"),
_ => println!("Some other color"),
}
let num = 5;
match num {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
}
// error[E0004]: non-exhaustive patterns:
// `i32::MIN..=0_i32` and `4_i32..=i32::MAX` not covered
fn function_one() -> String {
do_other_stuff();
return "This string will be returned".to_string();
}
fn function_two() -> String {
do_other_stuff();
"This will be returned, even though there is no return keyword".to_string()
// Tail expression
}
fn function_three() {
do_other_stuff();
"This will NOT be returned due to the trailing semicolon".to_string();
}
// In Rust, almost everything is an expression,
// meaning it evaluates to a value.
// For example, `if` is an expression, not a statement.
let answer = if 1 + 1 == 2 {
do_stuff();
42
} else {
do_other_stuff();
24
};
println!("The answer is {answer}") // Prints "The answer is 42"
let answer = if 1 + 1 == 2 {
match 1 + 1 {
2 => {
{
{
{
42
}
}
}
}
_ => 24
}
}
else {
24
};
println!("The answer is {answer}") // Prints "The answer is 42"
let x: u8 = 2;
let y: u8 = 3;
let rival_team: &HashSet<(u8, u8)> = &HashSet::new();
// Everything is an expression, including block.
// How can this be used? Might help to communicate intent. Compare these two, wo are doing the exact same thing:
// As we are reading the first line below, the intent isn't necessarily immediately clear:
let mut moves = HashSet::from([(x + 1, y + 1)]);
if let Some(new_x) = x.checked_sub(1) {
moves.insert((new_x, y + 1));
}
let capture_moves = moves
.intersection(rival_team)
.cloned()
.collect::<HashSet<_>>();
// But here, we immediately understand that all the code in the block is involved in computing capture_moves:
let capture_moves = {
let mut moves = HashSet::from([(x + 1, y + 1)]);
if let Some(new_x) = x.checked_sub(1) {
moves.insert((new_x, y + 1));
}
moves
.intersection(rival_team)
.cloned()
.collect::<HashSet<_>>()
};
// An additional advantage of the latter is that the temporary variable `moves` is contained in the scope
// (it's cleared after the block has finished)
struct Dog {
name: String,
}
impl Dog {
fn bark(&self) {
println!("{} barks", self.name);
}
}
let fido = Dog { name: "Fido".to_string() };
fido.bark();
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
struct Fox;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}
impl Animal for Fox {
fn speak(&self) {
println!("Ring-ding-ding-ding-dingeringeding!");
}
}
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
struct Fox;
fn make_animal_speak(animal: &impl Animal) {
// This function accepts any type that implements the Animal trait.
animal.speak();
}
let dog = Dog;
make_animal_speak(&dog);
struct Dog;
impl Dog {
fn speak() {
// Note that `speak` doesn't have a &self parameter,
// meaning it becomes an "associated function" (think static method)
println!("Woof!");
}
}
// Dog.speak(); // error[E0599]: no method named `speak` found for struct
Dog::speak(); // :: is used when accessing associated functions
// Separate files are also modules, and can be
// brought in with the `mod` keyword, but
// only from main.rs or lib.rs
mod separate_file_module; // This only works from main.rs or lib.rs
separate_file_module::do_something();
// From files other than main.rs or lib.rs,
// use `use` instead of `mod`
use crate::separate_file_module;
separate_file_module::do_something();
// Modules can also be defined inline.
// Tests are a good use case for this.
mod inline_module {
pub fn do_something() { // Note the `pub`
println!("Doing something");
}
}
inline_module::do_something();
These slides are available at
2. Install Rust, create and run "hello, world"
1. Watch video: How To Speak Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cd ~/.projects
cargo new rust-hello-world --bin
cd rust-hello-world/
cargo run