Bootstrap

Rust快速入门(四)

流程控制

if-else

if condition == true {
    // A...
} else {
    // B...
}

使用if进行赋值

fn main(){
    let condition = true;
    let a = if condition {
        5
    }else {
        6
    };
    println!("the value of a is {}.", a)
}

这里可以赋值是因为 “if语句块” 是一个 表达式

if 来赋值时需要保证每个分支返回的类型一样

for

fn main(){
    for i in 1..=5 {
        print!("{}", i)
    }
}

由于 m..n 不会取到n,所以我们使用 = 保证能取到1-5.

当我们使用 for循环时,我们常常使用引用形式,保证所有权不会转移到for循环中,导致后续无法继续使用集合(如果后续还需要使用的话)。(这里和前文同理,如果是Copy类型,则会对数据进行深拷贝,所以直接遍历基础类型就不会导致所有权转移)

  • 如果希望在循环中,修改该元素,那么必须使用 mut 关键字:

    for item in &mut collection {
      // ...
    }
  • 如果只是希望循环执行某个代码块n次:

    for _ in 0..n {
        // ...
    }

两种遍历方式:

fn main(){
    let arr = [1,2,3,4,5];
    // 两种遍历方式
    // 1. 按照下标
    for i in 0..arr.len() {
        let item = arr[i];
        println!("{}", item)
    }
    println!("_______________________");
    // 2. 直接遍历元素
    for item in arr {
        println!("{}", item)
    }
}

第一种由于需要在运行时检查索引是否越界,所以会有性能损耗。第二种直接会在编译过程中完成检测。

break

在rust中,break是一个表达式,可以返回一个值。所以在loop循环中,我们使用break进行终止

模式匹配

match 和 if let

match

match的匹配是一个穷尽式。

enum Direction {
    East,
    _West,
    North,
    South,
}
​
fn main(){
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("North or South");
        },
        _ => println!("West"),
    };
}
  • 这里 match 匹配必须要穷举出所有可能,因此可以使用 _ 来列出所有可能性。

  • match 所有分支都必须是一个表达式,且所有表达式最终返回值的类型必须相同。

  • | 类似逻辑运算符 OR

该结构十分类似 switch 其中_类似于 default

通用

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

使用match 进行赋值:

enum IpAddr {
    Ipv4,
    Ipv6,
}
​
fn main(){
    let ip1 = IpAddr::Ipv6;
    let ip_str = match ip1 {
        IpAddr::Ipv4 => "127.0.0.1",
        _ => "::1",
    };
    println!("{}", ip_str)
}
if let

只有一个模式的值需要被处理,往往用于匹配一个模式。其他值忽略场景下使用if let更加的简洁:

match v {
    Some(3) => println!("three"),
    _ => (),
}
​
//-------------------
if let Some(3) = v {
    println!("three");
}

解构 Option

用于解决Rust中变量是否有值的问题,其定义如下:

enum Option<T> {
    None,
    Some(T),
}

一个变量要么有值:Some(T),要么为空:None.

方法 Mehtod

在rust中,方法于对象成对出现

定义

在Rust中使用 impl 来定义方法:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}
​
impl Circle {
    fn new(x:f64, y:f64, radius:f64) -> Circle {
        Circle{
            x:x,
            y:y,
            radius: radius,
        }
    }
​
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
    
}

方法定义与golang非常相似,结构体存储数据,调用方法功能放在方法中。即对象定义与方法定义是分开的,这种方式给予了使用者极高的灵活性。

self、&self、&mut self

self其实代替的是Circle, &self 其实是 &self:Self 的简写。在一个 impl 块内,Self指代被实现方法的结构体类型,self 指代此类型的实例,换句话说, self指代的是Circle的结构体实例,我们为哪个结构体实现方法,那么 self就指代哪个结构体。

同理self存在所有权概念,与引用一致。

简单总结下,使用方法代替函数有以下好处:

  • 不用在函数签名中重复书写 self 对应的类型

  • 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大

方法和结构体字段相同

在Rust中,允许方法跟结构体的字段同名,这点很好理解可以联想Java里面public关键字修饰的属性和其对应的getter方法。

根据上述描述,我们可以知道在Rust中,这种方法常被用于声明getter方法。

对上述Circle结构体与方法进行完善:

mod my {
    pub struct Circle {
        x: f64,
        y: f64,
        radius: f64,
    }
    
    impl Circle {
        pub fn new(x:f64, y:f64, radius:f64) -> Circle {
            Circle{
                x:x,
                y:y,
                radius: radius,
            }
        }
    
        pub fn x(&self) -> f64 {
            self.x
        }
    
        pub fn y(&self) -> f64 {
            self.y
        }
    
       pub  fn area(&self) -> f64 {
            std::f64::consts::PI * (self.radius * self.radius)
        }
        
    }
}
​
fn main() {
    let circle = my::Circle::new(10.0, 20.0, 5.0);
    println!("the x is {}; the y is {}", circle.x(), circle.y());
    println!("the area is {}", circle.area())
}mod my {
    pub struct Circle {
        x: f64,
        y: f64,
        radius: f64,
    }
    
    impl Circle {
        pub fn new(x:f64, y:f64, radius:f64) -> Circle {
            Circle{
                x:x,
                y:y,
                radius: radius,
            }
        }
    
        pub fn x(&self) -> f64 {
            self.x
        }
    
        pub fn y(&self) -> f64 {
            self.y
        }
    
        pub fn area(&self) -> f64 {
            std::f64::consts::PI * (self.radius * self.radius)
        }
        
    }
}
​
fn main() {
    let circle = my::Circle::new(10.0, 20.0, 5.0);
    println!("the x is {}; the y is {}", circle.x(), circle.y());
    println!("the area is {}", circle.area())
}

这里使用mod对结构体和方法进行了封装,所以需要使用pub关键字来让外部可访问被封装的信息。

注意:在c\cpp中,有两个不同的运算符来调用方法, . 在对象上调用方法,而 -> 在一个对象的指针上调用方法,这里需要解引用指针。

Rust中并没有 -> 这个运算符,但是存在一个叫自动引用和解引用的功能,方法调用就是拥有这个行为的地方,当用户使用 object.something()调用方法时,Rust会自动为 object 添加 &, &mut, 或 * 以便使 object 与方法签名进行匹配,即:

p1.distance(&p2);
// 等价于
(&p1).distance(&p2);

为什么可以用第一种方法,是因为方法有一个明确的接收者self。

关联性函数

在Rust中为一个结构体定义一个构造器方法,只需要在参数中不包含self即可。

在Rust中我们约定使用new作为构造器名称,处于设计上考虑,new不作为关键字

因为构造器为函数,所以需要使用 :: 来调用,而不是使用方法类似的 . 来调用。

多impl定义

Rust允许我们定义多个 impl 块,目的是提供更多的灵活性和可读性,即可以将相关代码放在一起,进行解耦。

泛型和特征

泛型

一个非常有趣且通俗的解释:

泛型详解

T 就是泛型参数,我们一般约定俗成为 T

使用泛型参数有一个先决条件,就是在使用前必须先声明。

特征

与接口类似,我们来看看定义:

pub trait Summary {
    fn summarize(&self) -> String;
}
补充完整上述代码:

use std::fmt::format;
​
pub trait Summary {
    fn summarize(&self) -> String;
}
​
pub struct Post {
    pub title: String,
    pub author: String,
    pub content: String,
}
​
impl Summary for Post {
    fn summarize(&self) -> String {
        format!("文章: {}, 作者: {}", self.title, self.author)
    }
}
​
pub struct Weibo {
    pub username: String,
    pub content: String,
}
​
impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("发帖人: {}, 用户: {}", self.username, self.content)
    }
}
​
fn main() {
    let post = Post{title: "Rust 语言简介".to_string(), author: "Victor".to_string(), content: "rust 是世界上最好的语言.go".to_string()};
    let weibo = Weibo{username:"Benaso".to_string(), content: "gogogogogogo".to_string()};
​
    println!("{}", post.summarize());
    println!("{}", weibo.summarize());
}

特征定义与实现的位置(孤儿规则)

如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的

。。。后续再看,有点ex了,看不懂。

集合类型

动态数组Vector

创建动态数组的方式:

  • Vec::new

    这种方式是最 rusty的方式,调用Vec里面的new关联函数(构造器)

    let v:Vec<i32> = Vec::new();
    ​
    // 或者
    ​
    let mut v = Vec::new();
    v.push(1);

    这里因为可以根据下面的 1 判断向量类型,所以可以直接不声名类型。

  • vec![]

    该方法能够在创建时同时赋予初始化值

    let mut v = Vec::new();
    v.push(1);

数组访问

采用两种方式:

  1. 下标索引

  2. .get() 函数

相较于直接通过索引 &v[10] 通过 v.get(10)更加安全,对数组越界做了处理,有值的时候返回Some(T), 无值的时候返回 None。

注意,在如下例子种:

let mut v = vec![1, 2, 3, 4, 5];
​
let first = &v[0];
​
v.push(6);
​
println!("The first element is: {first}");

由于 first 是不可变借用,v.push(6) 是可变借用,虽然一个是读,一个是写操做,但是当数组大小不够用时,Rust会重新为其分配一块更大的空间,这样一来之前的引用就会指向一块无效的内存。

所以上述代码的first在println! 宏里面又使用了,显而易见会报错。

迭代遍历Vector 种元素

如果希望依次访问数组中元素,可以用迭代方式,这种方式比使用下标去遍历数组更加安全高效(下标遍历每次都会检查数组是否越界)

HashMap

Rust 中哈希类型(哈希映射)为 HashMap<K,V>

创建方式:

fn main() {
    let mut my_gems = hashMap::new();
    my_gems.insert("1", 1);
    my_gems.insert("2", 2);
    my_gems.insert("3", 3);
}

更新Map值

fn main() {
    use std::collections::HashMap;
​
    let mut scores = HashMap::new();
    scores.insert("1", 1);
    match scores.get("1") {
        Some(&score) => println!("{}", score),
        None => println!("no parameter!"),
    }
​
    let v = scores.entry("2").or_insert(2);
    assert_eq!(*v, 2);
    match scores.get("2") {
        Some(&score) => println!("{}", score),
        None => println!("no parameter!"),
    }
}

上面代码描述了插入使用 insert() 和使用 .entry().or_insert() 两种方法,第二种方法会先校验是否存在entry函数中的key,存在就进行插入。

get 方法返回一个 Option<&V> 类型的值。具体来说:

  • 如果键存在于 HashMap 中,get 方法返回 Some(&V),其中 &V 是指向对应值的引用。

  • 如果键不存在于 HashMap 中,get 方法返回 None

;