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 useEquivalent usageownership
for item in collectionfor item in IntoIterator::into_iter(collection)transfer ownership
for item in &collectionfor item in collection.iter()immutable borrowing
for item in &mut collectionfor 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

  1. Static distribution (monomorphism): type determined at compile time, high performance, no running overhead
  2. 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:

  1. Data pointer: pointing to Dog/Cat instance
  2. 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.

  1. All methods cannot have generics
  2. The method receiving self can only be: &self/&mut self, not self (ownership transfer)
  3. 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

WayaccomplishadvantageshortcomingUsage scenarios
static distributionGenerics T: TraitZero running overhead, highest performanceCannot store different types into the same collectionPerformance-sensitive, fixed-type scenarios
dynamic distributionBox<dyn Trait> / &dyn TraitSupport polymorphic containers and unified management of multiple implementationsRuntime virtual table lookup, slight performance lossPlug-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());
}