Control Flow
if-else if-else
fn main() {
let n = 6;
if n % 4 == 0 {
println!("number is divisible by 4");
} else if n % 3 == 0 {
println!("number is divisible by 3");
} else if n % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
loop control
for in
for i in 1..=5 {
println!("{}", i);
}
| How to use | Equivalent usage | ownership |
|---|---|---|
for item in collection | for item in IntoIterator::into_iter(collection) | transfer ownership |
for item in &collection | for item in collection.iter() | immutable borrowing |
for item in &mut collection | for item in collection.iter_mut() | variable borrow |
while
fn main() {
let mut n = 0;
while n <= 5 {
println!("{}!", n);
n = n + 1;
}
println!("I'm out!");
}
loop
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
- break can be used alone or with a return value
- loop is an expression
pattern matching
Haunting Functional Programming
match match
enum Direction {
East,
South,
West,
North,
}
fn main() {
let dire = Direction::South;
match dire {
Direction::East => { println!("east"); }
Direction::South | Direction::West => {
println!("south or west");
}
_ =>{
println!("north");
}
}
}
match expression assignment
let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let ip_str = match addr {
IpAddr::V4(ip) => {
"127.0.0.1"
}
IpAddr::V6(ip) => {
"other"
}
};
macth mode binding
enum Action {
Say(String),
MoveTo(i32, i32),
ChangeColorRGB(u8, u8, u8),
}
fn main() {
let actions = [
Say("Hello, world!".to_string()),
Action::MoveTo(100, 200),
Action::ChangeColorRGB(255, 0, 255),
];
for action in &actions {
match action {
Say(s) => {
println!("{}", s);
}
Action::MoveTo(x, y) => {
println!("move to {}, {}", x, y);
}
Action::ChangeColorRGB(r, b, _) => {
println!("change color RGB to {}, {}", r, b);
}
}
}
}
if let match
Sometimes only one mode value needs to be processed, and other cases are ignored
let v = Some(5);
if let Some(5) = v {
println!("five")
}
matches! macro
#[derive(Debug)]
enum MyEnum{
Foo,
Bar
}
fn main() {
let v = vec![MyEnum::Foo, MyEnum::Bar];
let filter = v.iter().filter(|x| matches!(x, MyEnum::Bar));
filter.for_each(|x| println!("{:?}", x));
let foo = 'f';
let contains_foo = matches!(foo,'a'..='z'| 'A'..='Z');
println!("{}", contains_foo);
let bar = Some(4);
println!("{:?}", matches!(bar, Some(x) if x > 2));
}
Mode applicable scenarios
Pattern is a special syntax in Rust, used to match structures and data in types. It is often used in conjunction with match expressions to achieve powerful pattern matching capabilities. Patterns generally consist of the following:
- Literal value
- Destructured array, enumeration, structure or tuple
- variable
- Wildcard
- placeholder
if let branch
Match one and ignore the rest
if let PATTERN = SOME_VALUE {
}
while let
Loop as long as the pattern matches
fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
while let Some(i) = v.pop() {
println!("{}", i);
}
}
for loop
fn main() {
let v = vec!['a', 'b', 'c', 'd', 'e', 'f'];
for (index, value) in v.iter().enumerate() {
println!("{} - {}", index, value);
}
}
let statement
let x = 5;
let (x, y, z) = (1, 2, 3);
MethodMethod
Define method
struct Circle{
x:f64,
y:f64,
radius:f64,
}
impl Circle{
fn new(x:f64,y:f64,radius:f64)->Circle{
Circle{x,y,radius}
}
fn area(&self)->f64{
std::f64::consts::PI*(self.radius*self.radius)
}
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
The difference between self, &self and &mut self
- self indicates that the ownership of Rectangle is transferred to this method. This form is rarely used.
- &self represents the method’s immutable borrowing of Rectangle
- &mut self represents variable borrowing
Generics and Traits
Generics
Using generics in structures
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let point = Point { x: 1, y: 'z' };
}
enum generics
enum Option<T> {
Some(T),
None,
}
method generic
struct Point<T>{
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn y(&self) -> f32 {
self.y
}
}
const generic
Generics for values Looking back at arrays, arrays have different lengths and different types
fn display_array(arr:[i32;3]){
println!("{:?}", arr);
}
fn main() {
let arr:[i32;3] = [1, 2, 3];
display_array(arr);
let arr:[i32;2] = [1, 2];
display_array(arr)
}
Compilation error At this time, you need to pass the array slice
fn display_array(arr:&[i32]){
println!("{:?}", arr);
}
fn main() {
let arr:[i32;3] = [1, 2, 3];
display_array(&arr);
let arr:[i32;2] = [1, 2];
display_array(&arr)
}
Then change i32 to receive an array of all types
use std::fmt::{Debug};
pub mod rust_base;
fn display_array<T>(arr: &[T])
where
T: Debug,
{
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);
let arr: [i32; 2] = [1, 2];
display_array(&arr)
}
Use const generics to solve
fn display_array<T:Debug,const N:usize>(arr: [T;N])
where
T: Debug,
{
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr)
}
const fn constant function
why needed In some scenarios, we hope to calculate some values during compilation to improve running performance. Basic usage
const fn add(a:usize,b:usize) -> usize {
a + b
}
const RESULT:usize = add(5,10);
fn main() {
println!("{}", RESULT);
}
const fn limit
Whether const fn is called at compile time or at runtime, their result is always the same, even if called multiple times. The only exception is that if you do complex floating point operations in extreme cases, you may get different results. Therefore, it is not recommended to use array length(arr.len())andEnumdiscriminantRelies on floating point calculations
Combining const fn with const generics
struct Buffer<const N:usize>{
data: [u8; N],
}
const fn compute_buffer_size(factor:usize)->usize{
factor*1024
}
fn main() {
const SIZE:usize = compute_buffer_size(4);
let buffer = Buffer::<SIZE> {
data: [0; SIZE],
};
println!("{}", buffer.data.len());
}
Trait Trait
Feature definition
pub trait Summary{
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct Post{
title: String,
content: String,
author: String,
}
impl Summary for Post {
fn summarize(&self) -> String {
format!("article{}, The author is{}", self.title, self.author)
}
}
pub struct Weibo{
username: String,
content: String,
}
impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{}Posted on Weibo{}", self.username, self.content)
}
}
fn main() {
let post = Post{title: "RustLanguage introduction".to_string(),author: "Sunface".to_string(), content: "RustAwesome!".to_string()};
let weibo = Weibo{username: "sunface".to_string(),content: "It seems that Weibo is not thereTweetEasy to use".to_string()};
println!("{}",post.summarize());
println!("{}",weibo.summarize());
}
Use features as function parameters
pub fn notify(item:&impl Summary){
println!("{}",item.summarize());
}
feature constraints
impl Trait syntactic sugar
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
multiple constraints
pub fn notify(item: &(impl Summary + Display)) {}
Equivalent to
pub fn notify<T: Summary + Display>(item: &T) {}
where constraint
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
Too complicated, easy way
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
Define the type first, and then constrain it through where
Function returning impl Trait
fn returns_summarizable() -> impl Summary {
Post{title: "RustLanguage introduction".to_string(),author: "Sunface".to_string(), content: "RustAwesome!".to_string()}
}
This kind of return has a big limitation,Only one specific type can be returned
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Post {
title: String::from(
"Penguins win the Stanley Cup Championship!",
),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Weibo {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
}
}
}
The above code will not compile because it returns two different types Post and Weibo
`if` and `else` have incompatible types
expected struct `Post`, found struct `Weibo`
The error message reminds us that if and else return different types. If you want to return different types, you need to use a trait object
Feature object
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Post {
// ...
}
} else {
Weibo {
// ...
}
}
}
Code cannot be compiled
Polymorphic implementation mechanism in Rust
- Static distribution (monomorphism): type determined at compile time, high performance, no running overhead
- Dynamic distribution (Trait Object
Box<dyn Trait>/&dyn Trait): The type is determined at runtime and supports collection storage of different polymorphic classes. It is truly polymorphic.
- Triat: Define a unified behavioral interface
- dyn Trait: Characteristic object, realizing dynamic polymorphism, requiring trait to satisfy object security(Contains only methods without generics, self is
&self/&mut self)
Static distribution (compile-time polymorphism, generics)
trait Animal{
fn speak(&self);
}
struct Dog;
impl Animal for Dog{
fn speak(&self) {
println!("woof woof woof");
}
}
struct Cat;
impl Animal for Cat{
fn speak(&self) {
println!("Meow meow meow")
}
}
fn make_sound<T:Animal>(animal:T){
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
make_sound(dog);
make_sound(cat);
}
Generic functions will generate separate code for each incoming type and are bound at compile time, which is the fastest.
Features
- Compile-time monomorphism, no virtual tables, no runtime overhead
- shortcoming:Cannot put Dog and Cat into the same Vec(different types)
Dynamic distribution (Trait Object, truly polymorphic, recommended for business use)
use Box<dyn Animal> Characteristic objects, different implementation classes can be stored in the same container, and the implementation will be automatically matched at runtime. This is Rust's standard polymorphic writing method.
// unified behavioral interface
trait Animal {
fn speak(&self);
}
// accomplish1: dog
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("{}: woof woof woof", self.name);
}
}
// accomplish2: cat
struct Cat {
name: String,
}
impl Animal for Cat {
fn speak(&self) {
println!("{}: Meow meow meow", self.name);
}
}
// accepts any implementation Animal characteristic object
fn make_sound(animal: &dyn Animal) {
animal.speak();
}
fn main() {
// Store different types into the same Vec<Box<dyn Animal>>
let mut animals: Vec<Box<dyn Animal>> = Vec::new();
animals.push(Box::new(Dog { name: "Prosperity".into() }));
animals.push(Box::new(Cat { name: "Mimi".into() }));
// Unified call for traversal, polymorphism takes effect
for animal in animals {
animal.speak();
}
// call alone
let dog = Dog { name: "Xiaohei".into() };
make_sound(&dog);
}
Dynamic distribution principle
Box<dyn Animal> Two pointers are stored internally:
- Data pointer: pointing to Dog/Cat instance
- Virtual table (vtable) pointer: points to the method list of this type to implement Animal, and the table is looked up and called at runtime.
Polymorphism with mutable methods (&mut dyn Trait)
If the trait needs to modify itself, use &mut dyn
// unified behavioral interface
trait Animal {
fn speak(&self);
fn rename(&mut self, new_name: &str);
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("{}: woof woof", self.name);
}
fn rename(&mut self, new_name: &str) {
self.name = new_name.to_string();
}
}
fn main() {
let mut dog = Dog { name: "Dog".to_string() };
let mut animal:&mut dyn Animal = &mut dog;
animal.speak();
animal.rename("rhubarb");
animal.speak();
}
Key Limitations: Trait Object Safety Rules For dyn Trait to be legal, the trait must be object-safe.
- All methods cannot have generics
- The method receiving self can only be: &self/&mut self, not self (ownership transfer)
- Method return type cannot be Self (this is uppercase Self and refers to the current trait or method type alias)
Error example
trait BadTrait {
// With generics, unsafe
fn foo<T>(&self, x: T);
// take ownershipself, not safe
fn bar(self);
}
Comparison between static distribution and dynamic distribution
sheet
| Way | accomplish | advantage | shortcoming | Usage scenarios |
|---|---|---|---|---|
| static distribution | Generics T: Trait | Zero running overhead, highest performance | Cannot store different types into the same collection | Performance-sensitive, fixed-type scenarios |
| dynamic distribution | Box<dyn Trait> / &dyn Trait | Support polymorphic containers and unified management of multiple implementations | Runtime virtual table lookup, slight performance loss | Plug-ins, unified processing of multiple types (GUI, hardware driver) |
Self and self
In Rust, there are two self, one refers to the current instance object, and one refers to the characteristic or method type alias.
trait Draw{
fn draw(&self)->Self;
}
#[derive(Clone)]
struct Button;
impl Draw for Button {
fn draw(&self)->Self{
self.clone()
}
}
fn main() {
let button = Button;
let new_b = button.draw();
}
Dive deeper into features
Association type
Association typeIt is to create a custom type in the statement block of the characteristic definition, so that the type can be used in the method signature of the characteristic:
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
}
}
fn main() {
let c = Counter{..}
c.next()
}
Self used to refer to the specific type of the current caller, then Self::Item is used to refer to the type defined in the implementation Item type
Default generic type parameters
use std::ops::Add;
#[derive(Debug,PartialEq)]
struct Point{
x: i32,
y: i32,
}
impl Add for Point{
type Output = Point;
fn add(self, rhs: Self) -> Self::Output {
Point{x: self.x + rhs.x, y: self.y + rhs.y}
}
}
fn main() {
let point = Point { x: 1, y: 0 } + Point { x: 2, y: 3 };
println!("{:?}", point);
}
Call a method with the same name
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
// person.fly();
Pilot::fly(&person); // callPilotmethod on features
Wizard::fly(&person); // callWizardmethod on features
person.fly(); // callHumanmethods of the type itself
}
- Call its own method by default
- Secondly call the feature method
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}