This repository contains code samples and some general principles that guides major concepts in the Rust Programming Language.
Feel free to use as your refresher for certain rust concepts that can be hard to grasp. This is my first low level programming language, it hasn't been easy plus Rust is a really different animal. This short summary is heavily influenced by the Rust Book and Chris Krycho's podcast.
If you find anything wrong, typos, bugs and what not, send a pull request.
Also, remember to star this repo if you found it any useful 😃.
Every value/data has only one owner in Rust. By default, variables are immutable and block scoped (every variable becomes invalid at the end of it's scope. A Scope or block is basically a {..}
), Rust uses borrowing and referencing to deal with data ownership, as opposed to moving data about and having to explicitly return or pass ownership to another block or function for further use.
- At any given time, you can have either (but not both of) one mutable reference or any number of immutable references.
- References must always be valid
- You’re not allowed to modify something that another thing has reference to.
- You can only have one mutable reference to a particular piece of data in a particular scope.
- We also cannot have a mutable reference while we have an immutable one. In other words, we cannot borrow a mutable variable that is also borrowed as immutable.
- Ownership is one of Rust concepts that helps guarantee memory safety without a GC (Garbage Collector). It prevents Rust from having common low level language errors like dangling pointers, segfaults, data races etc. Ownership is Rust's central feature, it basically means that every value has an owner, as a matter of fact, only one owner at a time. One cannot simply understand ownership at first, understanding ownership is understanding most of Rust.
Take for example:
{// name is invalid here
let name = "Timolinn"; // name is valid here
// still valid here
} // name is out of scope, so it's now invalid.
- Rust use the notion of
borrowing
andmoving
to implement ownership rules. - Ownership rules are checked at compilation level using a tool called the
borrow checker
. - Data can be
borrowed
ormoved
, when youborrow
data you get a reference to that value. - Each value in Rust has the
owner
. - No two scopes can
own
the same data/variable, You can have as many immutable references as you want, but you can only have onemutable
reference at a time. - You can't have another reference to a
mutable value
, even it is animmutable reference
. - When the owner goes out of scope, the value will be
dropped
. - Moving large chunks of data can be pretty expensive, use References.
- Rust data values use scoped to determine validity, unless
borrowed
ormoved
the value is dropped at the end of it's scope/block. - Every value remains valid until it goes out of scope.
- In Rust memory belongs to
scope
.
A string slice is a reference to a part of a String
. it is usually created with the &[start..end]
syntax. The resulting string is a reference to a part of the original string. The &str
type or in English string literal can also be referred to as a string slice. Refer to String for more about strings.
For example:
let name = String::from("Timothy"); // Timothy
let nameSlice = &[0..3]; // Tim
let nameSlice2 = &[0..4]; // Timo
The start
and end
act as an index of the characters that make up the String
. The start..end
syntax is a range that begins at start and continues up to, but not including, end
. We can include end
by using ..=
.
Example:
let striker = String::from("Morata");
let striker2 = &striker[0..=3]; // Mora
let striker2 = &striker[..=3]; // Mora
let striker2 = &striker[..]; // Morata
- Slice do not have Ownership
- A slice does not include the the character in the last index unless we add an equal sign.
- Internally, the slice data structure stores the starting position and the length of the slice, which corresponds to
ending_index
minusstarting_index
like here&[starting_index..ending_index]
. - String (
&str
) literals are slices (let s = "Rustlings!"
). - Slices also apply to
arrays
.
Structs are like tuples but more flexible, in that each piece of data can be named in such a way that it is clear what the value means.
struct User {
name: String,
email: String,
age: u32,
sex: String,
active: bool,
}
Struct definition is like a general template for a type, instances fill in that template with particular data to create values of the type. An example of an instance is below:
let MrBean = User {
email: String::from("johnenglish@mi7.com"),
name: String::from("Johnny English"),
age: 18,
sex: String::from("Male"),
active: true
};
- We can use the dot notation to get a specific value eg. use
MrBean.age
to get the value of age. - We can't change the value of any data unless the instance is mutable,
let mut MrBean = User {...}
. - Marking only certain fields as mutable is not allowed by the rust compiler.
- Rust's field init shorthand syntax let's us define a value with a variable of the same name without having to write the variable name twice. So
User { name: name }
is same asUser { name }
impl
blocks (implementation block) hold methods that specify the behavior that instances of the struct possess.structs
are allowed to have multipleimpl
blocks.derive
annotation can be used to add useful behaviors to structs by implementingtraits
automatically.- Rust's Struct update syntax let's create new instances based off the data of a previous instance. eg.
let mut MrsBean = User {
email: String::from("mrsjohnenglish@mi7.com"),
name: String::from("Sandra English"),
sex: String::from("Female"),
age: MrBean.age,
active: MrBean.active // update syntax
};
OR
let mut MrsBean = User {
email: String::from("mrsjohnenglish@mi7.com"),
name: String::from("Sandra English"),
sex: String::from("Female"),
..MrBean
};
Tuple structs have the added meaning the struct name provides but don’t have names associated with their fields; rather, they just have the types of the fields. Useful for scenarios where you want named tuples, much like python's collections.namedtuple
that creates tuple-like objects.
struct Color(i32, i32, i32);
struct Coord(i64, i64);
let black = Color(0, 0, 0);
let naija = Coord(4, 14);
Tuple structs still behaves like ordinary tuples, you can use them with the update syntax, you can also use a .
notation followed by the index to access an individual value.
println!("{}", black.0); // 0
println!("{}", naija.1); // 14
Enumerations or Enums as mostly referred to allows the definition of types by enumerating through a possible set of values. Enums are like variants, no data can be of more than one variant of the same enum. Classic example from the Rust book is the two IP address types (IPV4 and IPV6), the two fundamental types of Ip Addresses. Each Enum variant can be used as a type.
enum IpAddr {
V4,
V6
}
The code above create an enum witht two variants, v4
and V6
. Therefore we can create instances of each type of IP Address like
this:
let ipv4 = IpAddr::V4;
let ipv6 = IpAddr::V6;
ALternatively, we can use structs to improve the code:
struct Ipv4Addr {
addr: String,
}
struct Ipv6Addr {
addr: String,
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
Here we use structs to store our data before wrapping them into our enum. This can also be achieved without using structs. See the example below:
enum IpAddr {
V4(String),
V6(String),
}
let localhost = IpAddr::V4(String::from("127.0.0.1"));
The Option
enum is defined by the Rust standard library,
it encodes the very common scenarios where a value could be something or nothing. Null does not exist in Rust, the Option
enum is Rust's way of expressing a null or not-null scenarios.
enum Option<T> {
Some(T),
None
}
The Option<T>
enum has two variants Some(T)
and None
. The symbol <T>
defines a generic type T
, this means that Some(T)
can contain data/value of any type.
let some_string = Some("Rustacean");
let some_number = Some(12);
let absent_number: Option<i32> = None;
Notice we didn't have to import Option<T>
or Some()
or None
? This is beacuse by defaults they are included in the prelude, that's enough to note how important they are to the language.
- Enums can encapsulate multiples types or variants.
- A variant van be of any type.
- We can pass data directly inside an enum instead of using structs.
- Enums can have
impl
blocks. - The
Option
enum can be beSome
orNone
, swap those withNotNull
orNull
respectively, incase you need more explanation. - The
Option
enum,Some
andNone
values are included in the prelude, therefore you don't need to importOption
to use it or namespaceOption::Some()
to use it. Null
values does not exist in Rust 😎, theOption
enum is here to save the day 😄.- In Rust,
Nullable
values are not assumed. - To extract the
v
inSome(v)
use pattern match or justlet v = Some(v).unwrap();
.
The match
operator is a control flow operator that enables you to compare a value against a series of patterns and executes code as per the pattern matched.
Quoting from the rust book:
Think of a
match
expression as being like a coin-sorting machine: coins slide down a track with variously sized holes along it, and each coin falls through the first hole it encounters that it fits into. In the same way, values go through each pattern in amatch
, and at the first pattern the value “fits,” the value falls into the associated code block to be used during execution.
For example:
enum CryptoCurrencies {
BitCoin,
Ethereum,
LiteCoin,
BitCoinCash
}
fn value_balance(crypto: CryptoCurrencies) -> f32 {
match coin {
CryptoCurrencies::BitCoin => {
println!("Satoshi's pride!");
1
},
CryptoCurrencies::Ethereum => 0.8,
CryptoCurrencies::LiteCoin => 0.5,
CryptoCurrencies::BitCoinCach => 0.2,
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
- Matches are exhaustive, you must cover every possible match case to get Rust to compile your code.
- The
_
is used as default match arm, should the provided value does not match any patter. - The match expression compares the resulting value against the pattern of each arm, in order.
- Curly brackets are allowed when defining associated code of an arm.
According to the Rust Book, The if let
let's you combine if
and let
into less verbose way to handle values that match one pattern while ignoring the rest.
if let Some(3) = some_u8_value {
println!("three");
}
A vector
is a data structure that can store multiple data of the same type, it is one of rust's collections. To create a new and empty vector we can use the conventional new()
method or the vec!
macro.
// note that the type annotation is important in this case because we are creating an empty vector
let v: <i32> = Vec::new();
let n = vec![1, 2, 3];
We can add elements into a vector using the .push
method.
let mut v: <i32> = Vec::new();
v.push(1);
v.push(4);
We can read from a vector using the index signature or the .get
method.
// using the index syntax
let n = vec![1, 2, 3];
let one = n[0];
// using .get
match n.get(3) {
Some(3) => println!("matched letter {}", n.get(3)),
_ => None
}
when we need to store elements of a different type in a vector, we can define and use an enum!
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
- They can contain only same type of data.
- When a
vector
gets dropped, all it's elements are dropped too. - You can add tot a vector using the
.push
method. - You can read from a vector using the index syntax or the
.get
method. - When you try to access an invalid index in a
vecror
, the.get
(v.get(9_000)
) method returnsNone
without panicking while Rust will panick if you perform the same operation using the index syntax (&v[9_000]
). - Rust will not let you extend a mutable vector if there's an immutable reference to the same
vector
already. Check ownership rules above. - You can iterate over elements in a
vector
, mutably or immutably using loops. - Rust needs to know what types will be in the vector at compile time so it knows exactly how much memory on the heap will be needed to store each element.
String
type is a wrapper over a Vec<u8>
. It is utf-8
encoded and is allocated on the heap. There are 3 relevant ways to look at String
type in Rust:
- as bytes
- as scalar values
- as graheme clusters
Rust has only one string type in the core language, which is the string slice str
. The String
type is made available by the Rust std
library, it is growable and can be mutated.
To create a new String
we can do the following:
let mut s = String::new();
s.push_str("Hello World~");
let ns = String::from("Hello world");
let an_s = "hello world".to_string();
// Rust String are utf-8 encoded so we can also do the following.
let hello = String::from("नमस्ते");
let hello = String::from("你好");
Like every other proramming language, there is string concatenation in Rust. Generally there are two ways to string concatenate in Rust:
// using the `+` operator
let hello = String::from("Hello, ");
let world = String::from("World!");
// the hello value wi be moved here
let hello_world = hello + &world; // the hello variable is dropped here.
// OR using the `format!` macro.
let s1 = String::from("are you");
let s2 = String::from("a");
let s3 = String::from("Rustacean");
let s = format!("Hey! {} {} {}?", s1, s2, s3);
Rust's char
type can vary in size, it is based on the string encoding.
For example:
let s = "hello"; // 5 bytes
let slice_s = &s[0..1]; // h
let s2 = "Здравствуйте"; // 24 bytes
let slice_s2 = &s2[0..1]; // PANIC!!!
Because of this reason we must create string slices with caution because it can crash our program if we ever try to access
the wrong range.
Notice let slice_s = &s[0..1];
returns only h
? We can ask rust to include the char
of the index 1
by
slicing like this:
let slice_s = &s[0..=1]; // he
Fortuantely we can manipulate individual char
s of a String
by interating over them. Study and run the examples below:
for c in "नमस्ते".chars() {
println!("{}", c);
}
for b in "नमस्ते".bytes() {
println!("{}", b);
}
Notice the .bytes()
and .chars()
methods?
- Rust has only one string type in it's core, that is the
str
usually seen as&str
in most codebases. - String literals are stored in the binary output of a program and are known as string slices.
- There are three ways to look at a
String
in Rust's perspective,bytes
,scalar values
andgrapheme clusters
. - String indexing is not allowed in Rust for many reasons, one of which is that determining the perspective of which the
programmer is accessing the
String
from is not trivial, due to Rust's way of encoding strings. - Under the hood Rust's
String
type is aVec<u8>
. - The
format!
macro works like theprintln!
macro but returns the string instead printing to stdout. - You can use the
+
or theformat!
macro to concatenate a string. - We can iterate over strings as chars or as bytes.
HashMap<K, V>
stores a mapping of keys of type K to values of type V. It does this via a hashing function, which determines how it places these keys and values into memory. It's like an associative array for the PHP developer or the ES6 Map for Javascript developer.
To create a HashMap
we must first import the type.
use std::collections::HashMap;
let mut scores = HashMap::new();
// use .insert to add key value pairs
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
You can read value from a HashMap
using the .get(k: K)
method.
let team_blue = String::from("Blue");
let team_blue_score = score.get(&team_blue); // This will return a `Some(v)`
Read more about hashmaps here.
HashMaps
store data on the heap.HashMaps
are homogeneous, ie all keys must be of the same type, same goes for all the values.- Rust can infer types within a hashmap.
- Types that implement the
Copy
trait are copied into theHashMap
, while owned types are moved in to theHashMap
.
use std::collections::HashMap;
let color = String::from("Favorite color");
let mut map = HashMap::new();
map.insert(color);
// color is invalid at this point.
- You can pass references into a HasMap and the type will not be moved, however we must use lifetimes to make it work.
- You create a
HashMap<K, V>
using the conventionalnew()
method:let map = HashMap::new();
- You can also create a new
HashMap
from an array of tuples by calling the.collect()
method on it.
use std::collections::HashMap;
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
- Use the
.insert(K,V)
method to add a value to aHashMap<K, V>
and.get(&K)
to retrieve value. - For updating existing
HashMap
read here.
Errors are a fact of life in software development, Rust has a number of ways for handling "errorable" situations. I must mention that Rust does a lot work in trying to standardize error reporting by providing multiple features for handling errors. Rust sometimes takes care of the error handling for you if you want.
One way it does this is by using the ?
, another way is by calling unwrap()
method on a Result
type. Both the ?
and unwrap()
method tells rust to go ahead and eveluate the returned types by returning an Ok(V)
when the operation was successful, by panicking
or propagating an error when the Err()
type is returned. Find examples below.
Errors are grouped into two in Rust, recoverable and unrecoverable errors. Rust does not have exceptions, instead it provides the Result<T, E>
type for recoverable errors and the panic!
macro for crashing a program when it encounters unrecoverable errors.
Examples:
fn main() {
panic!("crash and burn 🔥🔥🔥🔥!"); // stops the program with a message and stacktrace
}
Rust handle Recoverable errors with the Result
enum
enum Result<T, E> {
Ok(T), // T represents the type of value to be returned in a success case
Err(E),// E represents the type of error on failure
}
Most functions in Rust std
and in third party packages return the Result
type if the function can fail
.
use std::fs::File;
fn main() {
// you could do this
let f = File::open("hello.txt").unwrap();
// OR this
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
},
};
}
// OR this
let f = File::open("hello.txt").expect("Failed to open hello.txt"); // `.expect` calls the `panic` for you with the error message provided as an argument.
// OR this
let mut f = File::open("hello.txt")?; // notice the question mark 🙂
- Once you learn how to work with the
Result
type, you'll have easier time propagating or handling errors in Rust. - The
Result
enum has two variantsOk(V)
andErr(E)
. - You can propagate your errors to the calling function by returning the
Result
type. - When your code
panics
it's an unrecoverable error, however you can handle recoverable errors using theResult<V, E>
type. expect()
andunwrap()
methods are better for prototyping and tests. You may use them as placeholders to get your protoype up and running until you're ready to make your code more robust, you can come back use theResult
.- Returning
Result
in a function in your library gives the code caller an option to handle the returned error in a way that fits it's use case/scenario. - Read here for more error handling guideline
Generics
are a tool in Rust for effectively handling code duplication. It is an abstract stand in for concrete types or properties.
That is to say that they are types that can become or enable code perform operations on abstract types. We can use generics to define function signatures or structs that we can use with many different concrete data types.
Take for example:
struct Point<T> {
x: T,
y: T,
}
fn main() {
// The compiler interpretes T as a integer here
let integer = Point { x: 5, y: 10 };
// The compiler interpretes T as a float here
let float = Point { x: 1.0, y: 4.0 };
}
Notice as T becomes whatever type we pass when creating an instance of struct Point<T>
.
We can also define generics in struct methods:
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10 };
let p2 = Point { x: "Hello", y: "world" };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Let's demostrate how generics can remove code duplication.
// returns the largest integer in a vector of i32
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
// returns the largest char in a vector of char
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
We can easily replace the two functions above with one, by using generics
, thereby eliminating code duplication (think DRY).
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
Generics
help minimize duplicate code.- Combining
generics
andtrait
bounds is an idiomatic way of defining desired behaviour for your functions, methods, structs, traits etc. - Using generics does not make your code any slower than using concrete types.
- Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled.
A trait tells the Rust compiler about functionality a particular type has and can share with other types. Traits are similar to a feature often called interfaces in other languages, although with some differences one of which is that traits
in Rust can have default implementations.
We can use trait bounds to specify that a generic can be any type that has certain behavior. A type’s behavior consists of the methods we can call on that type.
pub trait Summary {
fn summarize(&self) -> String;
fn summarize_author(&self) -> String;
fn author(&self) -> String {
format!("Written by {:?}", self.summarize_author())
}
}
The Summary
trait has three methods, summarize
, summarize_author
and author
. The author
method has a default implementation this means that any type that implements this trait has access to the the deafault method.
Implementing a trait on a type:
pub struct Tweet {
pub message: String,
pub author: String
}
pub struct NewsArticle {
pub headline: String,
pub author: String
}
pub struct LincolnsLetter {
pub subject: String,
pub author: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("The lad tweeted: {}", self.message)
}
fn summarize_author(&self) -> String {
format!("@{:?}", self.author)
}
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}!!!", self.headline)
}
fn summarize_author(&self) -> String {
format!("{}", self.author)
}
}
impl Summary for LincolnsLetter {
fn summarize_author(&self) -> String {
format!("{}", self.author)
}
}
Implementing a trait on a type is similar to implementing regular methods. The difference is that after impl, we put the trait name that we want to implement, then use the for keyword, and then specify the name of the type we want to implement the trait for.
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
The notify function takes a generic type parameter of T
that must implement the Summary
trait.
We can also use the +
operator and the where
keyword to define clearer and more concise trait bounds.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
The code above is same as the code below but made more clearer with the where keyword:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
- One restriction to note with trait implementations is that we can implement a trait on a type only if either the trait or the type is local to our crate. it means we can’t implement external traits on external types.
- Traits are similar to a feature often called interfaces in other languages, although with some differences one of which is that
traits
in Rust can have default implementations. - Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.
- The Summary trait would also need to be a public trait for another crate to implement it.
- Default implementations can call other methods in the same trait, even if those other methods don’t have a default implementation.
- Use the
where
keyword for a clearer implementation. - To return a type that implements some trait we do
fn some_function() -> impl SomeTrait
. - We can also conditionaly implement methods based on trait bounds. More here
Every reference
in Rust has a lifetime
. Lifetimes
specify how long a reference should live before it gets dropped
.
Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime will definitely be valid.
It’s possible for structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition
Example from the Rust book:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}
In the example above, an instance of ImportantExcerpt
can’t outlive the reference it holds in its part
field.
-
Lifetimes
are mostly inferred, just liketypes
. Same way just liketypes
,lifetimes
must be annotated when multiplelifetimes
are possible. -
When we specify the lifetime parameters in a function signature, we’re not changing the
lifetimes
of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to the constraints. -
The smaller of the
lifetimes
specified in a function definition becomes the main constraint in determining if the references will still be valid at the end of the function scope. -
When returning a reference from a function, the
lifetime
parameter for the return type needs to match thelifetime
parameter for one of the parameters, or to a value created within the function, which would be a dangling reference because the value will go out of scope at the end of the function. -
The patterns programmed into Rust’s analysis of references are called the lifetime elision rules.
-
Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.
-
The compiler uses three rules to figure out what lifetimes references have when there aren’t explicit annotations, they include
- Each parameter gets its own lifetime.
- If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- If there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters
The compiler throws an error if all three laws fail.
- Closures are also known as anonymous functions. They are functions defined like variables and can also be passed as arguments to other functions. Unlike functions they have the ability to capture values from the scope of which they are defined
let a = 2;
let b = 4;
let calc = |a, b| {
a + b
};
Closures
can capture values from the scope they were defined.- They can be defined and assigned to a variable and executed in a different context.
- They do not require type anotations of parameters and return types.
Closures
may implement the following 3 function traits provided by the rust standard library:-
FnOnce
: This means the closure takes ownership of the variables from it's scope and consumes them, thereby they can only be called once. This can be forced with themove
keywork:let x = 3; let y = 40; let product = move |x, y| x * y;
-
FnMut
: This means the closure borrows the variables mutably -
Fn
: This means the closure borrows the variables immutably
-
Smart pointers originated from C++, they are like references (&
) but provide more capabilities and contain metadata. Some smart pointers in rust that we've already used are the String
and Vec<T>
types, both own some data and provide some metadata and capabilities on them.
The following are other smart pointers in rust:
Box<T>
- for allocating data on the heap.Rc<T>
- for creating multiple ownerships of dataRef<T>
andRefMut<T>
- accessed through theRefCell<T>
, it enforces the borrowing and ownership rules of Rust at runtime.
-
The
Box<T>
type is a smart pointer because it implements theDeref
trait, which allowsBox<T>
values to be treated like references. -
When a
Box<T>
value goes out of scope, the heap data that the box is pointing to is cleaned up as well because of theDrop
trait implementation. -
Deref coercion is a convenience that Rust performs on arguments to functions and methods. Deref coercion converts a reference to a type that implements Deref into a reference to a type that Deref can convert the original type into.
fn hello(name: &str) { println!("Hello, {}!", name); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
In the code above we don't need to explicitly add the deref operatpr (
*
) because of the implicit deref coercion feature of rust. -
The
Drop
trait lets you customize what happens when a value goes out of scope. This is basically a function calleddrop
that the Rust calls automatically when the value is out of scope. The compiler inserts the call to this method where needed.
Concurrency is when different parts of a program execute independently, while parallellism is when different parts of a program run at the same time. When we say concurrency think Processes, Threads. A process contains 1 or more threads, the process manages the resources available to the threads. A thread is a sequence of instructions that is to be executed by the operating system.
Concurrency can improve the performance of our programs, but they complexity in them. When multiple threads are running in the same memory space, sharing the same resource available in a process this can lead some known problems in our programs;
- Race Condition: Multiple threads trying to access the same memory
- Deadlocks: Two threads waiting on each other to finish using a resource one of them is supposedly using, preventing both threads from continuing.
- Hard to reproduce circumstance that led to a bug.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
Note that the handle.join().unwrap()
call ensures that the main thread waits for the spawnwd thread to complete execution before exiting.
Channels are means for communication between threads. Threads or Actors communication by sending each other messages containing data through channels.
use std::sync::mpsc;
use std::thread;
pub fn main() {
let (tx, rx)= mpsc::channel();
thread::spawn(move || {
let value = 50;
tx.send(value).unwrap();
});
let recvd = rx.recv().unwrap();
println!("Got: {}", recvd);
}
The Rust compiler won't compile if the data types that would pass through the channel is not defined, in the example above, rust infers the data types through the usage in the spawned thread.
Shared State Concurrency is another way of handling concurrency apart from using channels. Rust is well equipped to use shared state concurrency effectively compared to many other programming languages. We can effectively share memory with Mutexes which is so tricky that most developers prefer to use channels for concurrency, however the combination of Rust's type system, smart pointers and ownershiop rules, we cannot get locking nad unlocking with mutexes wrong.
use std::sync::Mutex;
pub fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 9;
}
println!("{:?}", m);
}
Combination of mutexes and smart pointers enable us write programs that uses multi threading and multiple ownerships
. This can be achieved by using the Arc<T>
from the standard library.
use std::sync::{Mutex, Arc};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for idx in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
println!("Thread {} spawned", idx);
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
While the Arc<T>
and mutex locks enable us to have multiple ownerships between threads, we can as well use the std::marker::Send
trait to transfer ownership between threads. Also the std::marker::Sync
indicates that a reference to T
(&T
) can be sent saftely to another thread.
- Rust has much lower level control over operating system threads as opposed to
green threads
in Golang. - A channel in programming has two halves: a transmitter and a receiver. The transmitter half is the upstream location where you a rubber ducks into a river, and the receiver half is where the rubber duck ends up downstream.
- A channel is a means of communication between threads. Do not communicate by sharing memory, share memory by communicating - Golang Slogan
- The
channel
takes ownership of the sent variable. - The Rust std implementation of channels allow only one
receiver
but can allow multipleproducers
orsenders
- Shared-State Concurrency is another way of handling concurrency, it simply means
sharing memory
in the sense that data/value transferred through a channel maybe accessed by multiple threads which leads to multiple ownership. Rust is very much equipped to handle this type of concurrency with it's type system, smart pointers and ownership rules. - Shared state concurrency requires the use of
Mutexes
(mutual exclusion). This is the process ofguarding
the data by the mutex. Every thread is required to request for access before accessing the data from the mutex's lock. - Using mutexes can be incredibly tricky, however, thanks to Rust's type system you cannot get locking and unlocking wrong.
Mutex<T>
provide interior mutability and can be used to mutate contents inside anArc<T>
, same way theCell
family does. Specifically, we can provide interior mutability in aRc<T>
using theRefCell<T>
.Arc<T>
provides multiple ownership functionality in a thread safe way as opposed toRc<T>
it is atomic reference counted type.Mutex<T>
comes with the risk of creating deadlocks. These occur when an operation needs to lock two resources and two threads have each acquired one of the locks, causing them to wait for each other forever.