19高级特性

本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,你可以将本章作为不经意间遇到未知的内容时的参考。

本章将涉及如下内容:

  • 不安全 Rust:用于当需要舍弃 Rust 的某些保证并负责手动维持这些保证
  • 高级 trait:与 trait 相关的关联类型,默认类型参数,完全限定语法(fully qualified syntax),超(父)trait(supertraits)和 newtype 模式
  • 高级类型:关于 newtype 模式的更多内容,类型别名,never 类型和动态大小类型
  • 高级函数和闭包:函数指针和返回闭包
  • 宏:定义在编译时定义更多更多代码的方式

不安全 Rust

不安全 Rust(unsafe Rust),不会强制执行内存安全保证,其他部分于常规 Rust 代码无异。

存在的原因:

  1. 因为静态分析本质上是保守的,编译器可能会拒绝一段其无法理解的程序,但是当你确定这是没有问题的,可以放到不安全代码块中;
  2. 底层计算机硬件固有的不安全性,Rust 允许直接与操作系统交互或者编写底层系统。

有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,被称为“不安全的超能力”:

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait
  • 访问 union 的字段

重要:

unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查

  • 如果在不安全代码中使用引用,它仍会被检查。

unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能。

  • 仍然能在不安全块中获得某种程度的安全。

unsafe的意图在于作为程序员你将会确保 unsafe 块中的代码以有效的方式访问内存。而不是意味着代码块中的代码一定危险。

保持 unsafe 块尽可能小,方便定位内存bug。

将不安全代码封装进一个安全的抽象并提供安全 API ,在尽可能隔离不安全代码的比较好。

解引用裸指针

不安全 Rust,有两个被被称为裸指针(raw pointers)的新类型,类似于引用。

  • 不可变的 *const T
  • 可变的 *mut T

这里的星号(*)是类型名称的一部分,并不是解引用运算符。

在裸指针的上下文中,不可变意味着指针解引用后不能直接赋值。

裸指针与引用和智能指针的区别在于:

  • 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针;
  • 不保证指向有效的内存;
  • 允许为空;
  • 不能实现任何自动清理功能;
1
2
3
4
5
6
7
8
9
10
11
12
13

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}

// 这里创建指向任意内存地址的裸指针
let address = 0x012345usize;
let r = address as *const i32;

注意,可以在安全代码中 创建 裸指针,只是不能在不安全块之外 解引用 裸指针和读取其指向的数据。

创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。

使用场景:

  • 一个主要的应用场景便是调用 C 代码接口,这在下一部分 “调用不安全函数或方法” 中会讲到。

  • 另一个场景是构建借用检查器无法理解的安全抽象。

调用不安全函数或方法

调用不安全函数,要求使用不安全块操作。

不安全函数和方法与常规函数方法十分类似,除了其开头有额外的unsafe

  • 关键字unsafe表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。
1
2
3
4
5
unsafe fn dangerous() {}

unsafe {
dangerous();
}

必须在unsafe块中调用不安全函数,否则会报错。

不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块。

创建不安全代码的安全抽象

将不安全代码封装进安全函数是一个常见的抽象。

使用 extern 函数调用外部代码

extern 关键字,有助于创建和使用外部函数接口(Foreign Function Interface,FFI)。

  • 有时你的 Rust 代码可能需要与其他语言编写的代码交互。
  • 外部函数接口允许不同编程语言调用这些函数。

extern 块中声明的函数在 Rust 代码中总是不安全的

  • 因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任
1
2
3
4
5
6
7
8
9
extern "C" {
fn abs(input: i32) -> i32;
}

fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}

声明并调用C语言标准库的abs函数,定义的 extern 函数

访问或修改可变静态变量

全局变量(global variables)在 Rust 中被称为静态(static)变量。

静态变量与常量类似

  • 静态变量

    • 使用static关键字声明;

    • 只能存储拥有'static 生命周期的引用

      • Rust 可以自己计算出其生命周期所以无需显示标注。
    • 访问不可变静态变量是安全的,

    • 但访问和修改可变静态变量是不安全的。

      • 拥有可以全局访问的可变数据,难以保证不存在数据竞争,所以 Rust 认为可变静态变量是不安全的。
    • 静态变量中的值有一个固定的内存地址,使用这个总是会访问相同的地址。

  • 常量

    • 使用 const 关键字声明;

    • 程序整个生命周期都是有效的;

    • 是不可变的,不允许对常量使用mut关键字;

    • 常量允许在任何被用到的时候复制其数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 不可变静态变量
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
// 访问不可变静态变量是安全的
println!("name is: {}", HELLO_WORLD);
}

// 可变静态变量
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
// 修改可变静态变量是不安全的
unsafe {
COUNTER += inc;
}
}

fn main() {
add_to_count(3);
// 访问可变静态变量也是不安全的
unsafe {
println!("COUNTER: {}", COUNTER);
}
}


// 常量
const MAX_POINTS: u32 = 100_000; // 数字中的下划线仅起到增加可读性的作用;

实现不安全 trait

unsafe 的另一个操作用例是实现不安全 trait。

当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe

1
2
3
4
5
6
7
unsafe trait Foo {
// methods go here
}

unsafe impl Foo for i32 {
// method implementations go here
}

访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,unionstruct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看参考文档了解有关联合体的更多信息。

何时使用不安全代码

使用 unsafe 来进行这五个操作(超能力)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。

高级 trait

关联类型在 trait 定义中指定占位符类型

关联类型(associated types),是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。

  • trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型

  • 如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。

一个带有关联类型的 trait 的例子是标准库提供的 Iterator trait。

  • 它有一个叫做 Item 的关联类型来替代遍历的值的类型。
1
2
3
4
5
pub trait Iterator {
type Item; // type 定义的 Item 就是关联类型

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

关联类型看起来与泛型有些类似,因为它运行定义一个函数而不指定其可以处理的类型。

  • 通过关联类型,无需标注类型,如果是泛型则需要标注类型,

默认泛型类型参数和运算符重载

默认类型参数(defalut type parameters)主要用于如下两个方面:

  • 扩展类型而不破坏现有代码。
  • 在大部分用户都不需要的特定情况进行自定义。

运算符重载Operator overloading)是指在特定情况下自定义运算符(比如 +)行为的操作。

  • Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。

消除歧义的完全限定语法:调用同名方法

Rust 允许为不同 trait 创建相同方法,同时允许同一个类型实现相同的方法。

但是当调用这些方法时,需要告诉 Rust 我们希望使用哪一个。

注:编译器默认调用直接实现在类型上的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 两个 trait 定义为拥有 fly 方法,并在直接定义有 fly 方法的 Human 类型上实现这两个 trait

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

// 当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法
fn main() {
let person = Human;
person.fly();
}
// 输出:*waving arms furiously*
1
2
3
4
5
6
7
// 我们需要使用更明显的语法以便能指定我们指的是哪个 fly 方法。
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}

然而,关联函数是 trait 的一部分,但没有 self 参数。当同一作用域的两个类型实现了同一 trait,Rust 就不能计算出我们期望的是哪一个类型,除非使用 完全限定语法fully qualified syntax)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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")
}
}

// 默认情况下,它直接调用了定义于 Dog 之上的关联函数。
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}

// 尝试调用 Animal trait 的 baby_name 函数,不过因为baby_name() 是关联函数而不是方法,因此没有self参数,Rust 并不知道该使用哪一个实现。
// 会得到编译时报错。
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}

为了消歧义并告诉 Rust 我们希望使用的是 DogAnimal 实现,需要使用 完全限定语法fully qualified syntax),这是调用函数时最为明确的方式。

1
2
3
4
5
6
// 使用完全限定语法来指定我们希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数。
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

// 会输出,A baby dog is called a puppy。

完全限定语法定义为:

1
2
3
4
5
6
7
8
9
10
// 完整语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);

// 调用方法时的省略语法:
// 因为方法有self,即receiver_if_method 部分,所以 Rust 可以计算出它属于那个type,所以可以省略 Type 部分;
Trait::function(receiver_if_method, next_arg, ...);

// 调用关联函数时的省略语法:
// 因为关联函数没有 receiver_if_method部分,所以需要告诉Rust 调用那个类型上的trait 实现
<Type as Trait>::function(next_arg, ...);

使用 supertrait 可以使我们的 trait 包含另一个 trait 的功能

有时我们可能会需要某个 trait 使用另一个 trait 的功能。

限制:

在这种情况下,相关被依赖的 trait 也被实现。

这个被依赖的 trait 是我们实现的 trait 的 父(超) traitsupertrait)。

  • 这类似于为 trait 增加 trait bound。
  • 要使用该 trait 就要同时实现其父 trait(supertrait)

语法:

1
2
// TraitB是TraitA的supertrait,要使用TraitA就要实现TraitB
trait TraitA: TraitB {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::fmt;

trait OutlinePrint: fmt::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 trait 时就需要实现 fmt:Display,否则编译器会报错。
struct Point {
x: i32,
y: i32,
}

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

newtype 模式用以在外部类型上实现外部 trait

为类型试下 trait 时,默认遵守孤儿原则(orphan rule),即只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。

一个绕开这个限制的方法是使用 newtype 模式newtype pattern),它涉及到在一个元组结构体中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。

注:使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

此方法的缺点是,因为 Wrapper 是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper 上实现 Vec<T> 的所有方法,这样就可以代理到self.0 上 —— 这就允许我们完全像 Vec<T> 那样对待 Wrapper。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref trait 并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。

高级类型

为了类型安全和抽象而使用 newtype 模式

newtype 模式可以用于

  • 一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单元。
  • 抽象掉一些类型的实现细节
    • 例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能。
  • 可以隐藏其内部的泛型类型。
    • 例如,可以提供一个封装了 HashMap<i32, String>People 类型,用来储存人名以及相应的 ID。使用 People 的代码只需与提供的公有 API 交互即可,比如向 People 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 i32 ID 赋予了这个名字了。newtype 模式是一种实现第十七章 “封装隐藏了实现细节” 部分所讨论的隐藏实现细节的封装的轻量级方法。

类型别名用来创建类型同义词

Rust 还提供了声明 类型别名type alias)的能力,使用 type 关键字来给予现有类型另一个名字。

例如,可以像这样创建 i32 的别名 Kilometers

1
type Kilometers = i32;

这意味着 Kilometersi32同义词synonym);Kilometers 类型的值将被完全当作 i32 类型值来对待。

类型别名的主要用途减少重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Box<dyn Fn() + Send + 'static> 类型很长
// 使用类型别名前
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}


// 使用类型别名后
// 这里我们为这个冗长的类型引入了一个叫做 Thunk 的别名
type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
// --snip--
}

fn returns_long_type() -> Thunk {
// --snip--
}

标准库中的 std::io 模块中也是用了类型别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::io::Error;
use std::fmt;

// 使用类型别名前
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;

fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

// 使用类型别名后
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}

类型别名在两个方面有帮助:易于编写 在整个 std::io 中提供了一致的接口。

as 与 type 的区别

as 有如下功能

  • 强制类型转换,消除特定包含项的 trait 的歧义;
  • useextern crate 语句中的项重命名;

type 的主要功能:

  • 定义一个类型别名或关联类型

使用场景:

通常当需要为一个类型定义别名时,通常使用 type,这样更清楚地暗示意图。

  • type Result<T> = std::result::Result<T, MyError>;

当你导入特定项目时发现它与当前命名空间中已有的内容冲突时,一般使用 use ... as ...

1
2
3
use std::io::Read as StdRead;

trait Read: StdRead { ... }

不过,我们应该优先使用路径限定标识符,然后才尝试使用 use...as...重命名。

1
2
3
use std::io;

trait Read: io::Read { ... }

注意:应该避免使用use...as.. 代替 type的情况。

从不返回的 never type

Rust 有一个叫做 ! 的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type

  • 这个名字描述了它的作用:在函数从不返回的时候充当返回值。
  • 描述 ! 的行为的正式方式是 never type 可以强转为任何其他类型

从不返回的函数被称为 发散函数diverging functions)。

1
2
3
fn bar() -> ! {
// --snip--
}

作用:

  • 用于continue,continue 的返回值是!即并不真正返回一个值,相反它把控制权交回上层循环;

    • 例如,当用于match的分支时,match的分支要求必须返回相同的类型,那么continue返回!,则其实就是强转为了其他分支返回的值的类型了。它把控制权交回上层循环,由Rust决定类型是什么。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 错误形式
      let guess = match guess.trim().parse() {
      Ok(_) => 5, // i32
      Err(_) => "hello", // String
      } // 类型不统一,所以编译出错

      // 正确形式
      let guess: u32 = match guess.trim().parse() {
      Ok(num) => num,
      Err(_) => continue, // continue 返回!,即不返回任何值,所以Rust决定 guess的类型是 num的类型即u32
      };
  • 用于panic!panic! 的返回值也是!,即没有返回任何值,类型的决定权就由Rust 根据其他分支决定;

    1
    2
    3
    4
    5
    6
    7
    8
    impl<T> Option<T> {
    pub fn unwrap(self) -> T {
    match self {
    Some(val) => val,
    None => panic!("called `Option::unwrap()` on a `None` value"), // 这里知道 val 是T类型,panic!返回!,即不返回任何值,它终止程序,所以最终unwrap返回的就是T类型。
    }
    }
    }
  • 一个有着 ! 类型的表达式是 loop

    1
    2
    3
    4
    5
    6
    7
      print!("forever ");

    // loop 表达式的值是 !,
    loop { // 这里循环永远不结束,所以表达式的值是!
    print!("and ever ");
    }
    // 希望结束就引入 break

!() 的区别

() 有一个唯一的值(),即有值;

  • () 是rust中的unit type,该类型是zero-size的,并且有一个唯一的值:()

  • 用在函数返回值时,有点类似于void

  • 当一个表达式或函数没有返回一个值时,返回的是();

  • 当一个Option或者Result并不关心返回值时,即当没有其他有意义的值可返回时使用,可以使用Option<()>或者Result<(), Box<dyn Error>>

  • HashSet<T> 实际上就是HashTable<T,()>

  • 当我们需要表示一个raw pointer,但是并不关心其实际的类型时,就可以使用*mut ()或者*const (),类似于c中的void*const void*

!没有值

  • 在Rust 中是 empty type ,即never type

  • 不能创建 ! 类型的值;

  • 使用它时表示该函数不会返回;

动态大小类型和 Sized trait

动态大小(dynamically sized types),是运行时才知道大小的类型。也被称为“DST”或“unsized types”。

Rust 需要在编译时知道为类型的值分配多少空间。

Rust 中动态大小类型的黄金规则:

  • 必须将动态大小类型的值置于某种指针之后

例如,可以将str与所有类型的指针结合:比如Box<str>Rc<str>

另外一个动态大小类型是:trait。

  • 每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型
  • “为使用不同类型的值而设计的 trait 对象”部分,我们知道为了将 trait 用于 trait 对象,必须将它们放入指针之后,比如&dyn Trait, 或 Box<dyn Trait>, 或 Rc<dyn Trait>

Rust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:Sized trait

这个 trait 为编译器在编译时就知道大小的类型自动实现。

  • 另外,Rust 隐式的为每一个泛型函数增加了 Sized bound。
1
2
3
fn generic<T>(t: T) {
// --snip--
}

实际上被当作如下处理:

1
2
3
fn generic<T: Sized>(t: T) {
// --snip--
}

泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:

1
2
3
fn generic<T: ?Sized>(t: &T) {
// --snip--
}

?Sized trait bound 与 Sized 相对;也就是说,它可以读作 “T 可能是也可能不是 Sized 的”。这个语法只能用于 Sized ,而不能用于其他 trait。

另外注意我们将 t 参数的类型从 T 变为了 &T:因为其类型可能不是 Sized 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。

高级函数与闭包

函数指针以及返回值闭包。

函数指针

我们不仅可以向函数传递闭包,还可以传递常规函数。

函数指针(function pointer)允许我们使用函数作为另一个函数的参数。

  • fn 是函数指针,(使用小写的f,区别于Fn闭包 trait)。
  • 不同于闭包,fn 是一个类型而不是一个 trait,
    • 所以使用时,直接指定fn作为参数而不是声明一个带有Fn 作为 trait bound 的泛型参数。
  • 函数指针实现了所有三个闭包 trait(FnFnMutFnOnce),
    • 所以总是可以在调用期望闭包的函数时传递函数指针作为参数。
    • 所以,我们也更倾向于编写使用泛型和闭包 trait 的函数。
      • 这样它就能接受函数或闭包作为参数。
    • 只有当与不存在闭包的外部代码交互时,才定义只期望接受fn而不接受闭包的情况。
      • 例如 C语言的函数没有闭包,就只接受函数作为参数。
  • 指定参数为函数指针的语法类似于闭包。

例如,使用 fn 类型接受函数指针作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn add_one(x: i32) -> i32 {
x + 1
}

// 使用 fn 参数指针类型
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}

fn main() {
let answer = do_twice(add_one, 5);

println!("The answer is: {}", answer);
}

作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 map 的应用。使用 map 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样:

1
2
3
4
5
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();

或者可以将函数作为 map 的参数来代替闭包,像是这样:

1
2
3
4
5
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
  • 注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 to_string 的函数;这里使用了定义于 ToString trait 的 to_string 函数,标准库为所有实现了 Display 的类型实现了这个 trait。

另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节。这些项使用 () 作为初始化语法,这看起来就像函数调用,同时它们确实被实现为返回由参数构造的实例的函数。它们也被称为实现了闭包 trait 的函数指针,并可以采用类似如下的方式调用:

1
2
3
4
5
6
7
8
9
enum Status {
Value(u32),
Stop,
}

let list_of_statuses: Vec<Status> =
(0u32..20)
.map(Status::Value)
.collect();

这里创建了 Status::Value 实例,它通过 map 用范围的每一个 u32 值调用 Status::Value 的初始化函数。一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。

返回闭包

之前在“第十章泛型” 中讲过,如果将泛型作为返回值,那么每次只能返回一种类型,而不是动态返回多种类型。

  • 所以,闭包表现为 trait,这意味着不能直接返回闭包。

对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为他们没有一个可返回的具体类型;例如不允许使用函数指针 fn 作为返回值类型。

这段代码尝试直接返回闭包,它并不能编译:

1
2
3
4
5
6
7
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
// 编译器报错:
error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
std::marker::Sized` is not satisfied
= note: the return type of a function must have a statically known size

错误又一次指向了 Sized trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象

1
2
3
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}

这段代码正好可以编译。

Macro)指的是 Rust 中一系列的功能:使用 macro_rules!声明Declarative)宏,和三种 过程Procedural)宏:

  • 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码;
  • 类属性(Attribute-like)宏定义可用于任意项的自定义属性;
  • 类函数宏看起来像函数不过作用于作为参数传递的 token;

宏的更多内容参考这里

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信