Table of Contents
Traits
- Definition: Traits are a way to define shared behavior in Rust. They define a set of methods that a type must implement in order to fulfill the contract of the trait.
- Purpose: Traits are used for abstracting over types, allowing you to write generic code that can work with multiple types as long as they implement the required behavior specified by the trait.
- Syntax: Traits are declared using the
trait
keyword, followed by the trait’s name and a set of method signatures. Types can then implement traits using theimpl
keyword.
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
}
Closures
- Definition: Closures are anonymous functions that can capture variables from their surrounding environment. They are similar to functions but are defined inline using a concise syntax.
- Purpose: Closures are useful for writing code that requires a small, one-off function without the need to define a separate named function. They are often used for passing behavior as arguments to higher-order functions.
- Syntax: Closures are written using a pipe (
|
) syntax to specify the parameters, followed by the body of the closure. They can capture variables from their enclosing scope using a move or borrow semantics.
let add = |x, y| x + y;
println!("The sum is: {}", add(3, 5));
where clause
In Rust, the where
clause is used in function declarations, trait declarations, and various other contexts to specify additional requirements or bounds on the generic parameters or associated types.
When used in a function declaration, the where
clause typically follows the list of generic parameters and is used to specify trait bounds or additional constraints on those parameters.
fn foo<T>(x: T) -> usize
where
T: std::fmt::Debug,
{
println!("{:?}", x); // We can use Debug trait here because of the where clause
0
}
T
is a generic parameter of the functionfoo
.- The
where
clause specifies thatT
must implement theDebug
trait, allowing us to use theprintln!
macro inside the function body.
Here are some common uses of the where
clause in function declarations:
- Trait Bounds: Specify traits that the generic type parameter must implement.
- Associated Type Bounds: Specify constraints on associated types within trait bounds.
- Multiple Bounds: Specify multiple trait bounds or bounds on multiple generic parameters.
- Complex Constraints: Specify more complex constraints that involve multiple types or traits.
“map” with Range
The map
method in Rust is used to create a new iterator where each element is the result of applying a given function to the corresponding element of the original iterator. When used with a range, it allows you to transform each number in the range according to the function you provide.
fn main() {
let squares: Vec<i32> = (1..5).map(|x| x * x).collect();
println!("{:?}", squares); // This will print [1, 4, 9, 16]
}
- Range Creation:
let range = 1..5;
- Mapping Function:
let squares = range.map(|x| x * x);
- The
map
method takes a closure|x| x * x
which squares each elementx
in the range.
- The
- Collecting Results:
let squares: Vec<i32> = squares.collect();
- The
collect
method is used to consume the iterator and collect the results into a vector.
- The
- This creates a range from 1 to 4.
core::array::from_fn
The core::array::from_fn
function in Rust is a utility that allows you to generate an array of fixed size by applying a closure to each index.
use core::array;
fn main() {
// Create an array of 10 elements where each element is its index squared
let squares: [usize; 10] = array::from_fn(|i| i * i);
println!("{:?}", squares); // Prints: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
}
Option<T>
Option<T>
is an enum in Rust that represents an optional value. It can be either:
Some(T)
: Indicates that there is a value of typeT
.None
: Indicates that there is no value.
The Option
type is used extensively in Rust to handle cases where a value might be absent
if let
In Rust, the if let
construct is used to match a pattern and execute code if the match is successful. It can be used to handle Option
and Result
types more ergonomically. Specifically, if let Some(next) = inputs_iter.peek()
checks if peek()
returns Some
and extracts the value into next
. If peek()
returns None
, the else
block is executed.
fn main() {
// Define a vector of vectors
let inputs = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
// Convert the vector into an iterator and make it peekable
let mut inputs_iter = inputs.into_iter().peekable();
// Loop through the iterator
while let Some(current) = inputs_iter.next() {
// Print the current element
println!("Current: {:?}", current);
// Peek at the next element
if let Some(next) = inputs_iter.peek() {
// If there is a next element, print it
println!("Next: {:?}", next);
} else {
// If there are no more elements, print a message
println!("No more elements.");
}
}
}
Ownership & borrowing
In Rust, “ownership” refers to the ownership system that Rust uses to manage memory safely without a garbage collector. Ownership is a set of rules that governs how memory is managed, ensuring that memory is freed when it is no longer needed.
When we say that a function or method takes ownership of data, it means that the function or method takes control over the data, and the original owner can no longer access the data. Conversely, when we say that a function or method borrows data, it means that the function or method can temporarily use the data, but the original owner retains control and can continue to use it once the borrowing ends.
ownership:
fn take_ownership(v: Vec<i32>) {
// `v` is now owned by this function
println!("{:?}", v);
}
fn main() {
let v = vec![1, 2, 3];
take_ownership(v);
// v is no longer accessible here because its ownership has been transferred
// println!("{:?}", v); // This would cause a compile error
}
borrowing
fn borrow(v: &Vec<i32>) {
// `v` is borrowed here, not owned
println!("{:?}", v);
}
fn main() {
let v = vec![1, 2, 3];
borrow(&v);
// v is still accessible here because its ownership was not transferred
println!("{:?}", v); // This is fine
}
Immutable borrowing
When you borrow a value immutably, you use &
. This means that the function can read the value but cannot modify it
mutable borrowing
When you borrow a value mutably, you use &mut
. This allows the function to modify the value