An overview of essential features and concepts in Rust. It delves into unique aspects of Rust like lifetime management and error handling, showcases how generics and traits enhance code reusability and modularity, and sheds light on code organization in Rust projects. The guide also covers functional programming components like closures and iterators. Moreover, it outlines the robust testing framework of Rust, and guides on publishing Rust packages and installing binary crates using Cargo, Rust’s built-in package manager. This invaluable resource serves as both a handy reference and a refresher for anyone, whether they’re a beginner or an experienced Rustacean.
Good to know
- Cargo is Rust’s build system and package manager.
- https://crates.io/ is where you can find the dependencies.
Naming Convention
1
2
| let variable_name = ...
const CONST_NAME = ...
|
- Constants can be defined in global scope, but variables cannot
Beautiful
- Compiler errors are very clear and actually helpful
- You can use
rustc --explain Exxxx
, to explain the error by error code1
2
3
4
| 27 | let number = if true { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
|
Surprising
Variable shadowing
1
2
3
| let x: String = ...
let x: Int = ... // this is valid. We can change a mutable variable type when we reassign it
x = "..." // Will not compile without `let`
|
String literals need to be converted to Strings.
Annoying
- snake_case_variable_names. I need to jump to a character
_
in the number line for every space in a variable name. - Integer Overflow without exceptions, there are few methods (literally) to handle cases of overflow explicitly
Interesting
- A rust developer is a
Rustacean
- The following two signatures are not equivalent, but the first will not accept string literals
1
2
| fn first_word(s: &String) -> &str { ... // does not accept string literals
fn first_word(s: &str) -> &str { ... // accept either a string or a string literal
|
- Documentation code examples run as tests
Conventions
Constructor
new
: Constructor shouldn’t fail.build
: Constructor might fail.
Getting Started
- Official Installation Instructions
Your first project
1
2
3
4
5
6
7
8
| cargo new project_name --vcs=git
cargo new library_name --lib
cargo check # Compiles but doesn't build an artifact
cargo build # Builds a debug artifact at: /target/debug
cargo run # Builds a debug artifact and runs it
cargo build --release # Builds a release artifact at: /target/release
cargo add dependency@version
cargo doc --open # Generate and open HTML documentation.
|
- By convention
src/main.rs
is the crate root for the binary crate.src/lib.rs
is the crate root for the library crate.
Simple IO
Println
1
2
| println!("some string")
println!("Hello, {} {}", 5, "times."); // prints `Hello, 5 times.`
|
Command Line Argument
1
2
| use std::env;
let args: Vec<String> = env::args().collect();
|
The first argument is the program name, then the following are the arguments passed to the program
Read File Content
1
2
| use std::fs;
let contents: String = fs::read_to_string(file_path)
|
Environment Variables
1
2
| use std::env;
env::var("VARIABLE_NAME")
|
Variables
- Variables are immutable by default, to make a variable mutable we need to use
mut
keyword1
2
| let immutable_variable: u32 = 15;
let mut mutable_variable: u32 = 25;
|
Functions
1
2
3
| fn plus_one(x: i32) -> i32 {
x + 1
}
|
1
2
3
4
5
6
7
8
9
| // Single-line
/*
* Multi-line
*/
/// Documentation comment
//! Documentation comment. This comments on the containing block
|
- Documentation comments support markdown
Data Types
bool
char
Integer
Length | Signed | Unsigned |
---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch (architecture word size) | isize | usize |
Float
f32
and f64
, where f64
is the default
Tuple
1
2
3
4
5
6
| let tuple: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tuple;
let five_hundred = tuple.0;
let six_point_four = tuple.1;
let one = tuple.2;
|
()
Unit is an empty tuple
Struct
These two are equivalent.
1
2
3
4
5
6
7
| fn create_point_with_zero_z(x: i32, y: i32) -> Point {
Point { x : x, y: y, z: 0 }
}
fn create_point_with_zero_z(x: i32, y: i32) -> Point {
Point { x, y, z: 0 } // note that we do not explicitly need to `x: x`
}
|
You can also create a new struct from an existing struct modifying one or more of its member variables
1
2
3
4
| let point_with_zero_z = Point {
z: 0,
..arbitrary_point
};
|
Methods and Associated Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| struct BankAccount {
account_number: u32,
balance: Money,
account_type: String,
}
impl BankAccount {
fn deposit(&mut self, amount: Money) {
self.balance.add(amount);
}
fn withdraw(&mut self, amount: Money) {
self.balance.subtract(amount);
}
fn new(account_number: u32, initial_balance: Money, account_type: String) -> BankAccount {
BankAccount {
account_number,
balance: initial_balance,
account_type,
}
}
}
|
enum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| enum Point {
Origin,
Point(i32, i32, i32),
}
impl Point {
fn is_origin(&self) -> bool {
match self {
Point::Origin => true,
_ => false,
}
}
fn origin() -> Point {
Point::Origin
}
}
let origin = Point::origin();
let point = Point::Point(1, 2, 3); // default (auto-generated) constructor
match point {
Point::Point(x, y, z) => println!("Point is at {}, {}, {}", x, y, z), // binding (deconstructing)
Point::Origin => println!("Point is at origin"),
}
|
we can also
1
2
3
4
5
6
7
8
9
| enum Message {
Move{ x: u32, y: u32 },
}
let x: Message = Message::Move { x: 10, y: 30 };
match x {
Message::Move { x, y } => {}
}
|
Collections
String
String interpolation
1
2
3
4
5
6
7
8
| let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let x = format!("{s1}-{s2}-{s3}");
let y = format!("{}-{}-{}", s1, s2, s3);
let iterator_over_lines = some_string.lines()
|
- Strings cannot be indexed because it is a utf-8 string, not ASCII.
- If you really must, you can slice a string in 4 bytes chunks
let s = &hello[0..4];
Array
1
2
3
4
| let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = [1; 3]; // results in [1, 1, 1] repeating 1 three times
let first = a[0];
let second = a[1];
|
Range
1
| let range: Range<i32> = (1..4);
|
Vector
1
2
3
4
5
6
7
| let mut v1: Vec<i32> = Vec::new();
let mut v2: Vec<i32> = vec![1, 2, 3];
v1.push(5);
v2.push(6);
let third: &i32 = &v1[2];
let maybe_third: Option<&i32> = v1.get(2);
|
HashMap
1
2
3
4
5
6
| let mut map = HashMap::new();
map.insert(1, "Hello");
map.insert(2, "World");
let value: Option<&&str> = map.get(&1);
map.entry(1).or_insert("!"); // will only insert if the key does not exist
|
Smart Pointers
Here is the memory leak zone For more information on how memory leaks can happen and why Reference Cycles Can Leak Memory
- Data structures that act like a pointer but also have additional metadata and capabilities.
String
and Vec<T>
are smart pointers.- Implement the
Deref
and Drop
traits.Deref
trait allows an instance of the smart pointer struct to behave like a reference so you can write your code to work with either references or smart pointers.Drop
trait allows you to customize the code run when an instance of the smart pointer goes out of scope.std::mem::drop
Force drops
- You can write your own smart pointers
1
2
3
| let x: i32 = 5;
let reference: Box<i32> = Box::new(x);
let dereference: i32 = *reference;
|
Box<T>
- Stores data in the heap instead of the stack
- Used in situations when
- When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
- When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
- When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type ```rust pub enum List { Cons(i32, Box
), Nil, }
impl List { pub fn nil() -> Box { Box::new(List::Nil) }
1
2
3
| pub fn cons(value: i32, list: Box<List>) -> Box<List> {
Box::new(List::Cons(value, list))
} } ```
|
Rc<T> Reference Counter
- Used in single-threaded scenarios only
- If multiple parts of our programs own the data and we know which part finishes last we can make that part the owner. But if we don’t, we use the reference counter.
- Use
Rc::Clone(&ref)
to increase the reference count without creating a copy.
RefCell<T> Reference Cell
- Used in single-threaded scenarios only
- Enforce borrowing rules at runtime instead of compile time. Rust compiler is conservative and may not compile for programs the developer knows are correct. This gives the developer the ability to move the checks to the runtime. If the check fails, the program will panic.
- interior mutability pattern is used to mutate the data under immutable reference. A good use case is a mock object.
- We use
borrow
and borrow_mut
methods instead of &
and &mut
to keep track of references in the run time.
Combining Rc<T> and RefCell<T>
- Get a value that has multiple owners and can be mutated.
Ownership
Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Shallow copying is not allowed. Instead, data is moved
1
2
3
4
| let s1 = String::from("hello");
let s2 = s1; // This invalidates `s1`, ends the scope of s1
println!("{}, world!", s1); // This code does not compile
|
You can, though:
1
2
3
4
| let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("s1 = {}, s2 = {}", s1, s2);
|
Primitives are copied by default. The following will compile without needing to call clone()
1
2
3
4
| let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
|
Even more interesting, calling a function takes over the variable ownership
1
2
3
| let s1 = String::from("hello, world!");
take_over(s1);
println!("{}", s1); // This will not compile, `s1` is no longer valid
|
When a function returns a value, the ownership is transferred.
Borrowing/Referencing/&
1
2
3
| let x: i32 = 5;
let reference: &i32 = &x;
let dereference: i32 = *reference;
|
1
2
3
4
5
6
7
8
9
| fn main() {
let s1 = String::from("hello, world!");
borrow_string(&s1);
println!("{}", s1);
}
fn borrow_string(s: &String) { // s here is immutable, and it cannot be changed
println!("{}", s);
}
|
To make a reference mutable, you have to mark it with mut
explicitly. You can only have one mutable reference for a variable.
1
2
3
4
5
6
7
8
9
10
| fn main() {
let mut s1 = String::from("hello, world!");
borrow_string(&mut s1);
println!("{}", s1);
}
fn borrow_string(s: &mut String) {
s.push_str(" I am borrowed");
println!("{}", s);
}
|
This will not compile
1
2
3
4
5
6
| let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // cannot borrow `s` as mutable more than once at a time
println!("{}, {}", r1, r2);
|
You cannot also create a mutable reference for something that is already being borrowed
1
2
3
4
5
6
7
| let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
|
Note that the cope of a reference ends the last time that reference is used. The following code works absolutely fine
1
2
3
4
5
6
| let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{}, and {}", r1, r2); // The scope of r1 and r2 ends here
let r3 = &mut s; // Not a problem at all
println!("{}", r3);
|
Slices: Reference to a subset of a collection
1
2
3
4
5
6
7
8
| let s = String::from("hello world");
let hello = &s[0..5]; // the end of range = index of the last element + 1
let world = &s[6..11];
// alternatively
let hello_2 = &s[..5]; // from the beginning
let world_2 = &s[6..]; // until the end
let s_2 = &s[..]; // everything
|
Lifetime
1
2
3
| &i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
|
1
| fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } // ?Maybe?? x and y have the same lifetime as 'a
|
The generic lifetime 'a
will get the concrete lifetime that is equal to the smaller of the lifetimes of x
and y
.
1
2
3
| struct SomeStruct<'a> {
field: &'a str, // Ths struct cannot outlive field
}
|
Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.
1
| let s: &'static str = "I have a static lifetime."; // reference that lives for the entire life of the program
|
1
| fn something<'a, T> // lifetime nad generics in the same time
|
Control Flow
if statement
Pattern Matching
if let
1
2
3
4
5
6
7
8
9
10
| let result: Result<u8, String> = Ok(3u8);
match result {
Ok(max) => println!("The maximum is configured to be {}", max),
Err(_) => (),
}
if let Ok(max) = result {
println!("The maximum is configured to be {}", max);
}
|
Repetition
loop
1
2
3
4
5
6
|
loop { // similar to `while true`
continue;
break some_value; // you can break with a value which would be the result of the loop expression.
};
|
while
1
2
3
4
| while some_condition {
continue;
break; // you cannot break with a value similar to while.
}
|
for
1
| for item in some_collection{}
|
1
2
3
4
| let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
|
Strings are special to iterate over a String. You can
1
2
| for c in some_string.chars(){}
for b in some_string.bytes(){}
|
For Hashmaps
1
2
3
| for (key, value) in &map {
println!("{key}: {value}");
}
|
tail recursion
Functional Programming
Closures
1
2
3
4
5
6
| fn add_one_v1 (x: u32) -> u32 { x + 1 }
// Rewriting function as a closure
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
let add_one_v5 = move |x| x + 1 ; // force move ownership.
|
- If we use a closure without a type annotation, the type of variables will be inferred from the first usage.
- Closures can capture variables from their environment.
- A closure that mutates a variable it captures must be declared as
mut
. - Closures implement one of these traits
- Iterators are lazily evaluated.
vector.iter()
1
| vector.iter().map(closure).collect()
|
Error Handling
Unrecoverable Errors: Panic
1
| panic!("crash and burn");
|
Recoverable Errors
1
2
3
4
| enum Result<T, E> {
Ok(T),
Err(E),
}
|
1
2
3
4
5
| File::open("hello.txt").unwrap(); //will panic if an error occurs.
File::open("hello.txt")
.expect("hello.txt should be included in this project"); //Panic with a custom error message
File::open("hello.txt")? // propagates the result to the calling function
File::open("hello.txt")?.read_to_string(&mut username)?; // `?` can be chained
|
?
operator calls the from
function, and the error type received is converted into the error type defined in the return type of the current function. This is defined in the From
trait.- We are only allowed to use the
?
operator in a function that returns Result
, Option
, or another type that implements FromResidual
. main
function can return errors too fn main() -> Result<(), Box<dyn Error>>
- User
expect
instead of unwrap
to document why you know this code will not panic1
2
3
| let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
|
Standard Errors
std::io::Error
- Error constructor signature:
pub fn new<E>(kind: ErrorKind, error: E) -> Error
, Note ErrorKind
Generics
1
2
3
4
5
6
7
8
9
10
11
| struct SomeStruct<T>{
field: T,
}
impl<T> SomeStruct<T> {
fn new(field: T) -> Self {
Self { field }
}
}
fn do_something<T, R>(value: T) -> R {...}
|
Traits
1
2
3
4
5
6
7
8
9
| pub trait SomeTrait {
fn do_something(&self) -> String {
// optional default implementation
}
}
impl<T> SomeTrait for SomeStruct<T> {
fn do_something(&self) -> T {}
}
|
1
2
3
| fn multiple_traits_expected(value: (impl SomeTrait + SomeOtherTrait)) {} // Opposite to structs we need `impl` keyword here
fn multiple_traits_expected_generics<T: SomeTrait + SomeOtherTrait>(value: T) {}
fn multiple_traits_expected_generics_with_where<T>(value: T) where T: SomeTrait + SomeOtherTrait {}
|
We can also conditionally implement a trait for any type that implements another trait.
1
2
| trait SomeNewDisplayBehaviour
impl<T: Display> SomeNewDisplayBehaviour for T {}
|
Safe Duck Typing (trait objects)
- Prefer generics over trait objects because
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| fn main() {
let mallard = MallardDuck {};
let rubber = RubberDuck {};
let ducks: Vec<&dyn Duck> = vec![&mallard, &rubber];
for duck in ducks {
listen_to_duck(duck);
}
}
fn listen_to_duck(duck: &dyn Duck) {
duck.quack();
}
struct MallardDuck {}
impl Duck for MallardDuck {
fn quack(&self) {
println!("Mallard Quack!");
}
}
struct RubberDuck {}
impl Duck for RubberDuck {
fn quack(&self) {
println!("Rubber Squeak!");
}
}
|
Concurrency
Rust has few concurrency features. The concurrency features listed below are part of the standard library. One may use these or other concurrency implementations.
Threads
- Rust standard library threads correspond 1:1 to operating system threads.
- When the main thread completes, all other threads shut down, whether they have completed or not.
1
2
3
| let handle: JoinHandle<i32> = thread::spawn(closure()); // closures returning a value
let joined: thread::Result<i32> = handle.join(); // wait for the thread to finish
let result: i32 = joined.unwrap(); // unwrap result
|
- We must
move
variables to the closure running in a thread.
1
2
3
4
| let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
|
Sleep
1
2
3
4
| use std::thread;
use std::time::Duration;
thread::sleep(Duration::from_secs(1));
|
Channels
std::sync::mpsc::channel()
: Multiple Producer, Single Consumer
1
| let (tx, rx) = mpsc::channel();
|
Transmitter
tx.send()
send
return a Result
which will error if the receiver has dropped- To have multiple transmitters we need to clone
tx
:
Receiver
rx.recv()
blocksrx.try_recv()
doesn’t block.recv
and try_recv
return a Result
, which will return an error if the transmitter is closed.- We can iterate over the received messages
1
2
| for received in rx { ... }
rx.iter().map{ ... }.for_each{ ... }
|
Mutex
1
2
3
4
5
6
7
8
9
| use std::sync::{LockResult, Mutex, MutexGuard};
let m: Mutex<i32> = Mutex::new(5);
{
let result: LockResult<MutexGuard<i32>> = m.lock();
let mut num: MutexGuard<i32> = result.unwrap();
*num = 6;
} // num goes out of scope here, so the lock is released
println!("num = {:?}", m.lock().unwrap());
|
- To give multiple threads access to a Mutex we need to
Arc<T>
, an atomically reference counter
1
2
3
| let mutex: Mutex<i32> = Mutex::new(0);
let counter: Arc<Mutex<i32>> = Arc::new(mutex);
let counter: Arc<Mutex<i32>> = Arc::clone(&counter); // for each thread
|
- Using
Arc<Mutex<T>>
may result in deadlocks.
Visibility Rules
- items in a module are private to that module and its sub-modules
- Making enum
pub
makes its fields public. - Making a struct
pub
, doesn’t make its members public. We need to make them public explicitly. - You can use methods like the
ok
method on Result
or the ok_or
method on Option
to do the conversion explicitly. - To bring something into scope (i.e. import) we need to use
use
. For example use std::io;
- Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude
Notes
- Rust library is called
crate
, a library crate
to be more specific as opposite to binary crate
which is your executable.
Code Organization
- We cannot test code in
main.rs
directly. So, we put our business logic in lib.rs
. Check link
Testing
1
2
3
4
5
6
7
8
| #[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
|
assert!
, assert_eq!
, assert_ne!
1
2
3
4
5
| assert!(
result.contains("Carol"), // assertion
"Greeting did not contain name, value was `{}`", // message
result output
);
|
1
2
3
4
| #[should_panic(expected = "hello")] // contains
fn it_works_some_how() {
panic!("hello world")
}
|
1
2
| #[test]
fn it_works_some_how() -> Result<(), String> {} // will fail on `Err`
|
1
2
3
| #[test]
#[ignore]
fn this_test_not_going_to_run()
|
1
2
3
4
5
6
| cargo test
cargo test part_of_test_name # partial match
cargo test --help # Help for cargo test
cargo test -- --ignored # Runs ignored tests
cargo test -- --help # Help for the test binary
cargo test -- --show-output # Show output from passing tests (printlns)
|
- You can only write integration tests for code in library crates
lib.rs
but not binary crates main.rs
1
2
3
4
5
6
7
8
9
| ├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
│ └── main.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
|
common
is not part of the tests but can be included in the tests (for setup, etc.)- code in lib can be exported
pub
to be used in both main and integration tests
Publishing your package to crates.io
- Once a crate is pulished it cannot be deleted
- login at crates.io
- create a token
cargo login $TOKEN
cargo publish
cargo publish -p package_name
if you have multiple packages in the same workspace.
cargo yank --vers "0.0.1"
Pervents others from downloading and or depending on this version.cargo yank --vers "0.0.1" --undo
The reverse of the previous operationInstalling binary crates
1
2
| cargo install crate_name
cargo uninstall crate_name
|
Resources
- Official Rust Book
- Rustlings
Comments powered by Disqus.