Let's continue with more complex Rust concepts and structures.
References, Ownership, and Borrowing
The stack is used for static memory allocation, while the heap is used for dynamic memory allocation, but both are stored in RAM, allowing for fast access by the CPU.
The stack is suitable only for data whose size is predetermined and constant. Conversely, data with variable or unknown sizes at compile time must be stored on the heap.
Ownership rules
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Borrowing rules
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
Ownership and functions
fn main() {
let num = 5;
copy_value(num); // num is copied by value
let text = String::from("Hello Rust!");
take_ownership(text); // text is moved into the function
let new_text = give_ownership(); // return value is moved into new_text
let some_text = String::from("Rust");
let final_text = take_and_give_back(some_text); // some_text is moved and then returned to final_text
}
Creating references
let text1 = String::from("hello world!");
let ref1 = &text1; // immutable reference
let mut text2 = String::from("hello");
let ref2 = &mut text2; // mutable reference
ref2.push_str(" world!");
Iterators
Iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up.
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
Note that v1_iter
is mutable: calling the next
method on an iterator changes internal state that the iterator uses to keep track of where it is in the sequence. In other words, this code consumes the iterator. Each call to next
eats up an item from the iterator.
The 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 call into_iter
instead of iter
. Similarly, if we want to iterate over mutable references, we can call iter_mut
instead of iter
.
All iterators implement a trait named Iterator
that is defined in the standard library. The definition of the trait looks like this:
// The `Iterator` trait only requires a method to be defined for the `next` element.
impl Iterator for Fibonacci {
// `Item` type is used in the return type of the `next` method
type Item = u32;
// Here, we define the sequence using `.curr` and `.next`.
// The return type is `Option<T>`:
// * When the `Iterator` is finished, `None` is returned.
// * Otherwise, the next value is wrapped in `Some` and returned.
// We use Self::Item in the return type, so we can change
// the type without having to update the function signatures.
fn next(&mut self) -> Option<Self::Item> {
let current = self.curr;
self.curr = self.next;
self.next = current + self.next;
// Since there's no endpoint to a Fibonacci sequence, the `Iterator`
// will never return `None`, and `Some` is always returned.
Some(current)
}
}
// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
Fibonacci { curr: 0, next: 1 }
}
Copy, Move, and Clone
Simple types that implement the Copy
trait are copied by value, meaning both the original and the copy can be used independently. Simple types often stored in the stack memory due to their fixed size and simplicity.
-
All the integer types, such as
i8
,i16
,i32
,i64
,i128
,u8
,u16
,u32
,u64
,u128
. -
The floating point types:
f32
andf64
. -
The Boolean type:
bool
, which can be eithertrue
orfalse
. -
The character type:
char
, representing a single Unicode scalar value. -
Tuples, but only if they contain types that are also
Copy
. For example,(i32, f64)
isCopy
, but(i32, String)
is not becauseString
does not implementCopy
. -
Arrays with a fixed size that contain types that are
Copy
. For example,[i32; 5]
isCopy
, but[String; 5]
is not becauseString
does not implementCopy
. -
Pointers, such as raw pointers (
*const T
,*mut T
) and function pointers (fn
), but not references (&T
,&mut T
) or smart pointers likeBox<T>
. It's important to note that if a type implements theDrop
trait, it cannot implement theCopy
trait. This is because theDrop
trait requires custom logic to be executed when a value goes out of scope, which would conflict with the unrestricted bitwise copying behavior ofCopy
types.
let num = 5;
let copy_of_num = num; // `num` implements Copy, so it's copied, not moved
For types that do not implement Copy
, such as String
, assignment moves the value. After moving, the original variable cannot be used.
let text = String::from("Hello");
let moved_text = text; // `text` is moved to `moved_text`
// println!("{}", text); // This would cause an error because `text` is no longer valid
To explicitly clone the data, creating a separate instance that does not affect the original, use the clone
method.
let text = String::from("Hello");
let cloned_text = text.clone(); // `text` is cloned, both `text` and `cloned_text` are valid
Error Handling
Throw unrecoverable error
panic!("Critical error! Exiting!");
Option enum
fn find_user_id(username: &str) -> Option<u32> {
if user_db.exists(username) {
return Some(user_db.get_id(username))
}
None
}
Result enum
fn fetch_user(user_id: u32) -> Result<User, Error> {
if is_user_logged_in(user_id) {
return Ok(get_user_details(user_id))
}
Err(Error { message: "User not logged in" })
}
? operator
The question mark operator (?
) unwraps valid values or returns errornous values, propagating them to the calling function. It is a unary postfix operator that can only be applied to the types Result<T, E>
and Option<T>
.
fn calculate_salary(database: Database, user_id: i32) -> Option<u32> {
Some(database.get_employee(user_id)?.get_position()?.salary)
}
fn establish_connection(database: Database) -> Result<Connection, Error> {
let connection = database.find_instance()?.connect()?;
Ok(connection)
}
Threads
Rust provides a mechanism for spawning native OS threads via the spawn
function, the argument of this function is a moving closure. Rust ensures safety and easy management of threads, adhering to its principles of memory safety and concurrency.
Creating a Thread
You can create a new thread by calling std::thread::spawn
and passing a closure containing the code you wish to run in the new thread.
use std::thread;
const THREADS_COUNT: u32 = 10;
let mut items = vec![];
for i in 0..THREADS_COUNT {
// Spin up another thread
item.push(thread::spawn(move || {
println!("this is thread number {}", i);
}));
}
for i in item {
// Wait for the thread to finish. Returns a result.
let _ = i.join();
}
Sharing Data Between Threads
Rust's ownership rules extend to threads, ensuring safe data sharing. To share data between threads, you can use atomic types, mutexes, or channels.
Using Mutex for Safe Data Access
Mutex means mutual exclusion
, which means "only one at a time". Mutex
is safe, because it only lets one process change data at a time. To do this, it uses .lock()
use std::sync::{Arc,Mutex};
use std::thread;
use std::time::Duration;
struct JobStatus {
jobs_completed: u32,
}
fn main() {
// using an Arc to share memory among threads, and the data inside
// the Arc is protected with a mutex.
let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 }));
let mut handles = vec![];
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));
let mut jobs_count = status_shared.lock().unwrap();
jobs_count.jobs_completed += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
println!("jobs completed {}", status.lock().unwrap().jobs_completed);
}
}
// Output:
jobs completed 5
jobs completed 5
jobs completed 5
jobs completed 7
jobs completed 7
jobs completed 7
jobs completed 7
jobs completed 9
jobs completed 9
jobs completed 10
Using Channels for Communication
Rust provides asynchronous channels
for communication between threads. Channels allow a unidirectional flow of information between two end-points: the Sender
and the Receiver
.
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
for i in 0..10 {
let tx = tx.clone();
thread::spawn(move || {
let message = format!("Message {}", i);
tx.send(message).unwrap();
});
}
for _ in 0..10 {
let received = rx.recv().unwrap();
println!("Received: {}", received);
}
Combinators
#map
let some_name = Some("Rusty".to_owned());
let name_length = some_name.map(|name| name.len());
let name_result: Result<String, Error> = Ok("Alex".to_owned());
let person_result: Result<Person, Error> = name_result.map(|name| Person { name });
#and_then
let numbers = Some(vec![1, 2, 3]);
let first_num = numbers.and_then(|nums| nums.into_iter().next());
let parse_result: Result<&'static str, _> = Ok("10");
let int_result = parse_result.and_then(|num_str| num_str.parse::<i32>());
Generics, Traits, and Lifetimes
Generics used to create definitions for items like function signatures or structs, which we can then use with many different concrete data types.
struct Pair<T, V> {
first: T,
second: V,
}
impl<T, V> Pair<T, V> {
fn mix<V2, T2>(self, other: Pair<V2, T2>) -> Pair<T, T2> {
Pair {
first: self.first,
second: other.second,
}
}
}
Traits
A trait defines functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior (implements certain Trait)
trait Voice {
fn create(name: &'static str) -> Self; // required implementation
fn speak(&self) -> &'static str { "Oho" } // default implementation
}
struct Cat { name: &'static str }
impl Cat {
fn meow() { // ... }
}
impl Voice for Cat {
fn create(name: &'static str) -> Cat {
Cat { name }
}
fn speak(&self) -> &'static str {
"meow"
}
}
Trait bounds
The impl Trait
syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound; it looks like this:
pub fn post<T: Summary>(item: &T) {
println!("Musk broke his ketamine dose again {}", item.summarize());
}
functions with multiple generic type parameters can contain lots of trait bound information between the function’s name and its parameter list
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {...}
// Alternative using "where" keyword:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
impl trait
We can also use the impl Trait
syntax in the return position to return a value of some type that implements a trait, as shown here:
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
By using impl Summary
for the return type, we specify that the returns_summarizable
function returns some type that implements the Summary
trait without naming the concrete type. In this case, returns_summarizable
returns a Tweet
, but the code calling this function doesn’t need to know that.
The ability to specify a return type only by the trait it implements is especially useful in the context of closures and iterators
Trait objects
Dynamic dispatch through a mechanism called ‘trait objects’ - way to achieve polymorphism in Rust.
A trait object can be obtained from a pointer to a concrete type that implements the trait by casting it (e.g. &x as &Foo
) or coercing it (e.g. using &x
as an argument to a function that takes &Foo
).
These trait object coercions and casts also work for pointers like &mut T
to &mut Foo
and Box<T>
to Box<Foo>
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
struct Button {
width: u32,
height: u32,
label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
// --------
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
// -------
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
Supertraits
When you define a trait, you can specify that it requires the functionality of another trait using the supertrait syntax. This means that in order for a type to implement the dependent trait, it must also implement the supertrait(s).
use std::fmt;
// `Log` is a trait that has a supertrait `fmt::Display` from the Rust standard library. The `fmt::Display` trait requires the implementing type to define how it will be formatted when displayed as a string
trait Log: fmt::Display {
fn log(&self) {
let output = self.to_string();
println!("Logging: {}", output);
}
}
Operator overloading
Rust allows for a limited form of operator overloading. There are certain operators that are able to be overloaded. To support a particular operator between types, there’s a specific trait that you can implement, which then overloads the operator.
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Vector {
x: i32,
y: i32,
}
impl Add for Vector {
type Output = Vector;
fn add(self, other: Vector) -> Vector {
Vector {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let v1 = Vector { x: 1, y: 0 };
let v2 = Vector { x: 2, y: 3 };
let v3 = v1 + v2;
println!("{:?}", v3);
}
Lifetimes
Lifetimes ensure that references are valid as long as we need them to be, main goal of lifetimes is to prevent dangling references.
Most of the time, lifetimes are implicit and inferred, just like most of the time, types are inferred. We must only annotate types when multiple types are possible. In a similar way, 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
Lifetimes in function signatures
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Lifetimes in Struct Definitions
This example demonstrates how to associate the lifetime of the fields within the Profile
struct with the lifetime 'a
, ensuring that the references to name
and email
are valid for the duration of the Profile
instance's existence.
struct Profile<'a> {
email: &'a str,
name: &'a str,
}
let user_name = "Alex";
let user_email = "alex@example.com";
let user_profile = Profile { name: user_name, email: user_email };
Static Lifetimes
A 'static
lifetime is the longest possible lifetime, and it can live for the entire duration of a program. It's typically used for string literals.
let static_str: &'static str = "Permanent text";
static_str
is a reference to a string literal with a 'static
lifetime, meaning it's guaranteed to be valid for the entire program runtime.
Functions, Function Pointers, and Closures
Functions in Rust can be defined as associated functions of structs or as standalone functions. Function pointers and closures provide flexibility for dynamic function usage and functional programming paradigms.
Associated functions and methods
Structs can have associated functions and methods that act on instances of the struct.
struct Point { x: i32, y: i32 }
impl Point {
fn create(x: i32, y: i32) -> Point {
Point { x, y }
}
fn get_x(&self) -> i32 { self.x }
}
let point = Point::create(5, 10);
point.get_x();
Function Pointers
Function pointers allow passing functions as arguments to other functions.
fn compute(val: i32, operation: fn(i32) -> i32) -> i32 {
operation(val)
}
fn square(num: i32) -> i32 {
num * num
}
let squared_value = compute(5, square);
Creating Closures
Closures are anonymous functions that can capture their environment.
let increment = |num: i32| -> i32 { num + 1 };
let incremented_value = increment(5);
Returning Closures
Closures can be returned from functions. The impl
keyword is used to return a closure.
fn add(amount: i32) -> impl Fn(i32) -> i32 {
move |num: i32| num + amount
}
let add_five = add(5);
let result = add_five(10); // result == 15
fn add_or_subtract(a: i32) -> Box<dyn Fn(i32) -> i32> {
if a > 10 {
Box::new(move |b| b + a)
} else {
Box::new(move |b| b - a)
}
}
let var1 = add_or_subtract(10); // -10
let var2 = var1(2); // -10 +2 = 8
Closure Traits
-
FnOnce
: consumes the variables it captures from enclosing scope. -
FnMut
: mutably borrows values from its enclosing scope. -
Fn
: immutably borrows values from its enclosing scope.
fn apply<F>(value: i32, mut func: F) -> i32
where
F: FnMut(i32) -> i32,
{
func(value)
}
let double = |x: i32| x * 2;
let doubled_value = apply(5, double);
Store Closure in Struct
You can store closures in structs by specifying the closure trait in the struct definition.
struct Calculator<T>
where
T: Fn(i32) -> i32,
{
calculation: T,
}
let add_one = |num: i32| num + 1;
let calculator = Calculator { calculation: add_one };
let calculated_value = (calculator.calculation)(5);
Function that Accepts Closure or Function Pointer
Functions can accept closures or function pointers as parameters, allowing for flexible argument types.
fn execute_twice<F>(mut func: F, arg: i32) -> i32
where
F: FnMut(i32) -> i32,
{
func(arg) + func(arg)
}
let add_two = |x: i32| x + 2;
let executed_value = execute_twice(add_two, 10);
Pointers
References
let value = 10;
let reference_to_value = &value; // Immutable reference
let mutable_reference_to_value = &mut value; // Mutable reference
Raw Pointers
Rust has a number of different smart pointer types in its standard library, but there are two types that are extra-special. Much of Rust’s safety comes from compile-time checks, but raw pointers don’t have such guarantees, and are unsafe to use.
*const T
and *mut T
are called ‘raw pointers’ in Rust. Sometimes, when writing certain kinds of libraries, you’ll need to get around Rust’s safety guarantees for some reason. In this case, you can use raw pointers to implement your library, while exposing a safe interface for your users. For example, *
pointers are allowed to alias, allowing them to be used to write shared-ownership types, and even thread-safe shared memory types (the Rc<T>
and Arc<T>
types are both implemented entirely in Rust).
Here are some things to remember about raw pointers that are different than other pointer types. They:
- are not guaranteed to point to valid memory and are not even guaranteed to be non-NULL (unlike both
Box
and&
); - do not have any automatic clean-up, unlike
Box
, and so require manual resource management; - are plain-old-data, that is, they don't move ownership, again unlike
Box
, hence the Rust compiler cannot protect against bugs like use-after-free; - lack any form of lifetimes, unlike
&
, and so the compiler cannot reason about dangling pointers; and - have no guarantees about aliasing or mutability other than mutation not being allowed directly through a
*const T
.
Raw pointers are useful for FFI: Rust’s *const T
and *mut T
are similar to C’s const T*
and T*
, respectively. Raw pointers (*const T
and *mut T
) can point to any memory location.
#![allow(unused_variables)]
fn main() {
let x = 5;
let raw = &x as *const i32;
let mut y = 10;
let raw_mut = &mut y as *mut i32;
}
// When you dereference a raw pointer, you’re taking responsibility that it’s not pointing somewhere that would be incorrect. As such, you need `unsafe`:
#![allow(unused_variables)]
fn main() {
let x = 5;
let raw = &x as *const i32;
let points_at = unsafe { *raw };
println!("raw points at {}", points_at);
}
Smart Pointers
- Smart pointers are data structures that not only act like a pointer but also have additional metadata and capabilities.
- Smart pointers own the data they point to and are usually implemented using structs..
- Smart pointers implement the
Deref
andDrop
traits. TheDeref
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. TheDrop
trait allows you to customize the code that’s run when an instance of the smart pointer goes out of scope.
Box<T>
Use Box<T>
to allocate values on the heap.
let boxed_data = Box::new(5);
Rc<T> - multiple ownership with reference counting
let shared_data = Rc::new(5);
let clone_of_shared_data = Rc::clone(&shared_data);
Ref<T>, RefMut<T>, and RefCell<T> - enforce borrowing rules at runtime instead of compile time.
let a = 5;
let x1 = RefCell::new(a);
let x2 = x1.borrow(); // Ref - immutable borrow
let x3 = x1.borrow_mut(); // RefMut - mutable borrow
let x4 = r1.borrow_mut(); // RefMut - second mutable borrow
Multiple owners of mutable data
let x = Rc::new(RefCell::new(5));
Packages, Crates, and Modules
Rust's module system allows you to organize your code into packages, crates, and modules for better readability and reusability.
- Packages is cargo feature that lets you build, test, and share crates.
- Crates - A tree of modules that produces a library or executable.
- Modules and use - Let you control the organization, scope, and privacy of paths.
- Paths - A way of naming an item, such as a struct, function, or module.
Creating a Package
$ cargo new my_project --bin # For a binary
$ crate cargo new my_library --lib # For a library crate
Defining and using modules
mod sausage_factory {
pub use self::meats::PORK_SAUSAGE as sausage;
pub use self::meats::VEGGIE_SAUSAGE as veggie_sausage;
mod meats {
pub const PORK_SAUSAGE: &'static str = "Juicy pork sausage";
pub fn make_sausage() {
get_secret_recipe();
println!("sausage made with {} and secret ingredient!", PORK_SAUSAGE);
}
}
// private function so nobody outside of this module can use it
fn get_secret_recipe() -> String {
String::from("Ginger")
}
}
mod veggies {
pub const VEGGIE_SAUSAGE: &'static str = "Delicious veggie sausage";
...
}
}
fn main() {
sausage_factory::meats::make_sausage();
println!("Today we serve {} and {}", sausage_factory::sausage, sausage_factory::veggie_sausage)
}
Import module and use with custom name:
// Import module and use with custom name:
use std::fmt::Result;
use std::io::Result as IoResult;
// Re-exporting with pub use
mod outer_module {
pub mod inner_module {
pub fn inner_public_function() {}
}
}
pub use crate::outer_module::inner_module;
Top comments (0)