Rust - Advanced
RUST SRM
Rust Programming Notes (Advanced Topics)
10. Struct
Structs are custom data types that let you group related data.
Example:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
println!("User {} has email {}", user1.username, user1.email);
}
11. Struct Implementation
You can implement methods and associated functions for structs.
Example:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method
fn area(&self) -> u32 {
self.width * self.height
}
// Associated function
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect1.area());
let sq = Rectangle::square(3);
println!("Square area: {}", sq.area());
}
12. Enum
Enums allow you to define a type by enumerating its possible variants.
Example:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// Method body would be defined here
println!("Message called");
}
}
fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}
13. Pattern Matching with Option and Result Enum
Option
Option<T>
represents an optional value: every Option
is either Some
and contains a value, or None
, and does not.
Example:
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Some(x) => println!("Result: {}", x),
None => println!("Cannot divide by zero"),
}
// Using if let
if let Some(x) = divide(10.0, 0.0) {
println!("Result: {}", x);
} else {
println!("Division by zero is not allowed");
}
}
Result<T, E>
Result<T, E>
is used for returning and propagating errors. It has two variants: Ok(T)
, representing success and containing a value, and Err(E)
, representing error and containing an error value.
Example:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
};
}
14-15. Error Handling
Rust groups errors into two major categories: recoverable and unrecoverable errors.
Unrecoverable Errors with panic!
For unrecoverable errors, the panic!
macro is used.
Example:
fn main() {
panic!("crash and burn");
}
Recoverable Errors with Result
For recoverable errors, the Result
enum is used.
Example:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
Using unwrap
and expect
:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
16. Generics
Generics allow you to write code that works with multiple types. Here’s a practical example:
use std::ops::Add;
struct Point<T> {
x: T,
y: T,
}
impl<T: Add<Output = T>> Add for Point<T> {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2;
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
let p4 = Point { x: 1.1, y: 1.2 };
let p5 = Point { x: 2.1, y: 2.2 };
let p6 = p4 + p5;
println!("p6.x = {}, p6.y = {}", p6.x, p6.y);
}
This example demonstrates:
- A generic
Point
struct that can work with any typeT
. - Implementation of the
Add
trait forPoint<T>
, allowing points to be added together. - The use of the same struct and implementation for both integer and floating-point types.
17. Traits
Traits define shared behavior across types. They’re similar to interfaces in other languages.
Example:
trait Summary {
fn summarize(&self) -> String;
}
struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
18. Lifetimes
Lifetimes are Rust’s way of ensuring that references are valid for as long as we need them to be. Every reference in Rust has a lifetime, which is the scope for which that reference is valid.
Key points:
- Lifetimes are usually implicit and inferred by the compiler.
- We must annotate lifetimes when the lifetimes of references could be related in a few different ways.
- Lifetime annotations don’t change how long any of the references live. They describe the relationships of the lifetimes of multiple references to each other.
Example with explanation:
// 'a is a lifetime parameter
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("Longest string: {}", result);
}
}
In this example:
'a
is a lifetime parameter that represents the scope for which the returned reference is valid.- The function signature
longest<'a>(x: &'a str, y: &'a str) -> &'a str
tells the compiler that the returned reference will be valid for the smaller of the lifetimes ofx
andy
. - This prevents us from using the result after the shorter-lived input reference has gone out of scope.
Lifetimes in struct definitions:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
This struct definition means an instance of ImportantExcerpt
can’t outlive the reference it holds in its part
field.
19. Cargo
Cargo is Rust’s package manager and build system. It handles many tasks:
- Creating new projects:
cargo new project_name
- Building projects:
cargo build
- Running projects:
cargo run
- Testing:
cargo test
- Checking code without producing an executable:
cargo check
- Building documentation:
cargo doc
- Publishing libraries to crates.io:
cargo publish
Cargo.toml file:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Adding a dependency:
cargo add some_crate
Updating dependencies:
cargo update
Cargo workspaces allow you to manage multiple related packages:
[workspace]
members = [
"package1",
"package2",
]