目录

---------

本笔记大部分来自 rust 圣经,笔者进行了一部分简化和自己的理解

表达式和语句

Rust 中,表达式会进行求值,然后返回一个值,例如 5 + 6 的返回值是 11,因此它也是表达式

利用表达式,可以做到其他语言无法实现的东西:

Rust
fn main() {
  let y = {
    let x = 3;
    x + 1 // 这里如果加 ;那么就是语句了
  }
  println!(y); // 4
}

而语句会执行一些操作,但是不会返回值,例如:

Rust
fn sum_num(x: i32, y: i32) {
  let x = x + 1; // 语句
  let y = y + 5; // 语句
  x + y // 表达式
}

函数

rust 的函数大致结构:

Rust
fn add_sum(i: i32, j: i32) -> i32 {
  i + j
}

看起来很简单,但是 rust 函数要求有以下几点:

特殊返回类型

  1. 函数没有返回值

rust 中,如果函数没有返回值,会默认返回一个零长度的元组 ()

例如下面的 report 函数隐式返回了一个 ():

Rust
use std::fmt::Debug;

fn report<T: Debug>(item: T) {
  println!("{:?}", T);
}

显示的返回 ()

Rust
fn clear(text: &mut String) -> () {
  *text = String::from("")
}

发散函数

永不返回!,当时用 ! 作为返回类型的时候,表示该函数永不反回,可以用来作为程序崩溃的函数,例如:

Rust
fn exit_end() -> ! {
  panic("end of road!");
}

或者无限循环,因为循环永远不跳出,因此函数也永不反回!

Rust
fn forever() -> ! {
  loop {
    // ...
  };
}

所有权

理解所有权之前,我们需要了解的区别:

由上可知,堆是一种缺乏组织的的数据结构,因此入栈是比堆上分配内存快的多,因为入栈时操作系统无需分配新的空间,只需要将数据放入栈顶即可。

同时,借助 CPU 高速缓存,读取速度方面,栈可以比堆的访问速度块 10 倍以上(栈数据可以存储在 CPU 高速缓存中,而对数据只能存储在内存中)

跟踪堆上的数据何时分配和释放是非常重要的,否则会产生内存泄漏,rust 的所有权就是为了更加安全的使用堆数据。

所有权原则

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

变量作用域

和大多数语言一样,变量是具有作用域的,例如:

Rust
{
  let s = "hello"; // s 声明后开始有效
} // 作用域结束,s 不再有效

数据交互

先看两个类似的代码:

Rust
let x = 5;
let y = x;
println!(x);
Rust
let s1 = String::from("hello");
let s2 = s1;
println!"{}", s1);

虽然两者十分类似,但是第二段代码编译会出问题的,因为 String 类型是分配在堆中的,我们将 s1 的所有权转让给了 s2,此时 rust drop 掉了 s1s2 是无效引用。

第一段代码不会报错,是因为整数是基本类型,都被存放在栈中,将 x 赋值给 y,便是进栈的过程。

再来看另一段代码:

Rust
fn main() {
  let x = "hello world";
  let y = x;
  println!("{}, {}", x, y);
}

这段代码却不会报错,是因为 x 的类型是 &str,只是存储在二进制的字符串 "hello world",并没有持有所有权

深拷贝

rust 永远不会自动创建数据的“深拷贝”,但是如果我们确实需要 String 堆上的数据,而不仅仅是站上的数据,可以使用一个叫做 clone 的方法:

Rust
let s1 = String::from("hello");
let s2 = s1.clone();
println!"s1 = {}, s2 = {}", s1, s2);

浅拷贝

浅拷贝只发生在栈上,因此性能更好,例如:

Rust
let x = 5;
let y = x;

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

这样既产生了类似深拷贝的效果,也没有报所有权的错误,原因是像整数类型在编译时是已知大小的,会被存放在栈上。

另外,rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型,如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,判断变量是否具有 Copy 特征的通用规则:

任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy。如下是一些 Copy 的类型:

函数传值与返回

将变量传递给函数一样会发生所有权的转移,或者赋值,例如:

Rust
fn main() {
  let s = String::from("hello");
  
  takes_ownship(s); // s 的值移动到函数里
  // s 不再有效
  
  let x = 5;
  
  makes_copy(x); // x 移动到这个函数里
  // i32 是可 Copy 的,所以 x 继续有效
}

fn take_ownship(str: String) {
  println!("{}", str);
} // str 移除作用域,drop 掉其占用的内存

fn makes_copy(interger: i32) {
  println!("{}", interger);
} // interger 移除作用域

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

Rust
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) 来实现,获取变量的引用,称之为借用

引用与解引用

常规引用是一个指针类型,指向对象存储的内存地址,和 c 语言指针操作基本一致:

Rust
fn main() {
  let x = 5;
  // y 是 x 的一个引用
  let y = &x;
  
  assert_eq!(5, x);
  // *y 便是解引用
  assett_eq!(5, *y);
}

不可变引用

我们可以通过引用的方式,不转移所有权

Rust
fn main() {
  let s1 = String::from("hello");
  // 通过 &s1 引用的方式,不转移 s1 的所有权
  let len = cal_len(&s1);
	// s1 还可以继续使用
}

// 引用类型,需要在类型前加 &
fn cal_len(s: &String) -> usize {
  s.len()
}

& 符号即是引用,允许我们只使用值,而不获取所有权

可变引用

不可变引用是无法修改的,例如以下代码会报错:

Rust
fn main() {
  let s1 = String::from("hello");
  // 通过 &s1 引用的方式,不转移 s1 的所有权
  let len = change_str(&s1);
	// s1 还可以继续使用
}

// 引用类型,需要在类型前加 &
fn change_str(s: &String) {
  s.push_str("world");
}

我们只需要几个小调整,就可以修正错误:

  1. 声明可变类型,关键字:let mut;
  2. 创建可变引用,例如:&mut s1;
  3. 修改函数参数为可变引用类型,例如 s1: &mut String
Rust
fn main() {
  let mut s1 = String::from("hello");
  // 通过 &s1 引用的方式,不转移 s1 的所有权
  let len = change_str(&mut s1);
	// s1 还可以继续使用
}

// 引用类型,需要在类型前加 &
fn change_str(s: &mut String) {
  s.push_str("world");
}

注意点

  1. 可变引用同时只能存在一个

同一个作用域,特定数据只能有一个可变引用,例如以下代码是错误的:

Rust
let mut s = String::from("hello");
let s1 = &mut s;
let s2 = &mut s;

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

这种限制的好处使得 rus 在编译器就避免数据竞争

  1. 可变引用和不可变引用不能同时存在

这句话很好理解,不可变引用正在借用数据,突然被一个可变引用修改值了,那肯定是不合理的,例如以下错误代码:

Rust
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

不过随着 rust 的编译器优化,以下代码在新的编译器是可以通过的:

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
  // 新编译器中,r3作用域在这里结束

悬垂引用

悬垂引用(Dangling References)也叫做悬垂指针,意思是指针指向某个值后,这个值被释放掉了,而指针仍然存在:

Rust
fn main() {
  let ref_to_nothing = dangle();
}

fn dangle() -> &String {
  let s = String::from("hello");
  &s
}

借用规则总结

元组

元组是由多种类型组合到一起形成的,元组的长度和其中的元素顺序都是固定的,例如:

Rust
fn main() {
  let tup: (i32, f64, u8) = (500, 6.4, 1);
}

解构赋值

和 js 一样,rust 也可以使用解构赋值,不同的是:元组使用的是 () 小括号解构

Rust
fn main() {
  let tup: (i32, f64, u8) = (500, 6.4, 1);
  let (x, y, z) = tup;
}

访问元素

元组可以通过 . 运算符访问元素:

Rust
fn main() {
  let tup: (i32, f64, u8) = (500, 6.4, 1);
  let first = tup.0;
  let second = tup.1;
}

单元类型

单元类型就是 (),虽然看起来没啥用,但是 main 函数的返回值就是单元类型 (),发散函数的返回值也是 (),常见的例如 println! 的返回值也是 ()

还可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key

字符串

rust 中的字符串和其他语言差异很大,有两种类型:&strString,其中:

同时,两种类型并不相融,例如以下报错代码:

Rust
fn main() {
  // name 的类型是 &str
  let name = "hello world";
  greet(name);
}

fn greet(s: String) {
  println!("你好, {}", s);
}

String 类型转为 &str 类型,只需要取引用即可,以下三个方法效果相同:

Rust
let s = String:from("hello, world!");
let str = &s;
let str = &s[..];
let str = s.as_str();

字符串索引

在其他语言中,通过索引去访问字符串的某个字符或者字串是很正常的行为,但在 rust 中不允许,例如以下错误代码:

Rust
let s1 = String::from("hello");
let h = s1[0];

这是因为在 rust 中,字符串的底层数据格式是 [u8],一个字节数组,对于英文来说,一个字母在 utf-8 中仅占一个字节,而对于中文来说占了三个字节

还有一个原因导致 rust 不允许使用索引字符串,因为索引操作我们期望他的时间复杂度是 O(1),但对于 String 类型来说,时间复杂度无法保证

切片

切片允许 rust 引用集合中部分连续的元素序列,而不是整个集合:

Rust
let s = String::from("hello world");

let hello = &s[0..5]; // 可省略为 &s[..5]
let world = &s[6..11]; // 可省略为 &s[6..]

如果我们想要截取完整的 String 切片:

Rust
let s = String::from("helo");
let len = s.len();

let full = &s[0..len];
let full = &s[..]

对字符串使用切片时要小心不同字符的边界位置,因为切片要求的索引必须落在字符之间,也就是 utf-8 的边界,例如中文在 utf-8 中占用 3 个字节,下面的代码就不对:

Rust
let s = "你好世界";
let a = &s[0..2];

因为我们直接去了前两个字节,但是一个中文占三个字节,如果是 &s[0..3],那么程序就不会崩溃,解决这个问题可以查看 utf-8 切片

另外,由于切片是对几何的部分引用,因此不仅仅字符串有切片,其他集合类型也有,例如数组:

Rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]

操作字符串

String 类型是可变字符串,使用 String::from 定义

追加(push)

使用实例的 push_str 或者 push,前者可以追加字符串字面量,后者追加字符 char,两者都会修改原来的字符串,并不会返回新字符串,另外需要注意字符串变量必须使用 mut 关键字修饰。

Rust
fn main() {
  let mut s = String::from("Hello ");
  s.push_str("rust");
  println!("{}", s); // Hello rust
  s.push('!');
  println!("{}", s); // Hello rust!
}

插入(insert)

使用实例的 insert_str() 或者 insert() 方法插入字符串字面量,前者插入字符串,后者插入字符。两者都会修改原来的字符串,并不会返回新字符串

Rust
fn main() {
  let mut s = String::from("Hello rust");
  s.insert(5, ',');
  println!("{}", s); // Hello, rust
}

同样插入的索引是根据字符占的字节来说的,如果索引不在 utf-8 边界也会报错,例如以下报错代码代码:

Rust
fn main() {
let mut s = String::from("你好 世界");
// 中文字符在 utf-8 中占 3 字节,索引 2 不是边界
s.insert(2, ',');
println!("{}", s); // Hello, rust
}

替换(Replace)

  1. replace

是用于 String&str 类型,该类型不会修改原来的字符串,而是返回一个新的字符串

Rust
let s = String::from("hello rust, hello rust");
let new_str = s.replace("rust", "world");
println!("{}", new_str); // hello world, hello world
  1. replacen

replace 一样,只不过接受第三个参数,限制替换的个数:

Rust
let s = String::from("hello rust, hello rust");
let new_str = s.replacen("rust", "world", 1);
println!("{}", new_str); // hello world, hello rust
  1. replace_range

该方法会修改原来的字符串,因此只适用于 String 类型,接受两个参数,第一个参数表示范围,第二个表示需要替换的字符串

Rust
let mut s = String::from("hello rust,");
s.replace_range(6.., "world");
println!("{}", s); // hello world

删除(Delete)

与字符串删除相关的方法有 4 个:pop, remove, truncateclear,四个方法均会修改原来的字符串,因此仅适用于 String 类型

  1. pop 删除并返回字符串的最后一个字符

删除字符串的最后一个字符,如果存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None

Rust
let mut s = String::from("he");
let ch: Option<char> = s.pop();
match ch {
    Some(str) => println!("{}", str),
    None => println!("没有字符了")
}
  1. remove 删除指定位置的字符

返回删除位置的字符串,只接受一个参数,表示该字符起始索引位置,remove 和其他字符串操作一样,都是按字节处理,因为位置不合法会报错:

Rust
let mut s = String::from("你好 世界");
s.remove(0);
println!("{}", s); // 好 世界
  1. truncate 删除字符串中从起始到指定位置的全部字符

该方法无返回值,也是按照字节来处理字符串的,如果参数位置不是合法的字符边界,则会发生错误

Rust
let mut s = String::from("hello world");
s.truncate(5);
println!("{}", s); // hello
  1. clear 清空字符串
Rust
let mut s = String::from("hello world");
s.clear();
println!("{}", s);

链接(Concatenate)

  1. 使用 + 或者 +=

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型,+ 是返回一个新字符串,所以变量声明可以不需要 mut 关键字修饰

Rust
let str_append = String::from("hello ");
let str_rust = String::from("rust");
let result = str_append + &str_rust;
let mut result = result + "!";
result += "!!!!";
println!("{}", result); // hello rust!!!!!

注:String + &str 会返回一个 String,所以后续还可以进行 + 操作

  1. 使用 format! 连接字符串

format! 适用于 String&str,其用法与 print! 类似,该方法返回一个 String 类型

Rust
let prefix = "hello";
let suffix = "world";
let full = format!("{} {}", prefix, suffix);
println!("{}", full); // hello world

字符串转义

我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符:

Rust
fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // \u 可以输出一个 unicode 字符
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );

    // 换行了也会保持之前的字符串格式
    // 使用\忽略换行符
    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

当然,在某些情况下,可能你会希望保持字符串的原样,不要转义:

Rust
fn main() {
    println!("{}", "hello \\x52\\x75\\x73\\x74");
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);

    // 如果字符串包含双引号,可以在开头和结尾加 #
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // 如果还是有歧义,可以继续增加,没有限制
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

操作 UTF-8 字符串

前文提到过 rust 不允许使用索引查找字符,而根据索引操作字符串也是以字节数为准

如果我们要以 Unicode 字符的方式遍历字符串,最好使用 chars 方法,例如:

Rust
for c in "你好世界".chars() {
  println!("{}", c);
}
// 你
// 好
// 世
// 界

以字节形式:

Rust
for c in "你好世界".bytes() {
  println!("{}", c);
}

输出:

PlainText
228
189
160
229
165
189
228
184
150
231
149
140

获取子串

想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 hello你好नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 crates.io 上搜索 utf8 来寻找想要的功能。

可以考虑尝试下这个库:utf8_slice

结构体

结构体 struct 是一个比较高级的数据结构,在其他语言中,他可能叫做 objectrecord

语法

例如下面代码:

Rust
struct User {
  username: String,
  email: String
};

结构体使用 struct,后面紧跟结构体名称

该结构体名称是 User,拥有 2 个字段:usernameemail,均为可变 String 类型

创建实例

Rust
let mut user = User {
  username: String::from("Plumbiu"),
  email: String::from("plumbiuzz@gmail.com"),
  active: bool,
};

注意点:

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

访问结构体字段

通过 . 操作符可以获取结构体实例内部的字段值,也可以修改它们:

Rust
let mut user = User {
  username: String::from("Plumbiu"),
  email: String::from("plumbiuzz@gmail.com"),
  active: true,
};
user.email = String::from("xxx@gamil.com");

如果要修改结构体实例的属性时,结构体一定是 let mut 可变的

简化结构体创建

我们可以创建一个构造函数,返回一个 User 实例:

Rust
fn build_user(email: String, username: String) -> User {
  email: email,
  username: username,
}

js 一样,上面的代码还可以简化,当结构体和参数同名时,可以直接简写:

Rust
fn build_user(email: String, username: String) -> User {
  email,
  username,
  active: true,
}

结构体更新语法

如果我们想以已有的 user1 实例去构建 user2,也可以使用类似 js 的展开运算符,只不过 rust 中少了一个点:..

Rust
let user2 = {
  email: String::from("xxx@qq.com"),
  ..user1
}

.. 必须在结构体尾部使用

上述代码中,我们将 user1 的参数赋值给了 user2 参数,会自动触发 rust 的所有权转移,但是只有 username 发生了所有权转移,因为 active 字段是具有 Copy 属性的

但这并不意味着 user1 无法被使用,只是发生所有权转移的字段无法使用,例如 user.active 仍然可以访问

结构体内存排列

先来看以下代码:

Rust
#[derive(Debug)]
 struct File {
   name: String,
   data: Vec<u8>,
 }

 fn main() {
   let f1 = File {
     name: String::from("f1.txt"),
     data: Vec::new(),
   };

   let f1_name = &f1.name;
   let f1_length = &f1.data.len();

   println!("{:?}", f1);
   println!("{} is {} bytes long", f1_name, f1_length);
 }

上面定义的 File 结构体在内存中的排列如下图所示: img

从图中可以清晰地看出 File 结构体两个字段 namedata 分别拥有底层两个 [u8] 数组的所有权(String 类型的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型。

该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段

元组结构体(Tuple Struct)

结构体必须要有名称,但是结构体字段可以没有名称,这种结构体成为元组结构体,例如:

Rust
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

单元结构体(Unit-like Struct)

如果我们定义一个类型,但是不关心该类型的内容,只关心行为时,可以使用单元结构体:

Rust
struct AlwaysEqual;

let subject = AlwaysEqual;

// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {

}

打印结构体信息

上面代码中,我们使用了 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!("{:?}", s) 的方式进行打印,如果不加就会报错

Rust
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
		// 使用 {} 格式化输出
    println!("rect1 is {}", rect1);
}

报错信息:

pow
PlainText
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

错误信息告诉我们,我们的结构体没有实现 Display 特征,如果我们想要使用 {} 来格式化输出,那对应的类型就必须实现 Display 特征

如果按上面的 note 所说,将 {} 换成 {:?},运行后的报错信息:

PlainText
error[E0277]: `Rectangle` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

此时编译器让我们实现 Debug 特征,并额外提示我们可以加 #[derive(Debug)]

Rust 默认不会为我们实现 Debug,为了实现,有两种方式可以选择:

后者的实现更为简单,因此正确的写法应该是:

Rust
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

上面的打印结果:

PlainText
rect1 is Rectangle { width: 30, height: 50 }

如果我们想要格式化更好的输出结果,可以使用 {:#?} 代替 {:?}:

PlainText
rect1 is Rectangle {
    width: 30,
    height: 50,
}

枚举

枚举(enumeration)允许我们通过列举可能的成员定义一个枚举类型,例如扑克牌花色:

Rust
enum PokerSuit {
  Clubs,
  Spades,
  Diamonds,
  Hearts,
}

一张扑克牌只能有四种花色,这种特性就非常适合枚举,因为枚举值只能是其中某一个成员。枚举是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例。

枚举值

创建 PokerSuit 枚举类型的两个成员实例:

Rust
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;

我们通过 :: 操作符来访问 PokerSuit 下的具体成员,根据代码类型提示,heartdiamond 都是 PokerSuit 枚举类型的,我们可以定义一个函数使用它们:

Rust
#[derive(Debug)]
enum PokerSuit {
  Clubs,
  Spades,
  Diamonds,
  Hearts,
}

fn main() {
    let heart = PokerSuit::Hearts;
    let diamond = PokerSuit::Diamonds;

    print_suit(heart);
    print_suit(diamond);
}

fn print_suit(card: PokerSuit) {
    println!("{:?}",card);
}

print_suit 函数的参数类型是 PokerSuit,因此我们可以把 heartdiamond 传给它,虽然它俩是 PokerSuit 的成员实例,但是它们被 rust 定义为 PokerSuit 类型

扑克牌还会带有值,那么枚举如何实现呢?我们可以将数据关联到枚举中,类似元组结构体的写法:

Rust
enum PokerCard {
  	Clubs(u8),
    Spades(u8),
    Diamonds(u8),
    Hearts(u8),
}

fn main() {
  let c1 = PokerCard::Spades(5);
  let c2 = PokerCard::Diamonds(13);
}

数组

rust 中数组有两种,一种是速度很快但是长度固定的 array,另一种是可动态增长的但有性能孙好的 Vectorarray 为数组,Vector 为动态数组

对于数组 array 有以下要素:

数组在 Rust 中是基本类型,长度固定,与其他语言不同

创建数组

rust 中的数组定义与 javascript 类似:

Rust
let arr = [1, 2, 3, 4, 5];

这种方式定义的数组,大小固定、长度固定,因此数组(array)是存储在栈上

访问数组元素

跟大多数语言一样,我们可以通过索引获取数组元素:

Rust
let a = [1, 2, 3, 4, 5 , 6];
let first = a[0]; // 获取 a 数组第一个元素
let second = a[1]; // 获取第二个元素

另外,rust 还可以使用 [类型;长度] 语法初始化一个某个值重复出现 N 次的数组:

Rust
let a = [3;5]

与下面代码效果相同:

Rust
let a = [3, 3, 3, 3, 3];

越界访问:

rust 检查器不会检查数组越界,但是如果编译运行会出现 panic,例如以下代码:

Rust
a[100];

报错信息:

PlainText
println!("{}", a[100]);
  |            ^^^^^^ index out of bounds: the length is 6 but the index is 100
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

数组元素为非基础类型:

实际开发中,数组元素会存在非基本类型,例如:

Rust
let arr = [String::from("hello"); 3];
println!("{:#?}", arr);

那么会编译错误,因为基本类型在 rust 赋值是以 Copy 的形式,而复杂类型都没有深拷贝,这能一个一个创建,所以无法复制 3 次

如果我们将上面代码中写成:

Rust
let arr = [String::from("hello"), String::from("hello"), String::from("hello")];
println!("{:#?}", arr);

虽然可以编译通过,但很难看

正确的方法应该调用 std::array::from_fn:

Rust
let arr: [String; 3] = std::array::from_fn(|_i| String::from("hello"));
println!("{:#?}", arr);

数组切片

数组切片允许我们使用数组的一部分:

Rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);

**上面的数组切片 slice 的类型是 &[i32]**,与之对比,数组的类型是 [i32;5],简单总结下切片的特点:

流程控制

if else

几乎所有的编程语言都有 if else,rust 与其他大部分语言的不同是,它可以省略后面的括号,和 go 一样:

Rust
if condition == true {
  // ...
} else {
  // ...
}

配合表达式,if 语句可以返回值

Rust
fn main() {
  let condition = true;
  let number = if condition {
    5
  } else {
    6
  };
  println!("{}", number);
}

for 循环

for 循环是最常用的循环:

Rust
for i in 1..=5 {
  println!("{}", i);
}

我们可以使用 .. 或者 ..= 语法创建连续序列

使用 for 循环往往使用集合的引用形式,因为所有权会转移到 for 语句块中,后面就用不了这个集合了

Rust
for item in &container {
	// ...
}

如果要在循环中,修改元素,必须使用 mut 关键字:

Rust
for item in &mut collection {
  // ...
}

总结如下:

使用方法等价使用方式所有权
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()可变借用

循环中获取元素索引:

Rust
let a = [4, 3, 2, 1];
for (i, v) in a.iter().enumerate() {
  println("第{}个元素是{}", i + 1, v);
}

如果只想循环某个流程 10 次,又不想声明一个变量来控制:

Rust
for _ in 0..10 {
  // ...
}

while 循环

和其他语言一样,这里不多介绍

loop 循环

loop 循环是一种无限循环,只能在内部通过 break 关键字停止:

Rust
let n = 1;
loop {
  if (n > 5) {
    break;
  }
  n++
}

模式匹配

rust 中,模式匹配最常用的是 matchif let

match 匹配

match 匹配的通用模式:

Rust
match target {
  模式1 => 表达式1,
  模式2 = > {
    语句1;
    语句2;
    表达式2
  },
  _ => 表达式3 // 默认,相当于 switch 中的 default
}

在我们之前删除字符串的例子中,返回的类型是一个 Option 类型,表示可能为空,我们可以使用 match 关键字判断是否为空:

Rust
let mut str = String::from("hello world");
let pop_str = str.pop();
match pop_str {
  Some(ch) => { println!("{}", ch); },
  None => { println!("数值为空"); }
}

match 匹配中可以指名更确起的 Some 值,例如:

Rust
match pop_str {
	Some('d') => println!("第一个分支:{}", ch),
Some(ch) => println!("第二个分支{}", ch),
_ => println!("默认分支"),
}

使用 match 表达式赋值:

match 本身就是一个表达式,因此可以赋值:

Rust
enum Ipddr {
    Ipv4,
    Ipv6,
}

fn main() {
  let address = Ipddr::Ipv6;
  let ip_str = match address {
      Ipddr::Ipv4 => "127.0.0.1",
      _ => "::1",
  };
  println!("{}", ip_str); // ::1
}

因为这里匹配到 _ 分支,所以将 "::1" 赋值给了 ip_str

穷尽匹配

如果 match 没有匹配到所有情况,会报错:

Rust
enum Direction {
    East,
    West,
    North,
    South
}

fn main() {
  let dire = Direction::East;
  match dire {
    Direction::East => println!("East!"),
    Direction::West => println!("West!"),
    Direction::North => println!("North!"),
  }
}

上述代码中,我们没有使用匹配 South,所以会报错

单分支多模式

上面代码都是单分支单模式匹配,rust 提供了 | 语法,允许我们单个分之中匹配多个值:

Rust
let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

序列 ..= 指定匹配范围

Rust
let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

上面是针对数字的,事实上,rust 也可以匹配字符,例如:

Rust
let x = 'c';
match x {
    'a'..='j' => println!("a - j"),
    'k'..='z' => println!("j - z"),
    _ => println!("啥都没匹配到")
}

匹配额外条件

匹配守卫(match guard) 是一个位于 match 分支模式之后额外的 if 条件:

Rust
let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

if let 匹配

有时候我们只需要匹配一个值,其他值忽略的场景,如果使用 match,就要写成下面这样

Rust
let v = Some(3);
match v {
  Some(3) => println!("three"),
  _ => ()
}

我们只想匹配 v 为 u8 的情况,但却写了 _ => () 一串多余的代码,为了简化这个操作,我们可以使用 if let 匹配

Rust
let num = Some(3);
if let Some(num) = num  {
  println!("hello, {:?}", num);
}

以上代码只是展示使用,实际使用编译器会提前报错提示,num 类型永远是正确匹配的

当然,存在 if let 匹配,也就存在着 while let 条件循环匹配:

Rust
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
  println!("{}", top);
}

这里注意,rust 期望 Some(top) 值类型在前,判断的元素在后,上述代码如果写成 while let stack.pop() = Some(top) 语法报错

matches! 宏

matches! 将一个表达式根模式进行匹配,然后返回匹配的结果 true 或者 false

例如,我们想过滤一个动态数组,只保留其内部某个值:

Rust
enum MyEnum {
  Foo,
  Bar
}

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

现在我们要只保留 v 数组中的 MyEnum::Foo 值,其他语言可能会这么写:

Rust
v.iter().filter(|x| x === MyEnum::Foo)

但实际上,上述代码会报错,报错信息:

PlainText
binary operation `==` cannot be applied to type `&&MyEnum`

v 数组元素的类型是 &&MyEnum,无法进行比较,我们可以使用 match 匹配改写,当然还有一种更加简洁的方式,使用 matches

Rust
v.iter().filter(|x| matches!(x, MyEnum::Foo))

变量遮蔽

matchif let 都是一个新的代码块,这里的绑定相当于新变量,如果使用同名变量,就会发生变量遮蔽,例如:

Rust
fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后,age是{:?}",age);
}

运行结果:

PlainText
在匹配前,age是Some(30)
匹配出来的age是30
在匹配后,age是Some(30)

可以看到在 if let 语法块中,age 的类型是 i32,遮蔽了匹配之前的类型,这种现象对于 match 也同理,因此最好不要同名:

Rust
fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(x) = age {
       println!("匹配出来的age是{}",x);
   }

   println!("在匹配后,age是{:?}",age);
}

Option 解构

Option 是一个枚举类型,用来解决 Rust 中变量是否有值的问题,定义如下:

Rust
enum Option<T> {
  some(T),
  None,
}

也就是说一个变量要么有值:Some<T>,要么为空:None,我们可以通过 match 来匹配

OptionSomeNone 为了方便,都包含在了 prelude 中,可以直接通过名称使用,而无需 Option::Some 去调用,但是千万不要忘记 SomeNoneOption 下的成员

匹配 Option<T>

我们举一个例子就能明白:

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

fn main() {
  let five = Some(5);
  let six = plus_one(five);
  let none = plus_one(None);
}

解构匹配

我们熟悉的解构,其实也是一种匹配,例如:

Rust
struct Point {
  x: i32,
  y: i32,
}

fn main() {
  let p = Point { x: 0, y: 7 };

  let Point { x: a, y: b } = p;
  assert_eq!(0, a);
  assert_eq!(7, b);
}

Rust 中对结构体的解构匹配,需要在 let 关键字后面加上类型,否则报错

对元组结构体同理:

Rust
struct Point(i32, i32);

fn main() {
  let p = Point(4, 5);
  let Point (x, y) = p;
  println!("x: {}", x); // 4
  println!("y: {}", y); // 5
}

如果不是结构体元组,而是普通的元组,那么不需要在 let 关键字后面加上类型:

Rust
let p = (4, 5);
let (x, y) = p;
println!("x: {}", x); // 4
println!("y: {}", y); // 5

同理对数组也是:

Rust
let arr: [u16; 2] = [114, 514];
let [x, y] = arr;

使用 .. 忽略:

Rust
let arr: [i32; 4] = [1, 2, 3, 4];
let [x, .., y] = arr;
println!("x: {}", x); // 1
println!("y: {}", y); // 4

使用 _ 忽略

Rust
let arr: [i32; 4] = [1, 2, 3, 4];
let [x, _, y, _] = arr;
println!("x: {}", x); // 1
println!("y: {}", y); // 3

_ 不仅可以忽略解构时的值,也可以使用在函数参数中,表示我们没有使用这个值,同时一些函数的返回值我们不想处理,也可以使用 _变量名称 来省略

@ 绑定

@ 运算符允许为一个字段绑定另一个变量,例如下面案例我们希望测试 Message::Hello 的 id 字段是否位于 3..=7 范围内,同时使用 id 字段:

Rust
let msg = Message::Hello { id: 5 };
match msg {
  Message::Hello { id: id_val @ 1..=5 } => {
    println!("id 值为 {}", id_val);
  },
  Message::Hello { id: 6..=10 } => {
    // 这个分支下无法使用 id
    println!("id 在第二个分支上");
  },
  Message::Hello { id } => {
    println!("id 值为 {}", id);
  }
}

@ 运算符在绑定新变量的后面

@前绑定后解构(Rust 1.56新增)

使用 @ 绑定新变量的同时,对目标进行解构:

Rust
#[derive(Debug)]
struct Point {
  x: i32,
  y: i32,
}

fn main() {
  let p @ Point { x: px, y: py } = Point { x: 10, y: 23 };
  println!("x: {}, y: {}", px, py);
  println!("{:?}", p);

  let point = Point { x: 10, y: 5 };
  if let p @ Point { x: 10, y } = point {
    println!("x is 10, y is {} in {:?}", y, p);
  } else {
    println!("x i not 10");
  }
}

在之前解构时,let 后面要跟着类型,而如果使用 @ 运算符绑定新变量,需要在前面使用

新特性(Rust 1.53新增)

Rust
match 1 {
  num @ (1 | 2) => {
    println!("num is 1 or 2, {}", num);
  }
  _ => {}
}

(1 | 2) 是一个整体,一定要用括号包住,不然报错

方法

rust 中的方法类似于 class 中的方法,不同的是,rust 往往和对象成对出现,以下是 rust 和其他编程语言的区别:

img

rust 的对象定义和方法定义式分离的,这种方法会大大提高使用者的灵活度

定义方法

rust 使用 impl 实现方法,例如:

Rust
struct Circle {
  x: f64,
  y: f64,
  radius: f64,
}

impl Circle {
  // new 是 Circle 的关联函数,因为它的第一个参数不是 self,且 new 并不是关键字
  // new 通常用于初始化当前结构体的实例
  fn new(x: f64, y: f64, radius: f64) -> Circle {
    Circle { x, y, radius }
  }
	// Circle 的方法,&self 表示借用当前的 Circle 的结构体
  fn area(&self) -> f64 {
    std::f64::consts::PI * (self.radius * self.radius)
  }
}

fn main() {
  let c = Circle::new(1.0, 2.0, 3.5);
  println!("面积为:{}", c.area());
}

self、&self 和 &mut self

self 非常重要,在上述代码中,我们使用 &self 替代 circle: &Circle&self 其实是 self: &Self 的简写,在一个 impl 块内,Self 类型指代被实现方法的结构体类型,self 是此类型实例,总之,self 指代的是 Circle 结构体实例

self 也依然有所有权的概念:

方法名和属性名相同

rust 中允许方法名和属性名相同:

Rust
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)
    }
		// radius 方法与 radius 属性同名
    fn radius(&self) -> f64 {
      f64::sqrt(self.area() / std::f64::consts::PI)
    }
}

fn main() {
  let c = Circle::new(1.0, 2.0, 3.5);
  println!("面积为:{}", c.area());
  println!("半径为:{}", c.radius());
}

带有多个参数的方法

方法和函数一样,可以使用多个参数,只是除了 new 方法以外,其他函数调用时传入的参数从第二个开始计算(第一个是 self)

Rust
impl Circle {
  compare(&self, c: &Circle) -> bool {
    &self.radius > c.radius
  }
}

fn main() {
  let c1 = Circle::new(1.0, 2.0, 5.0);
  let c2 = Circle::new(4.0, 1.0, 6.0);
  println!("{}", c1.compare(&c2));
}

关联函数

rust 中规定没有 self 的函数被称之为关联函数,因为没有 self,不能用 f.xxx() 形式调用,因为他是一个函数而不是方法

Rust 有一个约定俗成的规则,用 new 作为构造器的名称,处于设计上考虑,rust 特地没有用 new 作为关键字

因为是函数,所以不能 . 的方式,而是用 :: 来调用。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间

多个 impl 定义

rust 允许我们为一个结构体定义多个 impl 块,目的是提高更多的灵活性和代码组织性:

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

impl Circle {
  fn radius(&self) -> f64 {
    f64::sqrt(self.area() / std::f64::consts::PI)
  }
}

为枚举实现方法

rust 的枚举类型之所以强大,是因为它还可以实现方法:

Rust
#![allow(unused)]
#[derive(Debug)]
enum Message {
    User(String),
    Target,
}

impl Message {
    fn call(&self) {
        println!("打给了, {:?}", self::Message::Target)
    }
}

fn main() {
    let m = Message::User("gxj".to_string());
    m.call();
}

泛型(Generics)

假设我们有一个计算两个数之和的函数,我们需要考虑 i8u8f32f64 ... 各种类型,类似下面代码:

Rust
fn add_i8(a:i8, b:i8) -> i8 {
    a + b
}
fn add_i32(a:i32, b:i32) -> i32 {
    a + b
}
fn add_f64(a:f64, b:f64) -> f64 {
    a + b
}

而使用泛型后,我们只需要写一个函数即可:

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

不是所有类型都可以相加,所以我们使用 std::ops::Add<Output = T> 限制范围,这个性质叫做 特征(Trait)

详解

上面的 T(type 缩写) 是泛型参数,这个名字可以随便起,除非有特殊性含义,一般越少越好

使用泛型参数,有一个先决条件,必需在使用前对其进行声明:

Rust
fn largest<T>(list: &[T]) -> T

该函数作用是从列表中找出最大值,其中列表中的元素类型为 T

&[T] 是数组切片类型

一个常见的错误实现:

Rust
fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

乍一看没什么问题,但是有一个很严重的问题:不是所有的类型都可以比较,因此我们需要添加 std::cmp::PartialOrd 特征,另外,我们需要注意的是,如果不添加 &,会导致所有权的转移

Rust
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list.iter() {
        if item > largest {
            largest = &item;
        }
    }

    largest
}

结构体中使用泛型

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

上面约束了 xy 必须是相同类型,如果不是相同类型,会发生报错,例如:

Rust
let p = Point { x: 1, y: 1.1 }

如果要希望 xy 是不同类型,可以添加类型:

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

枚举使用泛型

Option 为例,rust 中的 Option 源码:

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

另外对于另外一个 rust 比较常见的类型 Result

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

方法中使用泛型

既然函数中可以使用泛型,那么与函数类似的方法也可以:

Rust
struct Point<T> {
  x: T,
  y: T,
}
impl<T> Point<T> {
  fn x(&self) -> T {
    &self.x
  }
}

使用泛型参数前,依然需要提前声明:impl<T>,只有提前声明,才可以在 Point<T> 中使用,这样 rust 就知道 Point 尖括号中的类型是泛型,而不是具体类型

如果是具体类型,可以直接在 Point 的尖括号中指定,而不需要提前声明

Rust
impl Point<i32> {
fn distance_from_origin(&self) -> f32 {
 (self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

这段代码的意义是,只有 Tf32 类型的 Point<T> 具有 distance_from_origin 方法,而且他的没有定义该方法

const 泛型

rust 数组中,[i32; 2][i32; 3] 是不同的数组类型,例如以下代码会报错:

Rust
// 方法只接受类型为 [i32; 3] 的代码
fn display_arr(arr: [i32; 3]) {
    println!("{:?}", arr);
}
fn main() {
    let arr1 = [1, 2, 3];
    display_arr(arr1);
    let arr2 = [1, 2];
    display_arr(arr2);
}

我们可以修改一下代码,打印任意长度的 i32 类型:

Rust
fn display_arr(arr: &[i32]) {
    println!("{:?}", arr);
}
fn main() {
    let arr1 = [1, 2, 3];
    display_arr(&arr1);
    let arr2 = [1, 2];
    display_arr(&arr2);
}

但是我们如果不仅想打印 i32 类型的数组,而是也可以打印例如字符串数组,这就要使用到泛型了:

Rust
fn display_arr<T: std::fmt::Debug>(arr: &[T]) {
    println!("{:?}", arr);
}
fn main() {
    let arr1 = [1, 2, 3];
    display_arr(&arr1);
    let arr2 = [1, 2];
    display_arr(&arr2);
    let arr3: [String; 3] = std::array::from_fn(|_x| String::from("Hello"));
    display_arr(&arr3)
}

通过引用和泛型,很好解决了任意数组的打印,但是如果有些数组库,限制长度怎么办,我们可以使用 const 泛型,也就是针对值的泛型,可以很好的解决数组长度问题

Rust
fn display_arr<T: std::fmt::Debug, const N: usize>(arr: &[T; N]) {
    println!("{:?}", arr);
}

当然,这段代码配合 where 才能发挥全部作用,即限制数组长度

特征(Trait)

特征 trait 在其他语言中可能叫做接口,之前的代码 #[derive(Debug)] 也是特征的使用,它在我们定义的类型(struct)上自动派生 Debug 特征。再比如:

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

定义特征

如果不同类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必须的行为的集合

例如,我们有 PostWeibo 两种内容载体,而我们想对应的内容进行总结,无论是文章内容,还是微博内容,都可以进行总结,那么总结这个行为就是共享的,因此可以用特征来定义:

Rust
pub trait Summary {
  fn summarize(&self) -> String;
}

几点:

特征只定义行为“看起来”是什么样的,而不定义行为具体是什么样的

为类型实现特征

以下例子为 PostWeibo 实现了 Summary 特征:

Rust
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 usernmae: String,
  pub content: String,
}
impl Summary for Weibo {
  fn summarize(&self) -> String {
    format!("{}发表了微博{}", self.usernmae, self.content)
  }
}

实现特征的语法,例如 impl Summary for Weibo 翻译为中文就是 为 Weibo 实现 Summary 特征,后面花括号跟着实现该特征的具体方法

接着就可以在此类型上调用方法:

Rust
    let post = Post { title: "Rust".to_string(), author: "gxj".to_string(), content: "hello rust".to_string() };
    let weibo = Weibo { usernmae: "plumbiu".to_string(), content: "hello rust".to_string() };
    println!("{}", post.summarize());
    println!("{}", weibo.summarize());

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

上面我们将 Summary 定义成了 pub 公开的。这样,如果他人想要使用我们的 Summary 特征,则可以引入到他们的包中,然后再进行实现。

关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的! 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。

该规则被称为孤儿规则,可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码

默认实现

我们可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者可以重载该方法:

Rust
pub trait Summary {
  fn summarize(&self) -> String {
    String::from("(Read more...)")
  }
}

这样子只需要这样写,也可以有一个默认方法实现,例如:

Rust
impl Summary for Post {}

impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}

可以看到,Post 选择默认实现,而 Weibo 重载了该方法,调用和输出如下:

Rust
println!("{}",post.summarize()); // (Read more...)
println!("{}",weibo.summarize()); // plumbiu发表了微博hello rust

默认实现运行调用特征自身的其他方法,哪怕这些方法没有默认实现:

Rust
pub trait Summary {
  fn summarize_author(&self) -> String;
  fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
  }
}
impl Summary for Post {
  fn summarize_author(&self) -> String {
    String::from("plumbiu")
  }
}
println!("{}", post.summarize()); // (Read more from plumbiu...)

使用特征作为函数参数

特征不仅可以用来实现方法,还可以作为函数参数:

Rust
trait Summary {
    fn summary(&self) -> String;
}
fn notify(item: &impl Summary) {
    println!("Breaking news!{}", item.summary());
}
struct Weibo {}
impl Summary for Weibo {
    fn summary(&self) -> String {
        String::from("value")
    }
}
fn main() {
    let weibo = Weibo {};
    notify(&weibo);
}

特征约束(trait bound)

impl Trait 这种语法其实是一个语法糖,完整的书写形式是:

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

简单情况下,impl Trait 就足够使用,但对于复杂场景,特征约束可以让我们拥有更大的灵活性,例如:

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

上面的代码倒是没有问题,但如果我们简化一点,或者要求两个参数必须为相同类型,那么特征约束实现起来更加方便:

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

泛型类型 T 说明了 item1item2 必须拥有相同类型,同时 T: summary 说明了 T 必须实现 Summary 特征

多重约束

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

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

同理,还可以使用特征约束形式:

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

Where 约束

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

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

上述代码可以使用 where 约束

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

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

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

Rust
use std::fmt::Display;

struct Pari<T> {
    x: