Rust
Intro
Hello world!
1 | fn main() { |
Language Overview
Overview of the language with a guessing game program:
1 | use std::io; |
Common Programming Concepts
Variables and mutability
- Variables are by default immutable. Use
mutkeyword to make it mutable.
1 | let x = 5; |
- Use const keyword to specify constants. Must annotate type.
1 | fn main() { |
- Redefining variables shadows the old variable.
1 | fn main() { |
Data Types
Scalar Types
- Integer data types: i8, u8, i32, u32, i64, u64, i128, u128, isize, usize.
Default i32. - Floating point types: f32, f64. Default f64.
- Boolean type: true, false.
- Char type: ‘x’. 4 bytes, so unicode char is also supported.
let x: char = 'x'.
Compound Types
Tuples:
- Fixed size in compile time.
- Can contain different types.
- Use
vaiable_name.INDEXto specify individual elements: INDEX starts from 0.
1 | let tup = (500, 'x', 10.5); |
Array:
- Contains elements of fixed types.
1 | let a = [1, 2, 3, 4, 5]; |
Functions
1 | fn main() { |
Statements vs Expressions
- Expressions evaluate to values.
- Expressions doesn’t end with semicolon.
- Expressions can be converted to statements by using semicolon at the end.
- Function return is specified using expressions in rust.
Control Flow
if/else
- Condition must be boolean
1 | fn main() { |
- If is an expression so can be used in let statement
1 | fn main() { |
Looping
loop keyword:
- Loops indefinitely.
- break can use expression to return value.
- Loops can be named and used in break
Loop labels must begin with a single quote.
Example: break with expression
1 | fn main() { |
Example: named loop
1 | fn main() { |
while keyword:
1 | fn main() { |
for keyword:
- Used to loop through a collection:
Example 1:
1 | fn main() { |
Example 2:
1 | fn main() { |
Ownership in Rust
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.
In rust the memory is automatically returned
once the variable that owns it goes out of scope.
When a variable goes out of scope rust calls a special
method drop(). This is implemented on the specific type.
This function is responsible for cleaning up memory
for that type.
Variables and Data Interacting with Move
Rust always do shallow copy.
1 | let s1 = String::from("hello"); |
Variables and Data Interacting with Clone
It is possible to do deep copy with clone() method.
1 | let s1 = String::from("hello"); |
Ownership and Functions
Ownership is also move in function parameters:
1 | fn main() { |
Return Values and Scope
- Return values can also transfer ownership
1 | fn main() { |
Reference and Borrowing
To use functions with parameters you need
to give ownership of the parameters to the
function and then return it back:
1 | fn main() { |
To remove this hassle one can use references to refer
to some value without taking ownership. This is called
borrowing in rust terminology.
1 | fn main() { |
& is used to specify references.
To change the value of the variable that
a reference is pointing to, the reference also
need to be mutable. For example following code
won’t compile:
1 | fn main() { |
Use mut keyword to specify mutable reference:
1 | fn main() { |
Rules for mutable references:
- There can be as many as immutable references.
- There can be only one mutable reference simultaneously.
- Mutable reference can’t be mixed up with immutable reference
as immutable reference is expecting value won’t change. - This restrictions are given to remove data race at compile time.
Example 1: This won’t compile as two mutable reference
of the same variable.
1 | fn main() { |
Example 2:
1 | fn main() { |
Example 3:
1 | fn main() { |
Example 4:
1 | fn main() { |
Dangling References
It is not possible to create dangling references
in rust. Compiler does a lifetime analysis and will
be caught.
This will not compile and give compiler error:
1 | fn main() { |
The Rules of References
At any given time, you can have either one mutable
reference or any number of immutable references.References must always be valid.
Slice Type
Slices are a kind of reference which points
to a portion of a value.
&str - type of string slice.&[i32] - Type of i32 array slice.&[T] - Type of slice of array of type T.
String Slices
Type of string slice is &str
Example 1:
1 |
|
Example 2:
1 |
|
Example 3:
1 |
|
Example 4:
1 |
|
Example 5:
1 | fn first_word(s: &String) -> &str { |
Structs
Defining and Instantiating Structs
Definition:
1 | struct User { |
Using the struct:
1 | struct User { |
Specific field is specified using the dot notation:
1 | struct User { |
- It’s not possible to set a specific field as mutable.
All the fields have to be mutable.
Struct instantiation is an expression so it is possible to use
as functions return:
1 | struct User { |
Using the Field Init Shorthand
If field name and parameter name is same
then you don’t need to specify field names:
1 | struct User { |
Creating Instances From Other Instances With Struct Update Syntax
The syntax .. specifies that the remaining fields
not explicitly set should have the same value as
the fields in the given instance:
1 | struct User { |
Update syntax uses move semantics. So if any of the fields
uses move semantic then the ownership will be moved.
For example user1 will be invalid
after using the update syntaxt on user2.
as email and username field ownership will be moved
Using Tuple Structs without Named Fields to Create Different Types
1 | struct Color(i32, i32, i32); |
Unit-Like Structs Without Any Fields
Unit-like structs can be useful when you need to
implement a trait on some type but don’t have any data
that you want to store in the type itself
1 | struct AlwaysEqual; |
Ownership of Struct Data
To use a reference in a struct field you
need to specifiy lifetime to make sure
that the reference will be valid as long as
the struct is valid.
Defining methods of a struct
1 |
|
Rust doesn’t have an equivalent to the -> operator;
instead, Rust has a feature called automatic referencing and dereferencing.
Methods with More Parameters
1 |
|
Associated Functions
All functions defined within an impl block are called
associated functions because they’re associated with
the type named after the impl.We can define associated functions that don’t have
self as their first parameter.Associated functions that aren’t methods are often
used for constructors that will return a new instance of the struct.To call this associated functions we use :: after struct name.
1 |
|
It is allowed to have multiple impl block for the
same struct.
1 |
|
Enum and Pattern Matching
Defining Enum
1 | enum IpAddrKind { |
Enum’s can have data in them.
Consider following example:
1 | fn main() { |
This same program can be expresses with:
1 | fn main() { |
Methods on Enums
Similar to structs it is possible to implement
method on enums:
1 | fn main() { |
The Option Enum and Its Advantages Over Null Values
Rust doesn’t have NULL
The
Optiontype encodes the scenario in which
a value could be something or it could be nothing.Expressing this concept in terms of the type system
means the compiler can check whether you’ve handled
all the cases you should be handlingOptionis implemented as following enum:1
2
3
4enum Option<T> {
None,
Some(T),
}
Example:
1 | fn main() { |
Option<T>can be used withT.Option<T>can
be converted toTby handling null case.
Example:
1 | fn main() { |
The match Control Flow Construct
Can be used to compare a value agains a
series of patters and execute code based
on the pattern matched.This confirms the compiler that all possible
cases are handled.Each pattern is called an arm
Example:
1 | enum Coin { |
Another Example 2:
1 |
|
- Matches are exhaustive. You have to handle
all the possible cases.
Catch-all Patterns and the _ Placeholder
Example 1:
1 | fn main() { |
Rust will warn if any arm is specified after the catch all
pattern.It is possbile to use
_to add a catch all pattern
but don’t use the value.
1 | fn main() { |
- It also possible to take no action
for an arm:
1 | fn main() { |
Concise Control Flow with if let
- Works like reduced
match. Only handle one case
and ignore all the other case.
Consider following code:
1 | fn main() { |
This can be expressed more consciously with if/let combo:
1 | fn main() { |
Common Collection
Vectors
- Stores values of same type
- You must annotate the type while creating
a new vector.
1 | let v: Vec<i32> = Vec::new(); |
- If initial values are provided rust will infer the type.
1 | let v = vec![1, 2, 3]; |
- Updating a vecotr:
1 | fn main() { |
- There are two ways to reference a value stored in a vector:
via indexing or using the get method. If get method is used
Option<&T> is returned.
1 | fn main() { |
- Ownership and borrowing rules are applicable for references
to a vector. Following code won’t compile:
1 | fn main() { |
- Iterating over the values of a vector:
Immutable reference:
1 | fn main() { |
Mutable reference: To change the value that the mutable reference
refers to, we have to use the * dereference operator to get to the value .
1 | fn main() { |
- Using Enum to store multiple types: Vectors can only store values
that are the same type. Enums can be used as workaround for this.
With enum definitions different types can coexists in a vector.
1 | fn main() { |
Hashmap
- All keys have to be same type and
all values have to same type.
1 | fn main() { |
- Accesing values in a hashmap: Values can be accesed
withgetmethod. Get method returnsOptions<&T>.copiedmethod can be used to convert toOptions<T>.
In the following exampleunwrap_oris used to convert
the value to 0 if returned value is None (If no such key).
1 | fn main() { |
- Hashmap and ownership: For types that implement the Copy trait,
like i32, the values are copied into the hash map.
For owned values like String, the values will be moved and
the hash map will be the owner of those values
1 | fn main() { |
Inserting a value for the same key will overwrite it’s value
Adding a Key and Value Only If a Key Isn’t Present:
entrymethod returns an enumEntrythat represents
a value that might or might not exist. Theor_insert
method on Entry is defined to return a mutable reference
to the value for the corresponding Entry key if that key exists,
and if not, inserts the parameter as the new value
for this key and returns a mutable reference to the new value.
1 | fn main() { |
- Updating a Value Based on the Old Value:
1 | fn main() { |
Error Handling
- Rust doesn’t have execptions.
Result<T, E>for recoverable errors.panic!()macro for unrecoverable erros.
Recoverable Errors with Result
- Result Enum:
1
2
3
4enum Result<T, E> {
Ok(T),
Err(E),
}
Example 1:
1 | use std::fs::File; |
Example 2:
1 | use std::fs::File; |
- Using closures to write above program without match syntax
1
2
3
4
5
6
7
8
9
10
11
12
13
14use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
Shortcuts for Panic on Error: unwrap and expect
The unwrap method is a shortcut method implemented
just like the match expression. If the Result value is
the Ok variant, unwrap will return the value inside the Ok.
If the Result is the Err variant, unwrap will call the panic! macro
1 | use std::fs::File; |
expectsworks like same but gives the ability
to write custom panic message:
1 | use std::fs::File; |
Propagating erros
Instead of handling function returns
error to the caller to handle:
1 |
|
A Shortcut for Propagating Errors: the ? Operator
The ? placed after a Result value is defined to work
in almost the same way as the match expressions is
defined to handle the Result values.
If the value of the Result is an Ok,
the value inside the Ok will get returned from this expression,
and the program will continue. If the value is an Err,
the Err will be returned from the whole function as if
the return keyword is used so that the error value gets
propagated to the calling code.
There is a difference between what the match expression does
and what the ? operator does: error values that have the ? operator
called on them go through the from function, defined in the From trait
in the standard library, which is used to convert values from
one type into another. When the ? operator calls the from function,
the error type received is converted into the error type
defined in the return type of the current function.
This is useful when a function returns one error type to represent
all the ways a function might fail,
even if parts might fail for many different reasons.
1 |
|
This code can be made even shorter:
1 |
|
The ? operator can only be used in functions
whose return type is compatible with the value the ? is used on.
This operator can be used in a function that returns Result, Option,
or another type that implements FromResidual.
? operator can be used on a Result in a function that returns Result,
and you can be use on an Option in a function that returns Option.
To use ? in
main()return type of main needs to beResult<(), E>:
1 | std::error::Error; |
Generic Data Types
In Function Definitions
Example 1: This won’t compile as std::cmp::PartialOrd
trait isn’t implemented:
1 | fn largest<T>(list: &[T]) -> &T { |
In Struct Definitions
Example 1:
1 | struct Point<T> { |
Example 2:
1 | struct Point<T, U> { |
In Enum Definitions
Example 1:
1 |
|
Example 2:
1 |
|
In Method Definitions
Example 1:
1 | struct Point<T> { |
It is possible to implement method on a concrete type.
This will restrict the method for that specific type.
If a method is implemented on P<f64> that method will
only be implemented for P<f64>. Other P<T> won’t
have that method.
1 | struct Point<T> { |
Example 2: A more generic example
1 | struct Point<X1, Y1> { |
Monomorphization: Rust compiler turns the generic
code into concrete type.
Traits
A trait defines functionality a particular type has
and can share with other types.We can use trait bound to specify that a generic
type can be any type that has certain behaviour.
Defining a Trait
Example:
1 | pub trait Summary { |
Implementing a Trait on a Type
Example:
1 | pub trait Summary { |
A trait can be implemented on a type
if at least one of the trait or the type
it local to the crate. For example,
we can implement standard library traits
like Display on a custom type like Tweet
as part of our aggregator crate functionality,
because the type Tweet is local to our aggregator crate.
We can also implement Summary on Vecin our
aggregator crate, because the trait Summary is local
to our aggregator crate. But we can’t implement
the Display trait on Vecwithin our aggregator crate,
as both are external to our aggregator crate.
Default Implementations
- If implementation is provided for a
trait method, then it is not required
to implement that method for a type.
If a type doesn’t implement it the
default implementation of the function
will be used.
1 | pub trait Summary { |
- If a default implementation isn’t provided,
for a method, then a implementation must be
provided by the types that are using the
trait.
1 | pub trait Summary { |
Trait As Parameter
If a Trait is used a parameter to a function,
then that function will accept any type that
implements that Trait.
1 | pub trait Summary { |
Trait Bound Syntax
Function signatures can be written
more concisely with trait bound box.
For example notice following function:
1 | pub fn notify(item: &impl Summary) { |
With trait bounds this can be written:
1 | pub fn notify<T: Summary>(item: &T) { |
Benefit of this will be more clear with multiple
Trait parameter:
1 | pub fn notify(item1: &impl Summary, item2: &impl Summary) { |
With Trait bound syntax:
1 | pub fn notify<T: Summary>(item1: &T, item2: &T) { |
Specifying Multiple Trait Bounds with the + Syntax
We can specify more than one Trait bound.
Following function will accept any type that
implements Summary and Display trait.
Without Trait bound syntax:
1 | pub fn notify(item: &(impl Summary + Display)) { |
With Trait bound syntax:
1 | pub fn notify<T: Summary + Display>(item: &T) { |
Clearer Trait Bounds with where Clauses
Consider following function with Trait bounds:
1 | fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { |
With where clause this can be written:
1 | fn some_function<T, U>(t: &T, u: &U) -> i32 |
Returning Types that Implement Traits
impl Traitsyntax can be used in function
return to return a type that implements that
trait.impl Traitcan only be used if the function
returns a single type.
Example:
1 | pub trait Summary { |
Following code won’t compile as it can return different
types:
1 | pub trait Summary { |
Using Trait Bounds to Conditionally Implement Methods
1 | use std::fmt::Display; |
We can also conditionally implement a trait for
any type that implements another trait.
Implementations of a trait on any type that
satisfies the trait bounds are called blanket implementations.
1 | impl<T: Display> ToString for T { |
Lifetime
Lifetimes ensure that references are valid
as long as we need them to be.Most of the cases lifetime is automatically inferred.
we must annotate lifetimes when the
lifetimes of references could be related in a few different ways.
Rust requires us to annotate the relationships
using generic lifetime parameters to ensure
the actual references used at runtime will definitely be valid.The main aim of lifetimes is to prevent dangling references,
which cause a program to reference data other than
the data it’s intended to reference.
The Borrow Checker
The Rust compiler has a borrow checker that compares
scopes to determine whether all borrows are valid.Following program will be rejected because the
borrow checker will see that x’s lifetime ‘b is
shorter than r’s lifetime ‘a which is referencing
x.1
2
3
4
5
6
7
8
9
10fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+Following code will be accepted x’s lifetime ‘b
is longer than r’s lifetime ‘a. So the compiler
knows r will be always valid when x valid.1
2
3
4
5
6
7
8fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
Lifetime Annotation Syntax
1 | &i32 // a reference |
Lifetime Annotations in Function Signatures
Following function signature tells Rust that
for some lifetime ‘a, the function takes two parameters,
both of which are string slices that live
at least as long as lifetime ‘a.
The function signature also tells Rust
that the string slice returned from the function
will live at least as long as lifetime ‘a.
1 | fn main() { |
- When returning a reference from a function,
the lifetime parameter for the return type
needs to match the lifetime parameter for one of the parameters.
Lifetime Annotations in Struct Definitions
- We can define structs to hold references,
but in that case we would need to add
a lifetime annotation on every reference
in the struct’s definition.
1 | struct ImportantExcerpt<'a> { |
- This annotation means an instance of ImportantExcerpt
can’t outlive the reference it holds in its part field.
Lifetime Elision
The compiler uses three rules to figure out
the lifetimes of the references when there aren’t
explicit annotations. The first rule applies to
input lifetimes, and the second and third rules
apply to output lifetimes. If the compiler
gets to the end of the three rules and there are still
references for which it can’t figure out lifetimes,
the compiler will stop with an error.
These rules apply to fn definitions as well as impl blocks.
The first rule is that the compiler assigns
a lifetime parameter to each parameter that’s a reference.
In other words, a function with one parameter gets
one lifetime parameter:fn foo<'a>(x: &'a i32);
a function with two parameters gets two separate
lifetime parameters:fn foo<'a, 'b>(x: &'a i32, y: &'b i32);and so on.The second rule is that, if there is
exactly one input lifetime parameter,
that lifetime is assigned to all output
lifetime parameters:fn foo<'a>(x: &'a i32) -> &'a i32.The third rule is that, if there
are multiple input lifetime parameters,
but one of them is&selfor&mutself because this is a method,
the lifetime of self is assigned to all output lifetime parameters.
Lifetime Annotations in Method Definitions
Example 1:
1 | struct ImportantExcerpt<'a> { |
The Static Lifetime
'staticlifetime denotes that the
affected reference can live for
the entire duration of the program.All string literals have the
'staticlifetime
Generic Type Parameters, Trait Bounds, and Lifetimes Together
1 | fn main() { |
Closures: Anonymous Functions that Capture Their Environment
Capturing the Environment with Closures
- The
unwrap_or_else()method onOption<T>
is defined by the standard library.
It takes one argument: a closure without any arguments
that returns a value T (the same type stored
in the Some variant of theOption<T>)
1 |
|
Closure Type Inference and Annotation
Closures usually don’t require to specify
types of the parameter or the return value
like functions.Closures are stored in variables and used
without naming them and expose them only
to the user of the specific library.Closures are short and relevant only
within a narrow context.Within these limited contexts,
the compiler can infer the types
of the parameters and the return typeIt’s also possible to annotate type
like variables.
Example:
1 | use std::thread; |
Comparison with functions syntax:
1
2
3
4fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;If type isn’t annotated compiler infers
closure’s type.1
2
3
4
5
6
7
8
9
10
11
12fn main() {
let example_closure = |x| x;
// Compiler infers x as String and return type as String
// and locks this inference.
let s = example_closure(String::from("hello"));
// This will produce a compiler error
// as the compiler expects the parameter to be
// a string
let n = example_closure(5);
}
Capturing References or Moving Ownership
Closures can capture values from their environment in three ways,
which directly map to the three ways a function can take a parameter:
borrowing immutably, borrowing mutably, and taking ownership.
The closure will decide which of these to use based on
what the body of the function does with the captured values.
Captures an immutable reference:
1
2
3
4
5
6
7
8
9
10fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let only_borrows = || println!("From closure: {:?}", list);
println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);
}Captures a mutable reference:
1
2
3
4
5
6
7
8
9fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {:?}", list);
}Between the closure definition and the closure call,
an immutable borrow to print isn’t allowed because no other
borrows are allowed when there’s a mutable borrow.To force the closure to take ownership of the values it uses in the environment
even though the body of the closure doesn’t strictly need ownership,
you can use the move keyword before the parameter list.
This technique is mostly useful when passing a closure to a new thread
to move the data so that it’s owned by the new thread.
1 | use std::thread; |
Moving Captured Values Out of Closures and the Fn Traits
Once a closure has captured a reference or captured ownership of a value
from the environment where the closure is defined (thus affecting what,
if anything, is moved into the closure), the code in the body of the closure
defines what happens to the references or values when the closure is evaluated later
(thus affecting what, if anything, is moved out of the closure).
A closure body can do any of the following: move a captured value out of the closure,
mutate the captured value, neither move nor mutate the value,
or capture nothing from the environment to begin with.
The way a closure captures and handles values from the environment affects
which traits the closure implements, and traits are how functions and structs
can specify what kinds of closures they can use. Closures will automatically
implement one, two, or all three of these Fn traits, in an additive fashion,
depending on how the closure’s body handles the values:
FnOnceapplies to closures that can be called once.
All closures implement at least this trait, because all closures can be called.
A closure that moves captured values out of its body will only implement FnOnce
and none of the other Fn traits, because it can only be called once.FnMutapplies to closures that don’t move captured values out of their body,
but that might mutate the captured values. These closures can be called more than once.Fnapplies to closures that don’t move captured values out of their body
and that don’t mutate captured values, as well as closures that capture nothing
from their environment. These closures can be called more than once without mutating
their environment, which is important in cases such as calling a closure multiple times concurrently.
Definition of
unwrap_or_else(): ImplementsFnOnce1
2
3
4
5
6
7
8
9
10
11impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}Usage of
sort_by_key(): ImplementsFnMut1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
// this closure is called multiple times
list.sort_by_key(|r| r.width);
println!("{:#?}", list);
}
Iterators
Processing a Series of Items with Iterators
Example:
1 | fn main() { |
The Iterator Trait and the next Method
All iterators implement a trait named Iterator that is defined in the standard library.
The definition of the trait looks like this:
1 | pub trait Iterator { |
To use iterator for a custom type it has to implement
Iterator trait. To implement Iterator define an Item type,
and use this Item type in the return type of the next method.
values we get from the calls to next are immutable references to the values in the vector.
The iter method produces an iterator over immutable references.
If we want to create an iterator that takes ownership of v1 and returns owned values,
we can callinto_iterinstead of iter.
Similarly, if we want to iterate over mutable references, we can calliter_mutinstead of iter.
Methods that Consume the Iterator
- Methods that call next are called consuming adaptors,
because calling them uses up the iterator.
Example: sum method, which takes ownership of the iterator
and iterates through the items by repeatedly calling next, thus consuming the iterator.
1 |
|
Methods that Produce Other Iterators
- Iterator adaptors are methods defined on the Iterator trait
that don’t consume the iterator. Instead, they produce
different iterators by changing some aspect of the original iterator.
1 | fn main() { |
Using Closures that Capture Their Environment
1 |
|
Fearless Concurrency
Using Threads to Run Code Simultaneously
- The Rust standard library uses a 1:1 model of thread implementation,
whereby a program uses one operating system thread per one language thread.
Smart Pointers
Box to Point to Data on the Heap
Situations:
To use a type whose size can’t be known at compile time
and to use a value of that type in a context that requires
an exact size.To handle large amount of data and transfer ownership but
unsure data won’t be copied while transfering ownership.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
Usage
1 | fn main() { |
Enabling Recursive Types with Boxes
- Rust Doesn’t allow recursive types as their size can’t be
known in compile time.
1 | // Infinite recursion here |
Using Box to Get a Recursive Type with a Known Size
1 | enum List { |
Treating Smart Pointers Like Regular References with the Deref Trait
Following the Pointer to the Value
1 | fn main() { |
Using Box Like a Reference
1 | fn main() { |
Defining Your Own Smart Pointer
Create a new type MyBox<T>
1 | struct MyBox<T>(T); |
But deferencing won’t work on MyBox
1 | fn main() { |
Implementing the Deref Trait
1 | use std::ops::Deref; |
Implicit Deref Coercions with Functions and Methods
Deref coercion converts a reference to a type that implements
the Deref trait into a reference to another type.Deref coercion is a convenience Rust performs on arguments to
functions and methods, and works only on types that implement the Deref trait.It happens automatically when we pass a reference to a particular type’s
value as an argument to a function or method that doesn’t match the parameter
type in the function or method definition
Example: Reference to MyBox converts into &str
1 | use std::ops::Deref; |
How Deref Coercion Interacts with Mutability
Rust does deref coercion when it finds types and trait
implementations in three cases:
- From
&Tto&UwhenT: Deref<Target=U> - From
&mut Tto&mut UwhenT: DerefMut<Target=U> - From
&mut Tto&UwhenT: Deref<Target=U>
- In 1: Converts &T to &U
- In 2: Converts &mut T to &mut U
- In 3: Converts &mut T to &U. But &T to &mut U isn’t allowed
Cleanup with the Drop Trait
Example implementing Drop trait:
1 | struct CustomSmartPointer { |
Dropping a Value Early with std::mem::drop
It is not possible to disable drop functionality or
call drop method on a type directly but memory
can be freed earlier using the std::mem::drop function.
1 | struct CustomSmartPointer { |
Rc<T>, the Reference Counted Smart Pointer
Share ownership among multiple owners.
Only applicable for single threaded case. (Arc
for multi threade). Rc<T>is useful to allocate data from the heap for multiple
parts of a program to read and it is not possible to determine at
compile time which part will finish using the data lastKeeps a reference count of the owners. Will free the object when
count goes to zero.Convention is to use
Rc::clone(&T)instead ofT.clone().
As in most casesT.clone()does deep copy. Though in this case
it will do a referance count instead of deep copy. But by usingRc::clone(&T)it possible to visually tell that it is doing
reference counting.
Example 1:
1 | enum List { |
Example 2:
1 | enum List { |
RefCell<T> and the Interior Mutability Pattern
Interior mutability: Interior mutability is a design pattern
in Rust that allows you to mutate data even when there are
immutable references to that data. To mutate data,
the pattern uses unsafe code inside a data structure
to bend Rust’s usual rules that govern mutation and borrowing.
With references and
Box<T>borrowing rules are enforced
at compile time but withRefCell<T>borrowing rules are
enforced at runtime.Can only be used in single threaded scenario.
Interior mutability is possible with
RefCell<T>.To get interior mutability enclose the type
inRefCell<T>, then callborrow_mut()method
on the object to get mutable reference andborrow()
method to get immutable reference.borrow()method returnsRef<T>andborrow_mut()
returnsRefMut<T>.Same borrowing rules applied for
RefCell<T>but
enforced at runtime.
Example:
1 | pub trait Messenger { |
Having Multiple Owners of Mutable Data by Combining Rc<T> and RefCell<T>
Rc<T> provides multiple ownership to immutable data.
But with combination of RefCell<T> we can get multiple
ownership of immutable data.
Example:
1 | #[derive(Debug)] |