跳转至

Rust- 进阶

1 泛型 Generics

**rust泛型也就是C++中的模板**。rust和C++中对泛型的要求有一些不同,如下:
1. rust种**方法、函数的泛型必须要有类型限制**,比如,方法中对泛型进行的加法运算,就要给泛型添加`std::ops::Add`的限制。C++中可以加这方面的限制,也可以不加。
2. rust中**结构、枚举泛型则不需要加限制**。

rust 中 泛型参数 可以用 任意有效的符号 代替,但通常使用 TU 等代替。

1.1 函数泛型

  • 示例
#![allow(unused)]
fn main() {
    fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
        a + b
    }
    println!("{}",add(2,3));
}
  • 可能的输出
5

1.2 结构泛型

  • 示例
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 };
}

1.3 枚举泛型

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

1.4 方法泛型

方法泛型需要注意的是使用 impl<T>

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

1.5 泛型的具体化

这和**C++中模板的显式具体化**相同,为特点的类型定义规则。泛型会**优先匹配具体化**的实现。
#![allow(unused)]
struct Point<T> {
    x: T,
    y: T,
}
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}
fn main() {
    let p1:Point<f32> = Point{x:2.0,y:3.0};
    println!("{}",p1.distance_from_origin());
}

1.6 const 泛型

rust中const泛型盒C++中的基本类型模板形参相同。泛型形参可以指定整形类型。
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);
}
  • 可能的输出
[1, 2, 3]  
[1, 2]

1.7 const 泛型表达式

rust的泛型表达式和C++20中的`concepts` 和 `requires`很像,也是编译器检测泛型参数是否满足表达式。

2 集合

3 宏

https://rustwiki.org/zh-CN/book/ch19-06-macros.html

1. rust并不支持重载和变长参数,有些可以通过宏来实现相应的功能。
2. rust中的宏和C中的宏有很大不同,C中宏的定义有`#define`开始,可以有宏对象、宏函数。

在 Rust 中宏分为两大类:声明式宏 ( declarative macros) macro_rules! 和三种 过程宏 ( procedural macros ): - #[derive],在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 Debug 特征 - 类属性宏 (Attribute-like macro),用于为目标添加自定义的属性 - 类函数宏 (Function-like macro),看上去就像是函数调用

3.1 声明宏

https://www.rustwiki.org.cn/zh-CN/reference/macros-by-example.html

3.2 过程宏

https://www.rustwiki.org.cn/zh-CN/reference/procedural-macros.html

4 运算符重载

https://rustwiki.org/zh-CN/core/ops/ 可重载的运算符。

实现这些 traits 可使您重载某些运算符。

其中的某些 traits 由 prelude 导入,因此在每个 Rust 程序中都可用。只能重载由 traits 支持的运算符。 例如,可以通过 Add trait 重载加法运算符 (+),但是由于赋值运算符 (=) 没有后备 trait,因此无法重载其语义。 此外,此模块不提供任何机制来创建新的运算符。 如果需要无特征重载或自定义运算符,则应使用宏或编译器插件来扩展 Rust 的语法。

考虑到它们的通常含义和 运算符优先级,运算符 traits 的实现在它们各自的上下文中应该不足为奇。 例如,当实现 Mul 时,该操作应与乘法有些相似 (并共享期望的属性,如关联性)。

请注意,目前不支持重载 && 和 || 运算符。由于它们的短路特性,它们需要与 traits 不同的设计用于其他运算符,如 BitAnd。他们的设计正在讨论中。

许多运算符都按值取其操作数。在涉及内置类型的非泛型上下文中,这通常不是问题。 但是,如果必须重用值,而不是让运算符使用它们,那么在泛型代码中使用这些运算符就需要引起注意。一种选择是偶尔使用 clone。 另一个选择是依靠所涉及的类型,为引用提供其他运算符实现。 例如,对于应该支持加法的用户定义类型 T,将 T 和 &T 都实现 traits Add<T> 和 Add<&T> 可能是一个好主意,这样就可以编写泛型代码而不必进行不必要的克隆。

4.1 Examples

本示例创建一个实现 Add 和 Sub 的 Point 结构体,然后演示加减两个 Point。

use std::ops::{Add, Sub};

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

impl Add for Point {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {x: self.x + other.x, y: self.y + other.y}
    }
}

impl Sub for Point {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self {x: self.x - other.x, y: self.y - other.y}
    }
}

assert_eq!(Point {x: 3, y: 3}, Point {x: 1, y: 0} + Point {x: 2, y: 3});
assert_eq!(Point {x: -1, y: -3}, Point {x: 1, y: 0} - Point {x: 2, y: 3});

FnFnMut 和 FnOnce traits 由可以像函数一样调用的类型实现。请注意,Fn 占用 &selfFnMut 占用 &mut selfFnOnce 占用 self。 这些对应于可以在实例上调用的三种方法:引用调用、可变引用调用和值调用。 这些 traits 的最常见用法是充当以函数或闭包为参数的高级函数的界限。

以 Fn 作为参数:

fn call_with_one<F>(func: F) -> usize
    where F: Fn(usize) -> usize
{
    func(1)
}

let double = |x| x * 2;
assert_eq!(call_with_one(double), 2);

以 FnMut 作为参数:

fn do_twice<F>(mut func: F)
    where F: FnMut()
{
    func();
    func();
}

let mut x: usize = 1;
{
    let add_two_to_x = || x += 2;
    do_twice(add_two_to_x);
}

assert_eq!(x, 5);

以 FnOnce 作为参数:

fn consume_with_relish<F>(func: F)
    where F: FnOnce() -> String
{
    // `func` 消耗其捕获的变量,因此不能多次运行
    //
    println!("Consumed: {}", func());

    println!("Delicious!");

    // 再次尝试调用 `func()` 将为 `func` 引发 `use of moved value` 错误
    //
}

let x = String::from("x");
let consume_and_return_x = move || x;
consume_with_relish(consume_and_return_x);

// 此时无法再调用 `consume_and_return_x`

4.2 Structs

  • Yeet Experimental 在您的类型上实现 FromResidual<Yeet<T>> 以在函数返回您的类型时启用 do yeet expr 语法。
  • Range(half-open) 范围包括在 (start..end) 之下和仅在 (start..end) 之上。
  • RangeFrom 范围仅包括 (start..) 以下的范围。
  • RangeFull 无限制范围 (..)。
  • RangeInclusive 范围包括 (start..=end) 的上下边界。
  • RangeTo 范围仅排在 (..end) 之上。
  • RangeToInclusive 范围仅包括 (..=end) 以上的范围。

4.3 Enums

  • GeneratorStateExperimental 恢复生成器的结果。
  • Bound 一系列键的端点。
  • ControlFlow 用于告诉操作是应该提前退出还是像往常一样继续操作。

4.4 Traits

  • CoerceUnsized Experimental 一个 trait,指示这是一个指针或一个包装器,其中可以对指针调整大小。
  • DispatchFromDyn Experimental DispatchFromDyn 用于对象安全检查的实现 (特别允许任意 self 类型),以保证可以调度方法的接收者类型。
  • FromResidual Experimental 用于指定哪些残差可以转换为哪些 crate::ops::Try 类型。
  • Generator Experimental 由内置生成器类型实现的 trait。
  • OneSidedRange Experimental OneSidedRange 是为一侧无界的内置范围类型实现的。例如,a....b 和 ..=c 实现了 OneSidedRange,而 ..d..e 和 f..=g 则没有。
  • ResidualExperimental 允许检索实现 Try 的规范类型,该类型具有此类型作为它的残差,并允许它保留 O 作为它的输出。
  • TryExperimental? 运算符和 try {} 块。
  • Add 加法运算符 +
  • AddAssign 加法赋值运算符 +=
  • BitAnd 按位与运算符 &
  • BitAndAssign 按位与赋值运算符 &=
  • BitOr 按位或运算符 |
  • BitOrAssign 按位或赋值运算符 |=
  • BitXor 按位异或运算符 ^
  • BitXorAssign 按位异或赋值运算符 ^=
  • Deref 用于不可变解引用操作,例如 *v
  • DerefMut 用于可变解引用操作,例如在 *v = 1; 中。
  • Div 除法运算符 /
  • DivAssign 除法赋值运算符 /=
  • Drop 析构函数中的自定义代码。
  • Fn 采用不可变接收者的调用运算符的版本。
  • FnMut 采用可变接收者的调用运算符的版本。
  • FnOnce 具有按值接收者的调用运算符的版本。
  • Index 用于在不可变上下文中索引操作 (container[index])。
  • IndexMut 用于可变上下文中的索引操作 (container[index])。
  • Mul 乘法运算符 *
  • MulAssign 乘法赋值运算符 *=
  • Neg 一元否定运算符 -
  • Not 一元逻辑否定运算符 !
  • RangeBoundsRangeBounds 由 Rust 的内置范围类型实现,由 ..a....b..=cd..e 或 f..=g 等范围语法生成。
  • Rem 余数运算符 %
  • RemAssign 余数赋值运算符 %=
  • Shl 左移位运算符 <<。 请注意,因为此 trait 是针对具有多个右侧类型的所有整数类型实现的,所以 Rust 的类型检查器对 _ << _ 具有特殊的处理方式,将整数运算的结果类型设置为左侧操作数的类型。
  • ShlAssign 左移赋值运算符 <<=
  • Shr 右移运算符 >>。 请注意,因为此 trait 是针对具有多个右侧类型的所有整数类型实现的,所以 Rust 的类型检查器对 _ >> _ 具有特殊的处理方式,将整数运算的结果类型设置为左侧操作数的类型。
  • ShrAssign 右移赋值运算符 >>=
  • Sub 减法运算符 -
  • SubAssign 减法赋值运算符 -=

5 特征 Trait

5.1 默认实现

rust中可以在特征中声明一个方法,也可以给这个方法一个默认的实现;当impl这个特征时,可以重写这个特征方法,也可以直接使用默认的特征方法。这一点类似C++类方法的重写。

默认实现示例:

#![allow(unused)]
fn main() {
    pub struct Weibo {
        pub username: String,
        pub content: String,
    }
    pub trait Summary {
        fn summarize_author(&self) -> String;

        fn summarize(&self) -> String {
            format!("(Read more from {}...)", self.summarize_author())
        }
    }
    impl Summary for Weibo {
        fn summarize_author(&self) -> String {
            format!("@{}", self.username)
        }
    }
    let weibo = Weibo {
        username: String::from("nic"),
        content: String::from("123456"),
    };
    println!("1 new weibo: {}", weibo.summarize());
}S

// 输出结果:
1 new weibo: (Read more from @nic...)

5.2 特征约束 (trait bound)

在编写C++模板时,为了限制模板参数,一般会使用特征语法,比如使用`is_class`来匹配类类型,当然C++20标准中新增的概念和约束也是同样的作用。在rust中也是一样的作用,类型要具有特征才能匹配(能够编译通过),只是在语法上有些不同。

使用 Impl Trait 约束:

#![allow(unused)]

pub struct Post {
    pub title: String,   // 标题
    pub author: String,  // 作者
    pub content: String, // 内容
}
pub struct Weibo {
    pub username: String,
    pub content: String,
}
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
impl Summary for Weibo {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}
fn main() {
    pub fn notify<T: Summary>(item: &T) {
        println!("Breaking news! {}", item.summarize());
    }
    let weibo = Weibo {
        username: String::from("nic"),
        content: String::from("123456"),
    };
    let post = Post {
        title: String::from("娱乐劲爆"),
        author: String::from("nic"),
        content: String::from("某女星偷税"),
    };
    notify(&weibo);
    //Post没有impl Summary,因此编译不通过
    //notify(&post);
}

上面的 imple Trait 语法实际上是等价于下面的:

fn main() {
    pub fn notify1(item: &impl Summary) {
        println!("notify1 Breaking news! {}", item.summarize());
    }
    pub fn notify2<T: Summary>(item: &T) {
        println!("notify2 Breaking news! {}", item.summarize());
    }
    let weibo = Weibo {
        username: String::from("nic"),
        content: String::from("123456"),
    };
    notify1(&weibo);
    notify2(&weibo);
}

// 输出结果:
notify1 Breaking news! (Read more from @nic...)
notify2 Breaking news! (Read more from @nic...)

5.2.1 多重约束

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

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

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

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

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

5.2.2 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
{}

5.3 几个综合例子

5.3.1 为自定义类型实现 + 操作

在 Rust 中除了数值类型的加法,String 也可以做 加法,因为 Rust 为该类型实现了 std::ops::Add 特征,同理,如果我们为自定义类型实现了该特征,那就可以自己实现 Point1 + Point2 的操作:

use std::ops::Add;

// 为Point结构体派生Debug特征,用于格式化输出
#[derive(Debug)]
struct Point<T: Add<T, Output = T>> { //限制类型T必须实现了Add特征,否则无法进行+操作。
    x: T,
    y: T,
}

impl<T: Add<T, Output = T>> Add for Point<T> {
    type Output = Point<T>;

    fn add(self, p: Point<T>) -> Point<T> {
        Point{
            x: self.x + p.x,
            y: self.y + p.y,
        }
    }
}

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

fn main() {
    let p1 = Point{x: 1.1f32, y: 1.1f32};
    let p2 = Point{x: 2.1f32, y: 2.1f32};
    println!("{:?}", add(p1, p2));

    let p3 = Point{x: 1i32, y: 1i32};
    let p4 = Point{x: 2i32, y: 2i32};
    println!("{:?}", add(p3, p4));
}

5.3.2 自定义类型的打印输出

在开发过程中,往往只要使用 #[derive(Debug)] 对我们的自定义类型进行标注,即可实现打印输出的功能:

#[derive(Debug)]
struct Point{
    x: i32,
    y: i32
}
fn main() {
    let p = Point{x:3,y:3};
    println!("{:?}",p);
}

但是在实际项目中,往往需要对我们的自定义类型进行自定义的格式化输出,以让用户更好的阅读理解我们的类型,此时就要为自定义类型实现 std::fmt::Display 特征:

#![allow(dead_code)]

use std::fmt;
use std::fmt::{Display};

#[derive(Debug,PartialEq)]
enum FileState {
  Open,
  Closed,
}

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

impl Display for FileState {
   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     match *self {
         FileState::Open => write!(f, "OPEN"),
         FileState::Closed => write!(f, "CLOSED"),
     }
   }
}

impl Display for File {
   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
      write!(f, "<{} ({})>",
             self.name, self.state)
   }
}

impl File {
  fn new(name: &str) -> File {
    File {
        name: String::from(name),
        data: Vec::new(),
        state: FileState::Closed,
    }
  }
}

fn main() {
  let f6 = File::new("f6.txt");
  //...
  println!("{:?}", f6);
  println!("{}", f6);
}

以上两个例子较为复杂,目的是为读者展示下真实的使用场景长什么样,因此需要读者细细阅读,最终消化这些知识对于你的 Rust 之路会有莫大的帮助。

5.4 特征对象

基本上所有的编程语言中对象指的是结构体或基本类型,在rust中,特征也可以作为一个对象存在,函数可以返回特征对象,可以调用特征方法。
trait Draw {
    fn draw(&self) -> String;
}

impl Draw for u8 {
    fn draw(&self) -> String {
        format!("u8: {}", *self)
    }
}

impl Draw for f64 {
    fn draw(&self) -> String {
        format!("f64: {}", *self)
    }
}

// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
fn draw1(x: Box<dyn Draw>) {
    // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
    println!("{}", x.draw());
}

fn draw2(x: &dyn Draw) {
    println!("{}", x.draw());
}

fn main() {
    let x = 1.1f64;
    // do_something(&x);
    let y = 8u8;

    // x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw>
    // 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
    draw1(Box::new(x));
    // 基于 y 的值创建一个 Box<u8> 类型的智能指针
    draw1(Box::new(y));
    draw2(&x);
    draw2(&y);
}

// 输出结果:
f64: 1.1
u8: 8
f64: 1.1
u8: 8

上面代码,有几个非常重要的点: - draw1 函数的参数是 Box<dyn Draw> 形式的特征对象,该特征对象是通过 Box::new(x) 的方式创建的 - draw2 函数的参数是 &dyn Draw 形式的特征对象,该特征对象是通过 &x 的方式创建的 - dyn 关键字只用在特征对象的类型声明上,在创建时无需使用 dyn

struct Sheep {}
struct Cow {}

trait Animal {
    // 实例方法签名
    fn noise(&self) -> &'static str;
}

// 实现 `Sheep` 的 `Animal` trait。
impl Animal for Sheep {
    fn noise(&self) -> &'static str {
        "baaaaah!"
    }
}

// 实现 `Cow` 的 `Animal` trait。
impl Animal for Cow {
    fn noise(&self) -> &'static str {
        "moooooo!"
    }
}

// 返回一些实现 Animal 的结构体,但是在编译时我们不知道哪个结构体。
fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
        Box::new(Sheep {})
    } else {
        Box::new(Cow {})
    }
}

fn main() {
    let random_number = 0.234;
    let animal: Box<dyn Animal>;
    animal = random_animal(random_number);
    println!(
        "You've randomly chosen an animal, and it says {}",
        animal.noise()
    );
}

// 输出结果:
You've randomly chosen an animal, and it says baaaaah!

上面示例中 animal 是一个特征对象,random_animal 函数返回值是一个特征对象,因此也限制了只有实现了 Animal 特征的的对象才能被返回。

5.5 调用同名的方法

不同特征拥有同名的方法是很正常的事情,你没有任何办法阻止这一点;甚至除了特征上的同名方法外,在你的类型上,也有同名方法:

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

这里,不仅仅两个特征 Pilot 和 Wizard 有 fly 方法,就连实现那两个特征的 Human 单元结构体,也拥有一个同名方法 fly (这世界怎么了,非要这么卷吗?程序员何苦难为程序员,哎)。

既然代码已经不可更改,那下面我们来讲讲该如何调用这些 fly 方法。

5.5.1 优先调用类型上的方法

当调用 Human 实例的 fly 时,编译器默认调用该类型中定义的方法:

fn main() {
    let person = Human;
    person.fly();
}

这段代码会打印 *waving arms furiously*,说明直接调用了类型上定义的方法。

5.5.2 调用特征上的方法

为了能够调用两个特征的方法,需要使用显式调用的语法:

fn main() {
    let person = Human;
    Pilot::fly(&person); // 调用Pilot特征上的方法
    Wizard::fly(&person); // 调用Wizard特征上的方法
    person.fly(); // 调用Human类型自身的方法
}

运行后依次输出:

This is your captain speaking.
Up!
*waving arms furiously*

因为 fly 方法的参数是 self,当显式调用时,编译器就可以根据调用的类型 ( self 的类型) 决定具体调用哪个方法。

这个时候问题又来了,如果方法没有 self 参数呢?稍等,估计有读者会问:还有方法没有 self 参数?看到这个疑问,作者的眼泪不禁流了下来,大明湖畔的关联函数,你还记得嘛?

但是成年人的世界,就算再伤心,事还得做,咱们继续:

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

就像人类妈妈会给自己的宝宝起爱称一样,狗妈妈也会。狗妈妈称呼自己的宝宝为 Spot,其它动物称呼狗宝宝为 puppy,这个时候假如有动物不知道该如何称呼狗宝宝,它需要查询一下。

Dog::baby_name() 的调用方式显然不行,因为这只是狗妈妈对宝宝的爱称,可能你会想到通过下面的方式查询其他动物对狗狗的称呼:

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

铛铛,无情报错了:

error[E0283]: type annotations needed // 需要类型注释
  --> src/main.rs:20:43
   |
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot infer type // 无法推断类型
   |
   = note: cannot satisfy `_: Animal`

因为单纯从 Animal::baby_name() 上,编译器无法得到任何有效的信息:实现 Animal 特征的类型可能有很多,你究竟是想获取哪个动物宝宝的名称?狗宝宝?猪宝宝?还是熊宝宝?

此时,就需要使用 完全限定语法

5.5.2.1 完全限定语法

完全限定语法是调用函数最为明确的方式:

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

在尖括号中,通过 as 关键字,我们向 Rust 编译器提供了类型注解,也就是 Animal 就是 Dog,而不是其他动物,因此最终会调用 impl Animal for Dog 中的方法,获取到其它动物对狗宝宝的称呼:puppy

言归正题,完全限定语法定义为:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

上面定义中,第一个参数是方法接收器 receiver (三种 self),只有方法才拥有,例如关联函数就没有 receiver

完全限定语法可以用于任何函数或方法调用,那么我们为何很少用到这个语法?原因是 Rust 编译器能根据上下文自动推导出调用的路径,因此大多数时候,我们都无需使用完全限定语法。只有当存在多个同名函数或方法,且 Rust 无法区分出你想调用的目标函数时,该用法才能真正有用武之地。

5.6 特征定义中的特征约束

有时,我们会需要让某个特征 A 能使用另一个特征 B 的功能 (另一种形式的特征约束),这种情况下,不仅仅要为类型实现特征 A,还要为类型实现特征 B 才行,这就是 supertrait (实在不知道该如何翻译,有大佬指导下嘛?)

例如有一个特征 OutlinePrint,它有一个方法,能够对当前的实现类型进行格式化输出:

use std::fmt::Display;

trait OutlinePrint: Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

等等,这里有一个眼熟的语法: OutlinePrint: Display,感觉很像之前讲过的 特征约束,只不过用在了特征定义中而不是函数的参数中,是的,在某种意义上来说,这和特征约束非常类似,都用来说明一个特征需要实现另一个特征,这里就是:如果你想要实现 OutlinePrint 特征,首先你需要实现 Display 特征。

想象一下,假如没有这个特征约束,那么 self.to_string 还能够调用吗( to_string 方法会为实现 Display 特征的类型自动实现)?编译器肯定是不愿意的,会报错说当前作用域中找不到用于 &Self 类型的方法 to_string :

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

impl OutlinePrint for Point {}

因为 Point 没有实现 Display 特征,会得到下面的报错:

error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`

既然我们有求于编译器,那只能选择满足它咯:

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

上面代码为 Point 实现了 Display 特征,那么 to_string 方法也将自动实现:最终获得字符串是通过这里的 fmt 方法获得的。

5.7 在外部类型上实现外部特征 (newtype)

特征的孤儿规则,简单来说,就是特征或者类型必需至少有一个是本地的,才能在此类型上定义特征。

这里提供一个办法来绕过孤儿规则,那就是使用 newtype 模式,简而言之:就是为一个元组结构体创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。

该封装类型是本地的,因此我们可以为此类型实现外部的特征。

newtype 不仅仅能实现以上的功能,而且它在运行时没有任何性能损耗,因为在编译期,该类型会被自动忽略。

下面来看一个例子,我们有一个动态数组类型: Vec<T>,它定义在标准库中,还有一个特征 Display,它也定义在标准库中,如果没有 newtype,我们是无法为 Vec<T> 实现 Display 的:

error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/main.rs:5:1
|
5 | impl<T> std::fmt::Display for Vec<T> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------
| |                             |
| |                             Vec is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead

编译器给了我们提示: define and implement a trait or new type instead,重新定义一个特征,或者使用 new type,前者当然不可行,那么来试试后者:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

其中,struct Wrapper(Vec<String>) 就是一个元组结构体,它定义了一个新类型 Wrapper,代码很简单,相信大家也很容易看懂。

既然 new type 有这么多好处,它有没有不好的地方呢?答案是肯定的。注意到我们怎么访问里面的数组吗?self.0.join(", "),是的,很啰嗦,因为需要先从 Wrapper 中取出数组: self.0,然后才能执行 join 方法。

类似的,任何数组上的方法,你都无法直接调用,需要先用 self.0 取出数组,然后再进行调用。

当然,解决办法还是有的,要不怎么说 Rust 是极其强大灵活的编程语言!Rust 提供了一个特征叫 Deref,实现该特征后,可以自动做一层类似类型转换的操作,可以将 Wrapper 变成 Vec<String> 来使用。这样就会像直接使用数组那样去使用 Wrapper,而无需为每一个操作都添加上 self.0

同时,如果不想 Wrapper 暴露底层数组的所有方法,我们还可以为 Wrapper 去重载这些方法,实现隐藏的目的

5.8 关联类型

关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

以上是标准库中的迭代器特征 Iterator,它有一个 Item 关联类型,用于替代遍历的值的类型。

同时,next 方法也返回了一个 Item 类型,不过使用 Option 枚举进行了包裹,假如迭代器中的值是 i32 类型,那么调用 next 方法就将获取一个 Option<i32> 的值。

还记得 Self 吧?在之前的章节 提到过, Self 用来指代当前调用者的具体类型,那么 Self::Item 就用来指代该类型实现中定义的 Item 类型

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
    }
}

fn main() {
    let c = Counter{..}
    c.next()
}

在上述代码中,我们为 Counter 类型实现了 Iterator 特征,变量 c 是特征 Iterator 的实例,也是 next 方法的调用者。 结合之前的黑体内容可以得出:对于 next 方法而言,Self 是调用者 c 的具体类型: Counter,而 Self::Item 是 Counter 中定义的 Item 类型: u32

聪明的读者之所以聪明,是因为你们喜欢联想和举一反三,同时你们也喜欢提问:为何不用泛型,例如如下代码:

pub trait Iterator<Item> {
    fn next(&mut self) -> Option<Item>;
}

答案其实很简单,为了代码的可读性,当你使用了泛型后,你需要在所有地方都写 Iterator<Item>,而使用了关联类型,你只需要写 Iterator,当类型定义复杂时,这种写法可以极大的增加可读性:

pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable {
  type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash;
  fn is_null(&self) -> bool;
}

例如上面的代码,Address 的写法自然远比 AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash 要简单的多,而且含义清晰。

再例如,如果使用泛型,你将得到以下的代码:

trait Container<A,B> {
    fn contains(&self,a: A,b: B) -> bool;
}

fn difference<A,B,C>(container: &C) -> i32
  where
    C : Container<A,B> {...}

可以看到,由于使用了泛型,导致函数头部也必须增加泛型的声明,而使用关联类型,将得到可读性好得多的代码:

trait Container{
    type A;
    type B;
    fn contains(&self, a: &Self::A, b: &Self::B) -> bool;
}

fn difference<C: Container>(container: &C) {}

6 包管理

  • Package:可以用来构建、测试和分享包
  • WorkSpace:对于大型项目,可以进一步将多个 Crate 联合在一起,组织成 WorkSpace
  • Crate:一个由多个 Module 组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行
  • Module:可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元

一个真实项目中典型的 Package,会包含多个二进制包,这些包文件被放在 src/bin 目录下,每一个文件都是独立的二进制包,同时也会包含一个库包,该包只能存在一个 src/lib.rs

.
├── Cargo.toml
├── Cargo.lock
├── src
   ├── main.rs
   ├── lib.rs
   └── bin
       └── main1.rs
       └── main2.rs
├── tests
   └── some_integration_tests.rs
├── benches
   └── simple_bench.rs
└── examples
    └── simple_example.rs
  • 唯一库包:src/lib.rs
  • 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
  • 其余二进制包:src/bin/main1.rssrc/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
  • 集成测试文件:tests 目录下
  • 基准性能测试 benchmark 文件:benches 目录下
  • 项目示例:examples 目录下

6.1 mod

模块可以嵌套,也可以放在同一个文件,也可以分离到各个文件

6.1.1 Module 的声明

rust 中的 Module 通过关键字 mod 模组名{...} 来声明,这种声明方式和 C++ 中命名空间一样 namespace 命名空间名{...}

6.1.2 Module 的引入

mod 模组名 表示从和模组同名的文件加载该模块的内容, 这一点和 C++ 中 include 或者 C++20 中 Module 一样。

6.2 pub

rust pub 相当于C++中的public。rust中默认是private。
- 将结构体设置为 `pub`,但它的所有字段依然是私有的
- 将枚举设置为 `pub`,它的所有字段也将对外可见
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

6.3 use

rust 中 use 和 C++ 中 using namespace 很像,简短了在使用外部模组下内容时长长的模组路径。

  • 示例
#![allow(unused)]
fn main() {
mod front_of_house;
// 使用front_of_house:hosting的作用域引入到当前文件中,可以直接使用其下的内容
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
}

6.3.1 use .. as 别名

作用:将引入的内容 重新命名,避免引入的内容 同名污染 问题。

6.3.2 pub use

作用:当外部的模块项 A 被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 A,那么可以对它进行再导出。

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
}

6.4 self、super、crate

  • self 其实就是引用自身模块中的项。(通常省略,默认就是)
  • super 代表的是父模块为开始的引用方式,类似 ../
  • crate 相当于模组的根目录,用于绝对路径导入模组方式
#![allow(unused)]
fn main() {
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
}

6.5 不同文件中的 mod 引入

6.5.1 方式一使用 mod. rs

假设有如下文件

[shw@rocky9 src]$ tree
.
├── main.rs
└── mymath
    ├── add.rs
    └── mod.rs
  • add.rs 文件
// add.rs 文件
pub fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

pub 声明 add 函数为公开 - mod.rs 文件

// mod.rs文件
mod add;
pub use add::add;

mod add; 表示在另一个与模块同名的文件中加载模块的内容;pub use 使导入的 add 函数对其它模块可见。 - main.rs 文件

#![allow(unused)]
mod mymath;

fn main() {
    println!("{}",mymath::add(2,3));
}

6.5.2 方式二使用同目录名 rs 文件

假设有如下文件:

[shw@rocky9 src]$ tree
.
├── main.rs
├── mymath
│   └── add.rs
└── mymath.rs

mymath.rs 文件同方式一的 mymath/mod.rs,其它文件不变

// mymath.rs文件
mod add;
pub use add::add;

6.6 第三方包的引入

引入下第三方包中的模块操作步骤如下: 1. 修改 Cargo.toml 文件,在 [dependencies] 区域添加一行:rand = "0.8.3" 2. 此时,如果你用的是 VSCoderust-analyzer 插件,该插件会自动拉取该库,你可能需要等它完成后,再进行下一步(VSCode 左下角有提示)

好了,此时,rand 包已经被我们添加到依赖中,下一步就是在代码中使用:

use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
}

7 注释

  • https://course.rs/basic/comment.html
  • https://rustwiki.org/zh-CN/rustdoc/ 使用 cargo doc 命令能生成生成 HTML,CSS 和 JavaScript 文件。

7.1 代码注释

  • 行注释://
  • 块注释:/**/
fn main() {
    // 我是Sun...
    // face
    let name = "sunface";
    let age = 18; // 今年好像是18岁
}

7.2 文档注释

  • 行注释:///
  • 块注释:/** */
/// `add_one` 将指定值加 1
///
/// # Examples
///
/// 
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// 
pub fn add_one (x: i32) -> i32 {
    x + 1
}

7.3 模块注释

  • 行注释://!
  • 块注释:/*! */ 除了函数、结构体等 Rust 项的注释,你还可以给包和模块添加注释,需要注意的是,这些注释要添加到包、模块的最上方

与之前的任何注释一样,包级别的注释也分为两种:行注释 //! 和块注释 /*! ... */

现在,为我们的包增加注释,在 src/lib.rs 包根的最上方,添加:

/*! lib包是world_hello二进制包的依赖包,
 里面包含了compute等有用模块 */

pub mod compute;

然后再为该包根的子模块 src/compute.rs 添加注释:

//! 计算一些你口算算不出来的复杂算术题


/// `add_one`将指定值加1
///