《Effective Rust》方法 2:使用类型系统表达常见行为

  • King
  • 更新于 2024-04-21 06:25
  • 阅读 151

方法2:使用类型系统表达常见行为[方法1]讨论了如何在类型系统中表达数据结构;本节继续讨论在Rust的类型系统中行为的编码。方法(Methods)在Rust的类型系统中,行为首次出现的地方就是将方法添加到数据结构上:这些方法是对该类型实例的操作,通过self标识。这种方式以

方法 2:使用类型系统表达常见行为

[方法 1]讨论了如何在类型系统中表达数据结构;本节继续讨论在 Rust 的类型系统中行为的编码。

方法( Methods )

在 Rust 的类型系统中,行为首次出现的地方就是将方法添加到数据结构上:这些方法是对该类型实例的操作,通过 self 标识。这种方式以对象导向的方式将相关的数据和代码封装在一起,这与其他语言中的做法相似;然而,在 Rust 中,方法不仅可以添加到结构体类型上,也可以添加到枚举类型上,这与 Rust 枚举的普遍性质相符([方法 1])。

enum Shape {
    Rectangle { width: f64, height: f64 },
    Circle { radius: f64 },
}

impl Shape {
    pub fn area(&self) -> f64 {
        match self {
            Shape::Rectangle { width, height } => width * height,
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        }
    }
}

方法的名称为其编码的行为提供了一个标签,而方法签名提供了其输入和输出的类型信息。方法的第一个输入是 self 的某种变体,指示该方法可能对数据结构执行的操作:

  • • &self 参数表示可以从数据结构中读取内容,但不会修改它。
  • • &mut self 参数表示该方法可能会修改数据结构的内容。
  • • self 参数表示该方法会消耗数据结构。

抽象行为

调用方法总是会导致相同的代码被执行;从一次调用到下一次调用所改变的一切就是方法操作的数据。这涵盖了许多可能的情况,但是如果在运行时需要代码发生变化呢?

Rust 在其类型系统中包括了几个特性来适应这种情况,本节将探讨这些特性。

函数指针

最简单的行为抽象是[函数指针]:一个仅指向某些代码的指针,其类型反映了函数的签名。类型在编译时进行检查,所以到程序运行时,这个值只是指针的大小。

fn sum(x: i32, y: i32) -> i32 {
    x + y
}
// Explicit coercion to `fn` type is required...
let op: fn(i32, i32) -> i32 = sum;

函数指针没有与之关联的其他数据,因此,可以以各种方式将它们视为值:

// `fn` types implement `Copy`
let op1 = op;
let op2 = op;
// `fn` types implement `Eq`
assert!(op1 == op2);
// `fn` implements `std::fmt::Pointer`, used by the {:p} format specifier.
println!("op = {:p}", op);
// Example output: "op = 0x101e9aeb0"

一个需要注意的技术细节:需要显式地将函数强制转换为 fn 类型,因为仅仅使用函数的名称并不能得到 fn 类型的值;

这段代码无法编译!

let op1 = sum;
let op2 = sum;
// Both op1 and op2 are of a type that cannot be named in user code,
// and this internal type does not implement `Eq`.
assert!(op1 == op2);
error[E0369]: binary operation `==` cannot be applied to type `fn(i32, i32) -> i32 {main::sum}`
   --> use-types-behaviour/src/main.rs:117:21
    |
117 |         assert!(op1 == op2);
    |                 --- ^^ --- fn(i32, i32) -> i32 {main::sum}
    |                 |
    |                 fn(i32, i32) -> i32 {main::sum}
    |
help: you might have forgotten to call this function
    |
117 |         assert!(op1( /* arguments */ ) == op2);
    |                    +++++++++++++++++++
help: you might have forgotten to call this function
    |
117 |         assert!(op1 == op2( /* arguments */ ));
    |                           +++++++++++++++++++

相反,编译器错误表明类型类似于 fn(i32, i32) -> i32 {main::sum},这是一种完全内部于编译器的类型(即不能在用户代码中编写),它同时标识了特定的函数及其签名。

换句话说,sum 的类型既编码了函数的签名又编码了其位置(出于优化原因);这种类型可以自动强制转换为 fn 类型([方法 6])。

闭包

裸函数指针的使用是有限的,因为被调用函数唯一可以使用的输入是那些明确作为参数值传递的内容。

例如,考虑一些使用函数指针修改切片中每个元素的代码。

// In real code, an `Iterator` method would be more appropriate.
pub fn modify_all(data: &mut [u32], mutator: fn(u32) -> u32) {
    for value in data {
        *value = mutator(*value);
    }
}

这对于对切片进行简单的修改是有效的:

fn add2(v: u32) -> u32 {
    v + 2
}
let mut data = vec![1, 2, 3];
modify_all(&mut data, add2);
assert_eq!(data, vec![3, 4, 5,]);

然而,如果修改依赖于任何额外的状态,那么无法隐式地将这些状态传递给函数指针。

这段代码无法编译!

let amount_to_add = 3;
fn add_n(v: u32) -> u32 {
    v + amount_to_add
}
let mut data = vec![1, 2, 3];
modify_all(&mut data, add_n);
assert_eq!(data, vec![3, 4, 5,]);
error[E0434]: can't capture dynamic environment in a fn item
   --> use-types-behaviour/src/main.rs:142:17
    |
142 |             v + amount_to_add
    |                 ^^^^^^^^^^^^^
    |
    = help: use the `|| { ... }` closure form instead

错误信息指向了正确的工具:闭包。闭包是一段看起来像函数定义体(lambda 表达式)的代码,不同之处在于:

  • • 它可以作为表达式的一部分构建,因此,不需要与一个名称相关联
  • • 输入参数以竖线 |param1, param2| 给出(它们的关联类型通常可以由编译器自动推导)
  • • 它可以捕获其周围...

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
江湖只有他的大名,没有他的介绍。