In this session, we will delve into the more detailed concepts of rust features explaining rules of ownership and borrowing and how to implement them. It will also include what are smart pointers, how to perform pattern matching and exception handling in rust.
2. Lack of etiquette and manners is a huge turn off.
KnolX Etiquettes
Punctuality
Join the session 5 minutes prior to the session start time. We start on
time and conclude on time!
Feedback
Make sure to submit a constructive feedback for all sessions as it is very
helpful for the presenter.
Silent Mode
Keep your mobile devices in silent mode, feel free to move out of session
in case you need to attend an urgent call.
Avoid Disturbance
Avoid unwanted chit chat during the session.
3. 1. Brief Overview
2. Pattern Matching in Rust
Types of Patterns
3. Error Handling
4. Smart Pointers
Tyes of Smart Pointers
5. Generics
Types of Generics
6. Traits
4.
5. Brief Overview
Rust is a modern systems programming language designed for safety, concurrency, and
performance.
Its key features include strong static typing, zero-cost abstractions, and a focus on memory safety
without sacrificing performance.
One of its most distinctive features is its ownership system, which ensures memory safety by
enforcing strict rules about how memory is accessed and manipulated.
Ownership rules prevent common pitfalls like null pointer dereferencing, dangling pointers, and
data races by enforcing a single owner for each piece of data and allowing controlled borrowing
and lending of references.
This approach enables efficient memory management and eliminates many common bugs at
compile time, making Rust a powerful choice for building reliable and efficient software systems.
6. Pattern matching is a fundamental concept in Rust for handling control flow based on the
structure of data.
Rust's match keyword allows for exhaustive and flexible pattern matching.
Pattern matching in Rust is a powerful feature that allows developers to destructure complex data
types and control flow based on the structure of those types. It is primarily achieved through the
match keyword, which resembles the switch statement found in other languages but offers more
flexibility and safety.
Syntax:
match expression {
pattern1 => code_block1,
pattern2 => code_block2,
// More patterns...
_ => default_code_block // Optional default case
}
Pattern Matching in Rust
7. expression: The value to match against.
Pattern: Describes the structure to match against.
=> Separates pattern from associated code block.
_ Wildcard pattern for unmatched cases.
, Separates different patterns and code blocks.
Example -
8. Matching Literals- Matches against specific constant values.
The following code gives some examples:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
This code prints one because the value in x is 1. This syntax is useful when you want your code
to take an action if it gets a particular concrete value.
Types of Patterns -
9. Multiple Patterns
In match expressions, you can match multiple patterns using the | syntax, which is the
pattern or operator. For example, in the following code we match the value of x against the match
arms, the first of which has an or option, meaning if the value of x matches either of the values in
that arm, that arm’s code will run:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
Matching Ranges of Values with ..=
The ..= syntax allows us to match to an inclusive range of values. In the following code, when a
pattern matches any of the values within the given range, that arm will execute:
10. let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
If x is 1, 2, 3, 4, or 5, the first arm will match. This syntax is more convenient for multiple match values than
using the | operator to express the same idea; if we were to use | we would have to specify 1 | 2 | 3 | 4 | 5.
Specifying a range is much shorter, especially if we want to match, say, any number between 1 and 1,000!
The compiler checks that the range isn’t empty at compile time, and because the only types for which Rust
can tell if a range is empty or not are char and numeric values, ranges are only allowed with numeric
or char values.
Here is an example using ranges of char values:
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
11. Destructuring to Break Apart Values
struct Point { x: i32, y: i32, }
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
Remember that a match expression stops checking arms
once it has found the first matching pattern, so even
though Point { x: 0, y: 0} is on the x axis and the y axis, this
code would only print On the x axis at 0.
Destructuring Structs -
enum Message {
Quit,
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data ."); }
Message::Write(text) => {
println!("Text message: {text}"); }
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {r}, green {g}, and blue {b}",)
}
}
}
Destructuring Enums -
12. Ignoring Values in a Pattern
You’ve seen that it’s sometimes useful to ignore values in a pattern, such as in the last arm of a match, to get a
catchall that doesn’t actually do anything but does account for all remaining possible values.
Ignoring an Entire Value with _
We’ve used the underscore as a wildcard pattern that will match any value but not bind to the value. This is
especially useful as the last arm in a match expression, but we can also use it in any pattern, including function
parameters.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
This code will completely ignore the value 3 passed as the first argument, and will print This code only uses the y
parameter: 4.
13. Extra Conditionals with Match Guards
A match guard is an additional if condition, specified after the pattern in a match arm, that must also match for
that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone
allows.
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {} is even", x),
Some(x) => println!("The number {} is odd", x), None => (),
}
Benefits of Pattern Matching -
Exhaustiveness: Encourages handling of all possible cases.
Safety: Ensures exhaustive matching, reducing runtime errors.
Expressiveness: Concise syntax for complex control flow.
14. Error Handling
Rust favors handling errors explicitly rather than through exceptions.
Exceptional cases are represented using Result and Option types.
For unrecoverable errors, Rust provides the panic! macro.
Sometimes, bad things happen in your code, and there’s nothing you can do about it. In these cases, Rust
has the panic! macro. There are two ways to cause a panic in practice: by taking an action that causes our
code to panic (such as accessing an array past the end) or by explicitly calling the panic! macro. In both
cases, we cause a panic in our program. By default, these panics will print a failure message, unwind, clean
up the stack, and quit.
By default, when a panic occurs, the program starts unwinding, which means Rust walks back up the stack
and cleans up the data from each function it encounters. However, this walking back and cleanup is a lot of
work. Rust, therefore, allows you to choose the alternative of immediately aborting, which ends the program
without cleaning up. Memory that the program was using will then need to be cleaned up by the operating
system.
15. Result Type
Most errors aren’t serious enough to require the
program to stop entirely. Sometimes, when a
function fails, it’ for a reason that you can easily
interpret and respond to. For example, if you try to
open a file and that operation fails because the file
doesn’t exist, you might want to create the file
instead of terminating the process.
enum Result<T, E> {
Ok(T),
Err(E),
}
T represents the type of the value that will be
returned in a success case within
the Ok variant, and E represents the type of the
error that will be returned in a failure case within
the Err variant.
Recoverable Errors with Result
fn divide(x: i32, y: i32) -> Result<i32, &'static str> {
if y == 0 {
return Err("Division by zero");
}
Ok(x / y)
}
fn main() {
let result = divide(10, 0);
match result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
}
Handling Results -
16. Option Type
Represents an optional value that may or may not exist.
enum Option<T> {
Some(T),
None,
}
Handling Options -
18. Unwinding vs. Aborting
Rust allows two modes of handling panics: unwinding and aborting.
Unwinding performs stack unwinding to clean up resources.
Aborting terminates the program without unwinding the stack.
Benefits of Error Handling -
Explicitness: Errors are handled explicitly using Result and Option.
Safety: Encourages handling of error cases, reducing runtime failures.
Predictability: No unexpected control flow due to exceptions.
19. Smart Pointers
Smart pointers are specialized data structures in Rust's standard library that resemble traditional pointers but
offer enhanced functionalities and safety features.
They provide automatic memory management, ensuring that memory is deallocated when the smart pointer
goes out of scope, thereby preventing memory leaks.
They encapsulate raw pointers and manage the memory allocation and deallocation process internally,
reducing the risk of memory leaks and dangling pointers.
Types of Smart Pointers :
o Box<T>
o Rc<T>
o Arc<T>
o Mutex<T> & RwLock<T>
20. Box <T>
o Box<T> is the simplest smart pointer in Rust. It allows you to allocate memory on the heap and store a
value there. When the Box goes out of scope, its destructor is called, and the memory is deallocated.
Use Cases:
o Storing data of unknown size at compile time.
o Creating recursive data structures.
o Moving data between different parts of your program.
Example :
Types of Smart Pointers
fn main() {
let b = Box::new(5); // b is a Box pointing to an integer on the heap
println!("Value inside the Box: {}", b);
}
21. Rc <T>
• Rc<T> : (Reference Counted) is a smart pointer for shared ownership. It keeps track of the number of
references to a value and automatically cleans up the data when the last reference is dropped.
It allows multiple ownership of data within a single thread.
Rc is lightweight and performs well for small data structures.
Use Cases
o Use Rc when you need shared ownership within a single thread.
Example : use std::rc::Rc;
fn main(){
let data = Rc::new(42);
let reference1 = Rc::clone(&data);
let reference2 = Rc::clone(&data);
println!("Data: {}", data);
}
22. Arc<T>
Arc<T> (Atomically Reference Counted) is similar to Rc<T> but is thread-safe and can be shared
across threads. It uses atomic operations to manipulate the reference count, ensuring thread
safety.
Use Cases:
o Sharing immutable data between multiple threads.
Example :
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(42);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("Data in thread: {}", data_clone);
});
handle.join().unwrap();
println!("Data in main thread: {}", data);
}
23. Mutex<T> and RwLock<T>:
Mutex<T> and RwLock<T> are smart pointers that provide interior mutability by synchronizing access to
shared data. Mutex<T> allows only one thread to access the data at a time, while RwLock<T> allows
multiple readers or one writer at a time
Use Cases:
Sharing mutable data between multiple threads safely.
Example :
24. Generics <T>
Generics in Rust are a powerful language feature that allows you to write code that operates on different
types while maintaining type safety.
They provide a way to write reusable code that can work with different types and across multiple context
without sacrificing performance or safety
Let's explore generics and their uses in different aspects of Rust programming:
o Functions
o Structs
o Enums
o Methods
25. Functions<T>
Functions : In functions, generics allow you to write code that can accept arguments of any type. Instead
of specifying concrete types, you use placeholder type parameters.
Example :
Use Cases : Generics in functions are handy when you want to create a function that can operate on
different types without having to duplicate code for each type.
fn print_value<T>(value: T) {
println!("Value: {}", value);
}
26. Structs<T>
Generics in structs allow you to define data structures that can hold values of any type. Similar to functions,
you use placeholder type parameters when defining a generic struct.
Example :
Use cases : Generic structs are useful when you need to create a data structure that can store elements of
various types in a type-safe manner.
struct Pair<T> {
first: T,
second: T,
}
27. Enums<T>
Enums with generics enable you to define variants that can hold values of different types. Like structs, you
use placeholder type parameters for generic enum variants.
Example :
Use cases :Generic enums are often used to represent results or errors that can contain values of different
types, providing flexibility and type safety.
enum Result {
Ok(T),
Err(E),
}
28. Methods <T>
Rust allows you to define generic methods on structs. You use the same syntax with type parameters as in
functions and structs.
Example :
Use cases : Generic methods enable you to define behaviour that can work with any type, providing code
reuse and flexibility.
impl<T> Pair<T> {
fn get_first(&self) -> &T {
&self.first
}
}
29. Performance of Generics
Generics in Rust are a zero-cost abstraction. The compiler generates specialized versions of generic code for
each concrete type it's used with, eliminating runtime overhead.
Rust's monomorphization process ensures that generic code is compiled into efficient and specialized
versions, resulting in performance similar to non-generic code.
Overall, generics in Rust are a fundamental feature that enables code abstraction, reuse, and type safety
across various aspects of Rust programming, including functions, structs, enums, methods, and more. They
contribute to writing clear, concise, and efficient code in Rust.
30. Traits
Traits define behavior that types can implement.
They specify a set of methods that types must provide to satisfy the trait's requirements.
Methods within a trait are like function signatures, outlining what actions a type should support.
Traits act as contracts or interfaces in Rust, setting rules that types must follow to be considered part of a
group or to have a certain capability.
Once a type implements a trait, it gains access to the behaviors defined by that trait.
This allows different types to share common behavior, making them interchangeable in code that relies on
traits.
Traits promote code reuse and polymorphism, enabling more flexible and versatile Rust code.
31. Types of Traits
Marker Trait : Traits that do not contain any method signatures and are used to provide type-level
information or as flags.
Trait with Associated Functions: Traits that contain associated functions (functions associated with the trait
itself).
Trait with Required Methods: Traits that define a set of methods that types must implement.
Trait with Default Implementations: Traits that provide default implementations for some or all of their
methods, allowing types to choose whether to override them.
Trait with Supertraits: Traits that inherit methods from other traits, enabling hierarchical trait composition.