Rust -基础学习

602 阅读41分钟

知识点:

  • :: 是一种调用操作符,这里表示调用 String 中的 from 方法
  • println! 向命令行输出字符串, println!("some description {}",变量),变量会展示到占位符{}的位置。

println!() 和 print!()。这两个"函数"都是向命令行输出字符串的方法,区别仅在于前者会在输出的最后附加输出一个换行符。

1、什么是Rust语言?

由Mozilla发布于2014年,兼具安全与性能优势,是一种快速、高并发、安全且具有授权性的编程语言

Rust可以做什么?

1、编写操作系统、游戏引擎、和性能关键型应用程序。

2、构建高性能的Web应用程序、网络服务、类型安全的数据库对象关系映射,以及WebAssembly(WebAssembly:一个可以将其他程序编译成WebAssembly,从而在浏览器中运行的 可移植、体积小、加载快的全新的web新格式,javascript的一种替代品)。

2、例行“Hello,world!”

fn main() {  
    println!("Hello World!"); // 输出 "Hello World!"
}

编译 rustc <文件名>
执行 ./<文件名>

rust语言中使用fn关键词定义函数

println!() 是rust中的一个预定义的宏(宏:封装一些通用的处理逻辑,方便使用者调用)rust中使用宏需要加感叹号。

3、cargo——管理 Rust 工程和获取工程所依赖的库

  • 创建项目:cargo new project-name
  • 打包编译:cargo build
  • 运行项目:cargo run
cargo build/run --release 使用 release 编译会比默认的 debug 编译性能提升 10 倍以上,
但是 release 缺点是编译速度较慢,而且不会显示 panic backtrace 的具体行号
  • 静态检查 cargo clippy: 类似eslint,lint工具检查代码可以优化的地方
  • 格式化 cargo fmt: 类似go fmt,代码格式化
  • cargo tree: 查看第三方库的版本和依赖关系
  • cargo bench: 运行benchmark(基准测试,性能测试)
  • cargo udeps(第三方): 检查项目中未使用的依赖

4、变量绑定(给某一块内存地址起名字)

变量命名规范:包含字母、数字、下划线、并以字母、下划线开头,区分大小写

Rust中使用let定义变量
let a='abc'
let定义的变量为值不可变变量,使用关键字mut可以定义值可变变量

// let定义的变量为值不可变变量
let a=1;
a=2; // 报错 cannot assign twice to immutable variable `a`

// let mmut定义的是值可变变量
let mut b=2;
b=3; // b的值被重新赋值为3

// cont 定义的是常量,变量名大写,必须定义类型
const CCC:i32=555;

// 常量与变量的区别
/**
*** 常量与变量的区别;
*** 变量:可以被重新定义,即可以为变量”重新绑定”一个新的值。
*** 常量:不可以重新赋值,也不可以为常量”重新绑定”一个新的值。
*/

let d=1;
let d=2; // 不会报错,是为变量”重新绑定”一个新的值。

const EE:i32=1;
const EE:i32=2; // 报错the name `EE` is defined multiple times

重影(使变量得名称可以被重新使用):

// 变量可以“重新绑定”一个新的值,这个值得类型也是可变的
let a:i32=2;
println!("a is {}",a); // a is 2
let a='s';
println!("a is {}",a); // 输出 a is s

重影与可变变量的赋值不是一个概念,重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。

let mut s = "123";
s = s.len(); // 报错 mismatched types 可变变量的赋值不能改变值得类型

使用下划线开头可以忽略未使用变量的告警

如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为这可能会是个 BUG。但是有时创建一个不会被使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头

fn main() {
    let _x = 5;
    let y = 10;
}

使用 cargo run 运行下试试:

warning: unused variable: `y`
 --> src/main.rs:3:9
  |
3 |     let y = 10;
  |         ^ help: 如果 y 故意不被使用,请添加一个下划线前缀: `_y`
  |
  = note: `#[warn(unused_variables)]` on by default

可以看到,两个变量都是只有声明,没有使用,但是编译器却独独给出了 y 未被使用的警告,充分说明了 _ 变量名前缀在这里发挥的作用。

变量解构

let 表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:

fn main() {
    let (a, mut b): (bool,bool) = (true, false);
    // a = true,不可变; b = false,可变
    println!("a = {:?}, b = {:?}", a, b);

    b = true;
    assert_eq!(a, b);
}

解构式赋值

在 Rust 1.59 版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式了。

struct Struct {
    e: i32
}

fn main() {
    let (a, b, c, d, e);

    (a, b) = (1, 2);
    // _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
    [c, .., d, _] = [1, 2, 3, 4, 5];
    Struct { e, .. } = Struct { e: 5 };

    assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}

这种使用方式跟之前的 let 保持了一致性,但是 let 会重新绑定,而这里仅仅是对之前绑定的变量进行再赋值。

5、所有权和借用

所有权

所有权原则

  • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

拷贝(Copy),对于以下类型来说,给变量绑定一个值之后,相当于重新拷贝了一份相同的值。

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true 和 false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
  • 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的
let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

如上代码, x被绑定了一个i32类型的值 5,这种基本类型,不需要分配内存或者某种形式的资源,在编译时是已知大小的,会被存储到栈上,所以拷贝实际得值很快速。所以let y = x在这里是直接拷贝了一份值给y,不会违反所有权的原则,x和y拥有的是两个值。

克隆(Clone) 针对复杂类型 如String是由存储在栈中的堆指针、字符串长度,字符串容量共同组成,其中堆指针指向了真实存储字符串内容的堆内存,总的来说就是‘类型指向了一个堆上的空间,这里存储着它的真实数据’。

如下代码,中的 let s2 = s1 分成两种情况讨论:

  1. 拷贝 String 和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是 String 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响
  2. 只拷贝 String 本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了 8字节的指针8字节的长度8字节的容量,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1 和 s2

好吧,就假定一个值可以拥有两个所有者,会发生什么呢?

当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)  的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

因此,Rust 这样解决问题:当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

由于 Rust 禁止你使用无效的引用,你会看到以下的错误:

error[E0382]: use of moved value: `s1`
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait

引用&借用

函数传值与返回

将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,下面的代码展示了所有权、作用域的规则:

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // String是复杂类型,所以这里是将s的所有权转移给takes_ownership
                                    // s的所有权被转移,所以这里s以及不生效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 的类型是i32简单类型,所以这里是将x的值直接拷贝一份给  makes_copy
                                    // 这里x仍然有对其值得所有权

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

你可以尝试在 takes_ownership 之后,再使用 s,看看如何报错?例如添加一行 println!("在move进函数后继续使用s: {}",s);

同样的,函数返回值也有所有权,例如:

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能 借用(Borrowing) 解决这个问题。

Rust 通过 借用(Borrowing) 这个概念来达成上述的目的,获取变量的引用,称之为借用(borrowing) 。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

&x表示x的引用, *y表示解引用

不可变引用

“你只是有了值的使用权,并不是值的拥有者,不能对值进行改变”

fn main() {
    let s1 = String::from("hello");

    let s2 = takes_and_gives_back(&s1);

    let s3 = takes_and_gives_back1(&s1);

    fn takes_and_gives_back(a_string: &String) -> &String {
        a_string
    }

    fn takes_and_gives_back1(a_string: &String) -> String {
        String::from("say ") + a_string
    }

    println!("s1 is {}", s1); // s1 is hello
    println!("s2 is {}", s2); // s2 is hello
    println!("s3 is {}", s3); // s3 is say hello
}

可变引用

fn main() {
    let mut s = String::from("hello");

    change(&mut s);

    fn change(some_string: &mut String) {
        some_string.push_str(", world");
    }

    println!("s is {}", s); // s is hello, world
}

声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

限制

  • 同一作用域,特定数据只能有一个可变引用
  • 可变引用与不可变引用不能同时存在
  • 引用必须总是有效的
fn main() {
    // 可变引用同时只能存在一个
    let mut s = String::from("hello");

    let a1 = &mut s;
    let a2 = &mut s; // 报错 cannot borrow `s` as mutable more than once at a time

    println!("{}, {}", a1, a2);


    // 可变引用与不可变引用不能同时存在
    let mut s = String::from("hello");

    let b1 = &s; // 没问题
    let b2 = &s; // 没问题
    let b3 = &mut s; // 报错 cannot borrow `s` as mutable because it is also borrowed as immutable
    println!("{}, {}, and {}", b1, b2, b3);

    引用必须是有效的

    let reference_to_nothing = dangle();
    fn dangle() -> &String { // dangle 返回一个字符串的引用

        let s = String::from("hello"); // s 是一个新字符串

        &s // 报错 missing lifetime specifier
    }
    // 这里 s 离开作用域并被丢弃。其内存被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String。
}

6、数据类型

Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。

基本类型

基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成:

  • 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 - (f32, f64)、以及有理数、复数
  • 字符串:字符串字面量和字符串切片 &str
  • 布尔类型: true和false
  • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型: 即 () ,其唯一的值也是 ()

整型(Integer)

位长度(bit)有符号整型无符号整型备注
8i8u8
8i16u16
8i32u32
8i64u64
8i128u128
8isizeusize取决于所运行的目标平台,32位架构处理器则为32位

最大值 std::u128::MAX
最小值 std::i128::MAX

注:整数中间可以使用 _ 进行断句,从而更容易判断一个数值大小。

浮点数(Floating-Point)

Rust支持32位(f32)和64(f64)位浮点数,默认情况下xx.0表示64位浮点数,若定义时没有指定类型,则默认为32位浮点数

let a=2; // a被定义为i32

Rust不支持自增自减运算,理由是“这两个运算符需要使用者搞清楚先后问题,减弱了代码可读性,以及使用者对变量改变的意识能力。”

布尔类型(bool)

bool类型的值只能为 true 或 false

let a:bool = true;
let b:bool = 0; // 报错 mismatched types

字符类型 (char-UTF-8)

Rust的char类型大小为4字节,代表Unicode标量值。所以可以支持中、日、韩文等非英文字符,甚至表情符号和零宽度空格。

char 使用单引号包裹

由于中文文字编码有GBK和UTF-8两种,所以编程中使用中文字符可能导致乱码,这是因为源程序和命令行的文字编码不同,所以在Rust中必须使用UTF-8编码。

单元类型

单元类型就是 () ,对,你没看错,就是 () ,唯一的值也是 ()

main 函数就返回这个单元类型 (),你不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。

例如常见的 println!() 的返回值也是单元类型 ()。

再比如,你可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 这种用法和 Go 语言的 struct{} 类似,可以作为一个值用来占位,但是完全不占用任何内存。

函数

  • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
  • 每个函数参数都需要标注类型

详细参见:函数

复合类型

字符串&字符串对象

字符串: Rust核心内置的数据类型&str,字符串字面量。 字符串对象: Rust标准库中的一个公开的pub结构体。

  • &str let s="abc"; // &str

  • String : 字符串对象。

字符串对象方法

创建
// String::new() 创建一个新的空字符串,静态方法
let s1=String::new();

// String::from() 从具体的字符串字面量创建字符串对象
let s2=String::from("abc");
println!("s1 is {},s2 is {}",s1,s2); // 输出 s1 is ,s2 is abc
添加
  • push_str() 在字符串的末尾追加字符串字面量 &str
  • push() 在原来的字符串末尾追加单个字符 char
  • 这两个方法都是在原有的字符串上追加,并不会返回新的字符串。
  • 由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。
let mut s3 = String::new();
s3.push_str("abc"); // s3="abc"
s3.push('d'); // s3="abcd"
插入
  • insert() 方法插入单个字符 char
  • insert_str() 方法插入字符串字面量 &str
  • 这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。
  • 由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。
let mut s3 = String::from("abc");
s3.insert(1, 'd'); // s3="adbc"
s3.insert_str(2, "eee"); // s3="adeeebc"
替换
  • replace() 该方法可适用于 String 和 &str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
let str = String::from("do you wanna build a snow man!");
let str_replaced = str.replace("build", "distro");
dbg!(str_replaced); 
// dbg!:打印并返回给定表达式的值。\main.rs:4] str_replaced = "do you wanna distro a snow man!"
  • replacen() 该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串
let str = String::from("di li li li di li li li di da!");
let str_replaced = str.replacen("li", "ha", 1);
let str_replaced1 = str.replacen("li", "ha", 6);
dbg!(str_replaced); // [src\main.rs:5] str_replaced = "di ha li li di li li li di da!"
dbg!(str_replaced1); // [src\main.rs:6] str_replaced1 = "di ha ha ha di ha ha ha di da!"
  • replace_range() 该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰
let mut string_replace_range = String::from("I like rust!"); 
string_replace_range.replace_range(7..8, "R"); 
dbg!(string_replace_range); // string_replace_range = "I like Rust!"
删除
  • 与字符串删除相关的方法有 4 个,他们分别是 pop()remove()truncate()clear()。这四个方法仅适用于 String 类型。
/*
 pop —— 删除并返回字符串的最后一个字符
 该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。
*/
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();
let p2 = string_pop.pop();
dbg!(p1); // p1 = Some(  '!', )
dbg!(p2); // p1 = Some(  '文', )
dbg!(string_pop); // string_pop = "rust pop 中"

/*
 remove —— 删除并返回字符串中指定位置的字符
 该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。
 remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
 在Rust中使用UTF-8,一个汉字字节数为3字节
*/
let mut string_remove = String::from("测试remove方法");
println!("string_remove 占 {} 个字节", string_remove.len()); // 输出 string_remove 占 18 个字节
string_remove.remove(0); // 删除第一个汉字
// string_remove.remove(1); 该行代码会报错
string_remove.remove(9); // 删除第二个汉字
dbg!(string_remove); // 输出 string_remove = "试remove法"

/*
 truncate —— 删除字符串中从指定位置开始到结尾的全部字符
 该方法是直接操作原来的字符串。无返回值。
 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
*/
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate); // 输出 string_truncate = "测"

/*
 clear —— 清空字符串
 该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。
*/
let mut string_clear = String::from("string clear");
string_clear.clear();
dbg!(string_clear); //  输出 string_clear = ""
连接

++=要求左边的参数为String,右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时, 相当于调用了 std::string 标准库中的 add() 方法, 这里 add() 方法的第二个参数是一个引用的类型。 因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。 + 和 += 都是返回一个新的字符串。所以变量声明可以不需要 mut 关键字修饰

切片引用类型:引用集合中部分连续的元素序列,而不是引用整个集合。
eg:对于字符串而言,切片就是对 String 类型中某一部分的引用,它看起来像这样:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
创建切片的语法,使用方括号包括的一个序列: [开始索引..终止索引] ,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置

let string_append = String::from("hello ");
let string_rust = String::from("rust");// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!";
result += "!!!";

println!("连接字符串 + -> {}", result); // 连接字符串 + -> hello rust!!!!

add() 使用方法:fn add(self, s: &str) -> String
由于这里使用的add()方法涉及到所有权转移的概念,这里简单提一下。

let s1 = String::from("hello,");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 这里,s1的所有权被转移走了,因此后面不能再使用s1
assert_eq!(s3,"hello,world!"); // assert_eq 判断两个表达式返回值是否相等,不相等时当前线程会直接 panic
// 下面的语句如果去掉注释,就会报错
// println!("{}",s1);

所有权转移这里可以理解为,在Rust中需要分配内存或某种形式资源的类型的值只能被b被一个变量拥有,当它被转移到其他变量时,那么之前的变量便不再拥有它。
具体的可以去了解所有权转移

元组

元组使用对 () 包裹的一组数据,可以包含不同种类的数据。

let a: (i32, f64, u8) = (2, 2.1, 1);
// a.0 等于 1
// a.1 等于 2.1 // a.2 等于 1
let (x, y, z) = a; // y 等于 2.1

结构体

类似其他语言中的object Rust中的结构体由多种类型组合而成。这点与元组相同,但与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。

结构体定义

struct User {
    is_check: bool,
    name: String,
    check_count: u64,
}

创建结构体实例

初始化实例时,每个字段都需要进行初始化
初始化时的字段顺序不需要和结构体定义时的顺序一致

 let user1 = User {
     is_check: true,
     name: String::from("user1"),
     check_count: 1,
 };

访问结构体属性

let mut user1 = User {
     is_check: true,
     name: String::from("user1"),
     check_count: 1,
};

user1.name=String::from("user1_copy");

函数式实例化结构体

struct User {
    is_check: bool,
    name: String,
    check_count: u64,
}

fn build_user( name: String) -> User {
    User {
        name,
        is_check: true,
        check_count: 1,
    }
}

let user1 = build_user(String::from("user1"));
println!("{} {} {}",user1.name,user1.is_check,user1.check_count);

let user2 = build_user(String::from("user2"));
println!("{} {} {}",user2.name,user2.is_check,user2.check_count);

结构体更新语法

let user3 = User {
    name:String::from("user3"),
    ..user1
};

枚举类型(拥有同一类共性的集合)

enum Majiang { 
    Wan, 
    Bing, 
    Tiao, 
}
fn main() { 
    let c1 = Majiang::Wan(5); 
    let c2 = Majiang::Bing(9); 
}

枚举类型: 枚举类型是一个类型,它会包含所有可能的枚举成员——麻将有 万、饼、条三种基础牌,即枚举类型

枚举值: 枚举值是该类型中的具体某个成员的实例,万 牌中的一万,是万类型麻将中的一个实例,即枚举值

任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move{x:1,y:1};
    let m3 = Message::ChangeColor(255,255,0);
}

该枚举类型代表一条消息,它包含四个不同的成员:

  • Quit 没有任何关联数据
  • Move 包含一个匿名结构体
  • Write 包含一个 String 字符串
  • ChangeColor 包含三个 i32

当然,我们也可以用结构体的方式来定义这些消息:


struct QuitMessage; // 单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。

而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。

Option 枚举用于处理空值

在其它编程语言中,往往都有一个 null 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整型的零值是 0),也就是不存在值。当你对这些 null 进行操作时,例如调用一个方法,就会直接抛出null 异常,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 null 空值。

Tony Hoare, null 的发明者,曾经说过一段非常有名的话

我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。

尽管如此,空值的表达依然非常有意义,因为空值表示当前时刻变量的值是缺失的。有鉴于此,Rust 吸取了众多教训,决定抛弃 null,而改为使用 Option 枚举变量来表述这种结果。

Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下:


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

其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。

Option<T> 枚举是如此有用以至于它被包含在了 prelude(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 Some 和 None 也是如此,无需使用 Option:: 前缀就可直接使用 Some 和 None。总之,不能因为 Some(T) 和 None 中没有 Option:: 的身影,就否认它们是 Option 下的卧龙凤雏。

再来看以下代码:


let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

如果使用 None 而不是 Some,需要告诉 Rust Option<T> 是什么类型的,因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型。

当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option<T> 为什么就比空值要好呢?

简而言之,因为 Option<T> 和 T(这里 T 可以是任何类型)是不同的类型,例如,这段代码不能编译,因为它尝试将 Option<i8>(Option<T>) 与 i8(T) 相加:


let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

如果运行这些代码,将得到类似这样的错误信息:

error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
 -->
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + std::option::Option<i8>`
  |

很好!事实上,错误信息意味着 Rust 不知道该如何将 Option<i8> 与 i8 相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值,我们可以放心使用而无需做空值检查。只有当使用 Option<i8>(或者任何用到的类型)的时候才需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。

换句话说,在对 Option<T> 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:期望某值不为空但实际上为空的情况。

不再担心会错误的使用一个空值,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option<T> 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option<T> 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。

那么当有一个 Option<T> 的值时,如何从 Some 成员中取出 T 的值来使用它呢?Option<T> 枚举拥有大量用于各种情况的方法:你可以查看它的文档。熟悉 Option<T> 的方法将对你的 Rust 之旅非常有用。

总的来说,为了使用 Option<T> 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 Some(T) 值时运行,允许这些代码使用其中的 T。也希望一些代码在值为 None 时运行,这些代码并没有一个可用的 T 值。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。

这里先简单看一下 match 的大致模样,在模式匹配中,我们会详细讲解:


fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

plus_one 通过 match 来处理不同 Option 的情况。

数组

数组元素只允许同一类型的数据。

固定数组Array

长度固定,属于Rust的基本类型,存储在栈中。

数组定义方式
  • 数组元素为基本类型
    // 方式一:常用定义方式,通过赋值推导类型
    let a = [1, 2, 3, 4, 5];
    // 方式二:定义类型和长度[类型; 长度]
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    // 方式三:某个值X重复出现 N 次的数组 [X;N]
    let a = [3; 5]; // 5个3组成的数组
  • 数组元素非基本类型 前两种定义方式依然可行,但是会显得冗长,可以调用std::array::from_fn定义重复元素数组
let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!"));
数组取值
  • 获取元素
  let a = [1,2,3];
  let first = a[0]; // 获取a数组第一个元素
  • 获取切片
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3]; // [2, 3]

动态数组Vector

长度不固定,属于rust中的集合类型,存储在堆内存中

数组定义方式
  • 单独定义一个动态数组不赋值时,Rust编译器无法后去类型信息,所以需要声明类型
let v: Vec<i32> = Vec::new();
  • 声明动态数组并赋值时,Rust编译器可以根据赋值部分推导数组类型
let mut v = Vec::new();
v.push(1);
  • 宏定义 vec! 定义加初始化
let v = vec![1, 2, 3];
数组取值

下标取值和.get

let v = vec![1, 2, 3, 4, 5];
let v1 = &v[0]; // 返回元素值 1
let v2 = v.get(0); // 返回Option<&T> some(1)

7、流程控制

if

  • if 语句块是表达式,即if的返回值可以作为值赋值给变量 let a = if condition { 5 } else { 4 }
  • 用 if 来赋值时,要保证每个分支返回的类型一样

if...else...

if condition {
// todo
} else {
// todo
}

else if

  if condition1 {
        println!("condition1");
    } else condition2 {
        println!("condition2");
    } else condition3 {
        println!("condition3");
    } else {
        println!("default");
    }

for

for...in

for 元素 in 集合 {
  // todo
}
let a=[1,2,3,4]
// 下标遍历
for i in 0..a.len(){ // ..左闭右开区间  ..= 左闭右闭区间
  println!("{}", a[i]); // 下标遍历,每次需要做边界检查,且非连续可能在两次遍历间数组发生变化
}
// 集合遍历
for i in a{
  println!("{}", a[i]); // 无需边界检查,连续访问不存在下标检查的风险
}

使用方法等价使用方式所有权
for item in collectionfor item in IntoIterator::into_iter(collection)转移所有权
for item in &collectionfor item in collection.iter()不可变引用
for item in &mut collectionfor item in collection.iter_mut()可变引用

while

while condition { // condition为false时跳出循环
  // todo
}

loop 无限循环

loop{
  // todo
  if(condition){
    break 表达式(返回值);
  }
}

break&continue

  • break: 跳出循环,只有在loop中 break后可以跟一个返回值
  • continue: 跳出当次循环进去下次循环

8、模式匹配

match

match本身是一个表达式可以用来赋值 let res = match语句

match通用语法

match的匹配时穷尽匹配,即要考虑所有情况_=>代表其他默认情况

// 模式与target匹配时,进入相应的分支
match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

这里的匹配不是比较值是否相同而是匹配模式,通过target传入的值与一系列的模式进行比较,下面通过几个例子理解一下

    // 枚举
    enum Direction {
        Up,
        Down,
        Left,
        Right,
    }

    fn look_at() {
        let dire = Direction::Up;
        match dire {
            Direction::Down => println!("Down"),
            Direction::Left | Direction::Right => {
                println!("Left or Right");
            }
            _ => println!("West"),
        };
    }
    look_at;

    // 复杂类型枚举
    enum Action {
        Say(String),
        MoveTo(i32, i32),
        ChangeColorRGB(u16, u16, u16),
    }

    fn do_action() {
        let action = Action::Say("Hello Rust".to_string());
        match action {
            Action::Say(s) => {
                // 可以看出match匹配的是模式而不是值,这里匹配到Say()类型即进入当前分支
                println!("{}", s);
            }
            Action::MoveTo(x, y) => {
                println!("point from (0, 0) move to ({}, {})", x, y);
            }
            Action::ChangeColorRGB(r, g, _) => {
                println!(
                    "change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                    r, g,
                );
            }
        }
    }
    do_action;

    // Option 当我们把模式精确到一个值时,看起来就是在比较值了
    let v = Some(3u8);
    match v {
        Some(3) => println!("three"),
        _ => (),
    }

if let

当只需要匹配一个条件,其他条件忽略的情况下可以使用if let匹配

if let Some(3) = v {
    println!("three");
}

matchs! 宏

将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false。 例如,有一个动态数组,里面存有以下枚举:

enum MyEnum {
    Foo,
    Bar
}
fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

现在如果想对 v 进行过滤,只保留类型是 MyEnum::Foo 的元素,你可能想这么写: v.iter().filter(|x| x == MyEnum::Foo); 但是,实际上这行代码会报错,因为你无法将 x 直接跟一个枚举成员进行比较。好在,你可以使用 match 来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用 matches!: v.iter().filter(|x| matches!(x, MyEnum::Foo)); 很简单也很简洁,再来看看更多的例子:

let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));

匹配守卫

匹配守卫(match guard)是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。

let num = Some(10);
match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}
// 输出结果: 10

Some(10),通过第一个分支的Some(x),但是后面的守卫匹配为假,所以进入第二个分支。

9、方法

方法定义 impl

pub struct Human {
    height: i32,
    weight: i32,
}
impl Human {
    pub fn new(h: i32, w: i32) -> Human {
        Human {
            height: h,
            weight: w,
        }
    }
    pub fn height(&self) -> i32 {
        self.height
    }
    pub fn weight(&self) -> i32 {
        self.weight
    }
}

self、&self、&mut self

上面的代码中我们使用&self代替了Human结构体,这里&self等价于self:&self,在一个 impl 块内,Self 指代被实现方法的结构体类型

  • self 表示 Human 的所有权转移到该方法中,这种形式用的较少
  • &self 表示该方法对 Human 的不可变借用
  • &mut self 表示可变借用

10、泛型 Generics

  • 在 Rust 中,泛型参数的名称你可以任意起,但是出于惯例,我们都用 T ( T 是 type 的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。
  • 同一功能的函数需要处理不同类型参数时,可以使用泛型来泛指这些类型

// 使用泛型T 标识不同数据类型的参数a、b
fn add<T>(a:T, b:T) -> T {
    a + b
}

fn main() {
    println!("add i8: {}", add(2i8, 3i8));
    println!("add i32: {}", add(20, 30));
    println!("add f64: {}", add(1.23, 1.23));
}

但这里运行是会报错的,因为 T 可以是任何类型,但不是所有的类型都能进行比较,所以需要给泛型添加一个类型限制。这里可以使用std::ops::Add<Output = T> 特征(Trait)对 T 进行限制,特征在下一节会详细介绍,现在你只要理解,该特征的目的就是让类型实现可相加的功能。

fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
    a + b
}

使用场景

结构体中使用泛型

结构体中的字段类型也可以用泛型来定义,下面代码定义了一个坐标点 Point,它可以存放任何类型的坐标值

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

这里有两点需要特别的注意:

  • 提前声明,跟泛型函数定义类似,首先我们在使用泛型参数之前必需要进行声明 Point,接着就可以在结构体的字段类型中使用 T 来替代具体的类型
  • x 和 y 是相同的类型 如果想要x、y是不同类型的可以使用不同的泛型参数
struct Point<T,U> {
    x: T,
    y: U,
}
fn main() {
    let p = Point{x: 1, y :1.1};
}

枚举中使用泛型

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

enum Result<T, E> {
  Ok(T),
  Err(E),
}

方法中使用泛型

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

const泛型(Rust 1.51 版本引入的重要特性)

在之前的泛型中,可以抽象为一句话:针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?可能很多同学感觉很难理解,值怎么使用泛型?不急,我们先从数组讲起。

在数组那节,有提到过很重要的一点:[i32; 2] 和 [i32; 3] 是不同的数组类型,比如下面的代码:

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);
}

运行后报错:

error[E0308]: mismatched types // 类型不匹配
--> src/main.rs:10:19
 |
10 |     display_array(arr);
 |                   ^^^ expected an array with a fixed size of 3 elements, found one with 2 elements
                        // 期望一个长度为3的数组,却发现一个长度为2的

结合代码和报错,可以很清楚的看出,[i32; 3] 和 [i32; 2] 确实是两个完全不同的类型,因此无法用同一个函数调用。 首先,让我们修改代码,让 display_array 能打印任意长度的 i32 数组:

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);
}

很简单,只要使用数组切片,然后传入 arr 的不可变引用即可。 接着,将 i32 改成所有类型的数组:

fn display_array<T: std::fmt::Debug>(arr: &[T]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(&arr);

    let arr: [i32;2] = [1,2];
    display_array(&arr);
}

也不难,唯一要注意的是需要对 T 加一个限制 std::fmt::Debug,该限制表明 T 可以用在 println!("{:?}", arr) 中,因为 {:?} 形式的格式化输出需要 arr 实现该特征。

通过引用,我们可以很轻松的解决处理任何类型数组的问题,但是如果在某些场景下引用不适宜用或者干脆不能用呢?你们知道为什么以前 Rust 的一些数组库,在使用的时候都限定长度不超过 32 吗?因为它们会为每个长度都单独实现一个函数,简直。。。毫无人性。难道没有什么办法可以解决这个问题吗?

好在,现在咱们有了 const 泛型,也就是针对值的泛型,正好可以用于处理数组长度的问题:

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
}

如上所示,我们定义了一个类型为 [T; N] 的数组,其中 T 是一个基于类型的泛型参数,这个和之前讲的泛型没有区别,而重点在于 N 这个泛型参数,它是一个基于值的泛型参数!因为它用来替代的是数组的长度。

N 就是 const 泛型,定义的语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize。

在泛型参数之前,Rust 完全不适合复杂矩阵的运算,自从有了 const 泛型,一切即将改变。

11、特征(trait)

特征:定义一个可以被共享的行为,只有实现了该行为的特征,才能使用该行为。

特征定义

定义特征:把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。 例如人类和动物,他们具有一些相同的行为,比如睡觉。

pub trait Sleep { fn sleep_place(&self) -> String; }

为类型实现特征

特征只定义特征中的行为表象或者再定义以下行为的默认内容,因此我们需要为类型实现具体的特征,定义行为具体是怎么样的。

pub trait Sleep {
    // fn sleep(&self) -> String;
    // 特征中的行为具有默认内容
    fn sleep(&self) -> String {
        format!("I want go to sleep.")
    }
}

pub struct People {
    pub sleep_time: String,  // 睡觉时间
    pub sleep_place: String, // 睡觉地点
}

impl Sleep for People {
    fn sleep(&self) -> String {
        format!(
            "People sleep in {}, at {}",
            self.sleep_place, self.sleep_time
        )
    }
}

pub struct Animal {
    pub sleep_time: String,  // 睡觉时间
    pub sleep_place: String, // 睡觉地点
}

impl Sleep for Animal {
    fn sleep(&self) -> String {
        format!(
            "animal sleep in {}, at {}",
            self.sleep_place, self.sleep_time
        )
    }
}

pub struct Myself {
    pub sleep_time: String,  // 睡觉时间
    pub sleep_place: String, // 睡觉地点
}

impl Sleep for Myself {
    // 默认实现使用Sleep特征的默认行为
}

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

默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。

pub trait Sleep {
    fn sleep_place(&self) -> String;
    fn sleep(&self) -> String {
        format!("sleep_place is {}", self.sleep_place())
    }
}
pub struct People {
    pub sleep_time: String,  // 睡觉时间
    pub sleep_place: String, // 睡觉地点
}

impl Sleep for People {
    fn sleep_place(&self) -> String {
        String::from("bed")
    }
}

let people = People{sleep_time:String::from("morning"),sleep_place:String::from("bed")};

println!("result is {}", people.sleep());

使用特征作为函数参数

以下代码中的 weibo post都实现了summary特征,有点写不动了,做了一下搬运工O(∩_∩)O哈哈~

特征约束(trait bound)

虽然 impl Trait 这种语法非常好理解,但是实际上它只是一个语法糖:


pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

真正的完整书写形式如上所述,形如 T: Summary 被称为特征约束

在简单的场景下 impl Trait 这种语法糖就足够使用,但是对于复杂的场景,特征约束可以让我们拥有更大的灵活性和语法表现能力,例如一个函数接受两个 impl Summary 的参数:


pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

如果函数两个参数是不同的类型,那么上面的方法很好,只要这两个类型都实现了 Summary 特征即可。但是如果我们想要强制函数的两个参数是同一类型呢?上面的语法就无法做到这种限制,此时我们只能使特征约束来实现:


pub fn notify<T: Summary>(item1: &T, item2: &T) {}

泛型类型 T 说明了 item1 和 item2 必须拥有同样的类型,同时 T: Summary 说明了 T 必须实现 Summary 特征。

多重约束

除了单个约束条件,我们还可以指定多个约束条件,例如除了让参数实现 Summary 特征外,还可以让参数实现 Display 特征以控制它的格式化输出:


pub fn notify(item: &(impl Summary + Display)) {}

除了上述的语法糖形式,还能使用特征约束的形式:


pub fn notify<T: Summary + Display>(item: &T) {}

通过这两个特征,就可以使用 item.summarize 方法,以及通过 println!("{}", item) 来格式化输出 item

Where 约束

当特征约束变得很多时,函数的签名将变得很复杂:


fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

严格来说,上面的例子还是不够复杂,但是我们还是能对其做一些形式上的改进,通过 where


fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

使用特征约束有条件地实现方法或特征

特征约束,可以让我们在指定类型 + 指定特征的条件下去实现方法,例如:


use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

cmp_display 方法,并不是所有的 Pair<T> 结构体对象都可以拥有,只有 T 同时实现了 Display + PartialOrd 的 Pair<T> 才可以拥有此方法。 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过特征约束进行了约束。

也可以有条件地实现特征, 例如,标准库为任何实现了 Display 特征的类型实现了 ToString 特征:


impl<T: Display> ToString for T {
    // --snip--
}

我们可以对任何实现了 Display 特征的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display


let s = 3.to_string();

函数返回中的 impl Trait

可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征:


fn returns_summarizable() -> impl Summary {
    Weibo {
        username: String::from("sunface"),
        content: String::from(
            "m1 max太厉害了,电脑再也不会卡",
        )
    }
}

因为 Weibo 实现了 Summary,因此这里可以用它来作为返回值。要注意的是,虽然我们知道这里是一个 Weibo 类型,但是对于 returns_summarizable 的调用者而言,他只知道返回了一个实现了 Summary 特征的对象,但是并不知道返回了一个 Weibo 类型。

这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。例如,闭包和迭代器就是很复杂,只有编译器才知道那玩意的真实类型,如果让你写出来它们的具体类型,估计内心有一万只草泥马奔腾,好在你可以用 impl Iterator 来告诉调用者,返回了一个迭代器,因为所有迭代器都会实现 Iterator 特征。

但是这种返回值方式有一个很大的限制:只能有一个具体的类型,例如:


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",
            ),
        }
    }
}

以上的代码就无法通过编译,因为它返回了两个不同的类型 Post 和 Weibo

`if` and `else` have incompatible types
expected struct `Post`, found struct `Weibo`

报错提示我们 if 和 else 返回了不同的类型。如果想要实现返回不同的类型,需要使用特征对象