Writing Automated Tests
Writing automated tests to ensure your code behaves as expected.
How to Write Tests:
- Tests are functions annotated with the
#[test]
attribute. - Example:
rust #[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }
The Anatomy of a Test Function:
- Test functions are written inside a module annotated with
#[cfg(test)]
to ensure they are only compiled when running tests. - The
#[test]
attribute marks a function as a test.
Checking Results with the assert!
Macro:
- The
assert!
macro ensures a condition is true. If it’s not, the test will fail. - Example:
#[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7 }; let smaller = Rectangle { width: 5, height: 1 }; assert!(larger.can_hold(&smaller)); }
Testing Equality with assert_eq!
and assert_ne!
Macros:
assert_eq!
checks if two values are equal.assert_ne!
checks if two values are not equal.- Example:
rust #[test] fn it_adds_two() { assert_eq!(add_two(2), 4); }
Adding Custom Failure Messages:
- Custom messages can be added to assertions for more informative test results.
- Example:
rust #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol"), "Greeting did not contain name, value was `{}`", result); }
Checking for Panics with should_panic
:
- Use the
#[should_panic]
attribute to indicate that a test should panic. - Example:
rust #[test] #[should_panic(expected = "Guess value must be less than or equal to 100")] fn greater_than_100() { Guess::new(200); }
Using Result<T, E>
in Tests:
- Test functions can return
Result<T, E>
for more flexibility. - Example:
rust #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } }
Controlling How Tests Are Run:
- Tests can be run in parallel or consecutively.
- Example of running tests in parallel:
sh $ cargo test
Running a Subset of Tests by Name:
- You can run a specific test or a set of tests matching a name pattern.
- Example:
sh $ cargo test larger_can_hold_smaller
Ignoring Some Tests Unless Specifically Requested:
- Tests can be ignored by default and only run when explicitly requested using the
#[ignore]
attribute. - Example:
#[test] #[ignore] fn expensive_test() { // code that takes a long time to run }
Test Organization:
- Tests can be organized into modules and submodules.
- Integration tests are placed in the
tests
directory and are separate from the unit tests in the main library or binary. - Example of a basic integration test setup:
// In src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2 } // In tests/integration_test.rs extern crate my_crate; #[test] fn it_adds_two() { assert_eq!(my_crate::add_two(2), 4); }
Functional Language Features in Rust
Functional programming features in Rust, such as closures, iterators, and how they contribute to writing concise and expressive code. Here are the key points:
Closures: Anonymous Functions that Capture Their Environment:
- Closures are similar to functions but can capture variables from their enclosing scope.
- Example of a basic closure:
rust let add_one = |x: i32| -> i32 { x + 1 }; println!("{}", add_one(5));
- Closures can capture values from their environment in three ways: by borrowing, by mutable borrowing, and by taking ownership.
Capturing the Environment with Closures:
- Closures automatically capture values from their environment.
- Example:
rust let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y));
Type Inference and Annotation:
- Rust can infer the types of parameters and return values for closures in most cases, allowing for concise syntax.
- Example:
rust let add_one = |x| x + 1;
Storing Closures:
- Closures can be stored in variables or passed to functions.
- Example:
rust let example_closure = |x| x; let s = example_closure(String::from("hello"));
Using Closures That Capture Their Environment:
- Example with capturing the environment:
rust let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x; // x is moved into the closure
Iterators: Processing a Series of Elements:
- Iterators allow you to perform operations on a sequence of elements.
- Example of creating an iterator:
rust let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("{}", val); }
The Iterator
Trait and the next
Method:
- The
Iterator
trait defines thenext
method, which returns an option containing the next element. - Example:
rust 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);
Methods that Produce Other Iterators:
- Iterators have methods like
map
,filter
, andcollect
to create other iterators. - Example of using
map
:rust let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);
Using Closures that Capture Their Environment with Iterators:
- Combining closures and iterators can lead to powerful and concise code.
- Example of using
filter
andmap
:rust let v1: Vec<i32> = vec![1, 2, 3, 4, 5]; let v2: Vec<_> = v1.into_iter().filter(|&x| x % 2 == 0).map(|x| x * 2).collect(); assert_eq!(v2, vec![4, 8]);
Creating Custom Iterators with the Iterator
Trait:
- Implement the
Iterator
trait to create custom iterators. - Example:
struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2));
Performance of Iterators:
- Rust’s iterators are zero-cost abstractions, meaning they compile down to the same code as if you wrote the loop by hand.
- This ensures that using iterators does not incur a performance penalty.
Functional Language Features in Rust
Explores functional programming features in Rust, such as closures, iterators, and how they contribute to writing concise and expressive code.
Closures: Anonymous Functions that Capture Their Environment:
- Closures are similar to functions but can capture variables from their enclosing scope.
- Example of a basic closure:
rust let add_one = |x: i32| -> i32 { x + 1 }; println!("{}", add_one(5));
- Closures can capture values from their environment in three ways: by borrowing, by mutable borrowing, and by taking ownership.
Capturing the Environment with Closures:
- Closures automatically capture values from their environment.
- Example:
rust let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y));
Type Inference and Annotation:
- Rust can infer the types of parameters and return values for closures in most cases, allowing for concise syntax.
- Example:
rust let add_one = |x| x + 1;
Storing Closures:
- Closures can be stored in variables or passed to functions.
- Example:
rust let example_closure = |x| x; let s = example_closure(String::from("hello"));
Using Closures That Capture Their Environment:
- Example with capturing the environment:
rust let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x; // x is moved into the closure
Iterators: Processing a Series of Elements:
- Iterators allow you to perform operations on a sequence of elements.
- Example of creating an iterator:
rust let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("{}", val); }
The Iterator
Trait and the next
Method:
- The
Iterator
trait defines thenext
method, which returns an option containing the next element. - Example:
rust 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);
Methods that Produce Other Iterators:
- Iterators have methods like
map
,filter
, andcollect
to create other iterators. - Example of using
map
:rust let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);
Using Closures that Capture Their Environment with Iterators:
- Combining closures and iterators can lead to powerful and concise code.
- Example of using
filter
andmap
:rust let v1: Vec<i32> = vec![1, 2, 3, 4, 5]; let v2: Vec<_> = v1.into_iter().filter(|&x| x % 2 == 0).map(|x| x * 2).collect(); assert_eq!(v2, vec![4, 8]);
Creating Custom Iterators with the Iterator
Trait:
- Implement the
Iterator
trait to create custom iterators. - Example:
struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2));
Performance of Iterators:
- Rust’s iterators are zero-cost abstractions, meaning they compile down to the same code as if you wrote the loop by hand.
- This ensures that using iterators does not incur a performance penalty.
Summary of Chapter 15: Smart Pointers
Chapter 15 of “The Rust Programming Language” covers smart pointers, which are data structures that act like a pointer but have additional metadata and capabilities. Here are the key points:
- Smart Pointers Overview:
- Smart pointers provide more functionality than regular references, such as automatic memory management and additional operations.
- Common smart pointers in Rust include
Box<T>
,Rc<T>
, andRefCell<T>
.
- Using
Box<T>
to Point to Data on the Heap:
Box<T>
allows you to store data on the heap instead of the stack.- Example:
rust let b = Box::new(5); println!("b = {}", b);
- Enabling Recursive Types with
Box<T>
:
Box<T>
is used to create recursive types where the size of the type cannot be known at compile time.- Example of a recursive type:
rust enum List { Cons(i32, Box<List>), Nil, }
- Treating Smart Pointers Like Regular References with the
Deref
Trait:
- The
Deref
trait allows instances of smart pointers to behave like references. - Example:
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } }
- Running Code on Cleanup with the
Drop
Trait:
- The
Drop
trait allows you to specify code that runs when a smart pointer goes out of scope. - Example:
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } let c = CustomSmartPointer { data: String::from("my stuff") };
- Reference Counting with
Rc<T>
:
Rc<T>
is a reference-counting smart pointer used when multiple parts of the program need to own the same data.- Example:
use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); println!("count after creating b = {}", Rc::strong_count(&a));
- RefCell and the Interior Mutability Pattern:
RefCell<T>
allows for mutable borrowing at runtime, even when theRefCell
itself is immutable.- Example:
use std::cell::RefCell; let x = RefCell::new(5); *x.borrow_mut() += 1; println!("{}", x.borrow());
- Combining
Rc<T>
andRefCell<T>
to Have Multiple Owners of Mutable Data:
- Combining
Rc<T>
andRefCell<T>
allows multiple owners to mutate the same data. - Example:
use std::rc::Rc; use std::cell::RefCell; let value = Rc::new(RefCell::new(5)); let a = Rc::clone(&value); let b = Rc::clone(&value); *a.borrow_mut() += 1; println!("{}", b.borrow());
- Creating a Tree Data Structure:
- Example of a tree data structure using
Rc<T>
andRefCell<T>
:use std::rc::Rc; use std::cell::RefCell; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { List::Cons(_, item) => Some(item), List::Nil => None, } } } let a = Rc::new(List::Cons(5, RefCell::new(Rc::new(List::Nil)))); let b = Rc::new(List::Cons(10, RefCell::new(Rc::clone(&a)))); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); }
Smart Pointers
Smart Pointers Overview:
- Smart pointers provide more functionality than regular references, such as automatic memory management and additional operations.
- Common smart pointers in Rust include
Box<T>
,Rc<T>
, andRefCell<T>
.
Using Box<T>
to Point to Data on the Heap:
Box<T>
allows you to store data on the heap instead of the stack.- Example:
rust let b = Box::new(5); println!("b = {}", b);
Enabling Recursive Types with Box<T>
:
Box<T>
is used to create recursive types where the size of the type cannot be known at compile time.- Example of a recursive type:
rust enum List { Cons(i32, Box<List>), Nil, }
Treating Smart Pointers Like Regular References with the Deref
Trait:
- The
Deref
trait allows instances of smart pointers to behave like references. - Example:
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } }
Running Code on Cleanup with the Drop
Trait:
- The
Drop
trait allows you to specify code that runs when a smart pointer goes out of scope. - Example:
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } let c = CustomSmartPointer { data: String::from("my stuff") };
Reference Counting with Rc<T>
:
Rc<T>
is a reference-counting smart pointer used when multiple parts of the program need to own the same data.- Example:
use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); println!("count after creating b = {}", Rc::strong_count(&a));
RefCell and the Interior Mutability Pattern:
RefCell<T>
allows for mutable borrowing at runtime, even when theRefCell
itself is immutable.- Example:
use std::cell::RefCell; let x = RefCell::new(5); *x.borrow_mut() += 1; println!("{}", x.borrow());
Combining Rc<T>
and RefCell<T>
to Have Multiple Owners of Mutable Data:
- Combining
Rc<T>
andRefCell<T>
allows multiple owners to mutate the same data. - Example:
use std::rc::Rc; use std::cell::RefCell; let value = Rc::new(RefCell::new(5)); let a = Rc::clone(&value); let b = Rc::clone(&value); *a.borrow_mut() += 1; println!("{}", b.borrow());
Creating a Tree Data Structure:
- Example of a tree data structure using
Rc<T>
andRefCell<T>
:use std::rc::Rc; use std::cell::RefCell; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { List::Cons(_, item) => Some(item), List::Nil => None, } } } let a = Rc::new(List::Cons(5, RefCell::new(Rc::new(List::Nil)))); let b = Rc::new(List::Cons(10, RefCell::new(Rc::clone(&a)))); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); }
Fearless Concurrency
Using Threads to Run Code Simultaneously:
- Threads allow multiple parts of a program to execute simultaneously.
- Example of creating a thread:
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(); }
Using move
Closures with Threads:
- The
move
keyword allows closures to take ownership of values from the environment. - Example:
use std::thread; let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); handle.join().unwrap();
Message Passing to Transfer Data Between Threads:
- Channels provide a way to transfer data safely between threads.
- Example of creating and using a channel:
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {}", received); }
Channels and Ownership:
- Sending values through channels transfers ownership to the receiver.
- Example of sending multiple messages:
use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_millis(1)); } }); for received in rx { println!("Got: {}", received); } }
Shared-State Concurrency:
- Rust ensures safe shared-state concurrency through the ownership system.
- Using
Mutex<T>
to manage shared state:use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Atomic Reference Counting with Arc<T>
:
Arc<T>
(Atomic Reference Counted) is used to safely share ownership between threads.- Example:
use std::sync::Arc; use std::thread; let numbers = Arc::new(vec![1, 2, 3]); for _ in 0..10 { let numbers = Arc::clone(&numbers); thread::spawn(move || { println!("{:?}", numbers); }); }
Combining Arc<T>
and Mutex<T>
:
- Combining
Arc<T>
andMutex<T>
allows multiple threads to mutate shared data safely. - Example:
use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap());
Object-Oriented Programming Features
Characteristics of Object-Oriented Languages:
- Object-oriented programming is typically defined by three characteristics: encapsulation, inheritance, and polymorphism.
- Rust provides encapsulation and polymorphism but does not have inheritance in the traditional sense.
Encapsulation with Structs and impl
:
- Encapsulation involves grouping related data and behavior together, which can be achieved using structs and
impl
blocks. - Example:
pub struct AveragedCollection { list: Vec<i32>, average: f64, } impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option<i32> { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) } None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; } }
Inheritance as a Type System and Code Sharing Mechanism:
- Rust does not have inheritance, but it achieves code reuse through composition and traits.
- Traits are used to define shared behavior.
Polymorphism with Traits:
- Polymorphism is the ability to use different types interchangeably.
- Rust achieves polymorphism through trait objects.
- Example:
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(); } } } pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // code to draw a button } }
Trait Objects for Dynamic Dispatch:
- Trait objects allow for dynamic dispatch, enabling polymorphic behavior at runtime.
- Example:
rust let screen = Screen { components: vec![ Box::new(Button { width: 50, height: 10, label: String::from("OK"), }), ], };
Object Safety:
- For a trait to be used as a trait object, it must be object-safe.
- A trait is object-safe if all the methods defined in the trait have the following properties:
- The return type isn’t
Self
. - There are no generic type parameters.
- The return type isn’t
Designing with Traits Instead of Inheritance:
- Instead of inheritance, Rust encourages composition and using traits to define shared behavior.
- Example:
pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: 'a + Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger, { pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send("Error: You are over your quota!"); } else if percentage_of_max >= 0.9 { self.messenger.send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 0.75 { self.messenger.send("Warning: You've used up over 75% of your quota!"); } } }
Patterns and Matching
All the Places Patterns Can Be Used:
- Patterns are used in
match
arms,if let
expressions,while let
expressions,for
loops,let
statements, and function parameters.
Refutability: Whether a Pattern Might Fail to Match:
- Patterns are either refutable or irrefutable.
- Refutable patterns can fail to match (e.g.,
Some(x)
), while irrefutable patterns cannot fail to match (e.g.,x
).
Pattern Syntax:
- Patterns can match literals, variables, wildcards, and complex structures.
- Example of matching literals and variables:
rust let x = 1; match x { 1 => println!("One"), _ => println!("Not one"), }
Destructuring to Break Apart Values:
- Destructuring structs, enums, and tuples to extract inner values.
- Example of destructuring a tuple:
rust let (x, y, z) = (1, 2, 3); println!("x: {}, y: {}, z: {}", x, y, z);
Destructuring Structs:
- Example:
rust struct Point { x: i32, y: i32, } let p = Point { x: 0, y: 7 }; let Point { x, y } = p; println!("x: {}, y: {}", x, y);
Destructuring Enums:
- Example:
rust enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y), Message::Write(text) => println!("Text: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to red: {}, green: {}, blue: {}", r, g, b), }
Destructuring Nested Structures:
- Example:
rust let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
Ignoring Values in a Pattern:
- Using
_
to ignore values:rust fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {}", y); }
Ignoring Parts of a Value with a Nested _
:
- Example:
rust let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {:?}", setting_value);
Ignoring an Unused Variable by Starting Its Name with _
:
- Example:
let _x = 5; let y = 10; println!("y: {}", y);
Using _
to Ignore the Remaining Parts of a Value:
- Example:
let s = Some(String::from("Hello!")); if let Some(_) = s { println!("Found a string"); } println!("{:?}", s);
Ignoring Values with ..
:
- Using
..
to ignore parts of a value:
struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {}", x), }
Extra Conditionals with Match Guards:
- Adding extra conditions in match arms:
let num = Some(4); match num { Some(x) if x < 5 => println!("less than five: {}", x), Some(x) => println!("{}", x), None => (), }
@ Bindings:
- Using
@
to bind a value to a variable while also testing it:rust enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7 } => println!("Found an id in range: {}", id_variable), Message::Hello { id: 10..=12 } => println!("Found an id in another range"), Message::Hello { id } => println!("Found some other id: {}", id), }
Advanced Features
Unsafe Rust:
- Unsafe Rust allows you to bypass some of Rust’s safety guarantees.
- Four main actions you can perform in unsafe code:
- Dereference a raw pointer.
- Call an unsafe function or method.
- Access or modify a mutable static variable.
- Implement an unsafe trait.
- Example:
let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); }
Advanced Traits:
- Associated types, default type parameters, and fully qualified syntax.
- Example of associated types:
rust pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
- Using default type parameters for traits:
rust trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
Advanced Types:
- Newtype pattern and type aliases.
- Example of newtype pattern:
struct Kilometers(i32); let x = Kilometers(10); let y = Kilometers(20);
- Example of type alias:
rust type Thunk = Box<dyn Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi"));
Advanced Functions and Closures:
- Function pointers and returning closures.
- Example of function pointers:
fn add_one(x: i32) -> i32 { x + 1 } let f: fn(i32) -> i32 = add_one; println!("The result is: {}", f(5));
- Example of returning closures:
rust fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) }
Macros:
- Macros for metaprogramming, such as
macro_rules!
and procedural macros. - Example of a declarative macro:
macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } let v = vec![1, 2, 3];
- Procedural macros for custom derive, attribute-like macros, and function-like macros.