《与 Rust 勾心斗角》·点

上一篇:前言
三维世界的原点,就是在三个数轴上的投影为 0 的点。假设三个数轴为 x,y 和 z,则原点在它们上的投影可表示为 x = 0, y = 0, z = 0,用 Rust 代码可表示为
let x: f64 = 0.0; let y: f64 = 0.0; let z: f64 = 0.0;

亦即定义了三个变量 x, y, z,它们皆为 64 位的浮点类型,值皆为浮点类型的字面量 0.0。注意,0.00 不同。
现在可以写一个能够问候原点的程序了,
fn main() { let x: f64 = 0.0; let y: f64 = 0.0; let z: f64 = 0.0; println!("你好啊,({0}, {1}, {2})!", x, y, z); }

编译,运行:
$ rustc foo.rs $ ./foo 你好啊,(0, 0, 0)!

结构体 使用结构体类型可将 x, y, z 绑定到一起,构造抽象意义的三维原点,例如:
struct Point { x: f64, y: f64, z: f64 }fn main() { let origin: Point = Point {x: 0.0, y: 0.0, z: 0.0}; println!("你好啊,({0}, {1}, {2})!", origin.x, origin.y, origin.z); }

由于 rustc 能够根据值的语法形式推断出变量类型,因此
let origin: Point = Point {x: 0.0, y: 0.0, z: 0.0};

可简化为
let origin = Point {x: 0.0, y: 0.0, z: 0.0};

方法 Rust 语言允许为结构体类型定义方法——有些特殊的函数,例如:
impl Point { fn origin() -> Point { Point {x: 0.0, y: 0.0, z: 0.0} } }

使用上述定义的方法,可进一步简化构造原点的语句:
let origin = Point::origin();

origin 方法是通过类型 Point 调用,这类方法称为「静态方法(Static Method)」。也可以为结构体类型的实例定义方法,这类方法称为「实例方法(Instance Method)」,例如:
impl Point { fn hello(&self) { println!("你好啊,我是 ({0}, {1}, {2}!", self.x, self.y, self.z); } }

以下代码构造一个点的实例,并调用实例方法 hello
let x = Point::origin(); x.hello();

n 维点 若点的维度任意,用 C 语言,可将其定义为
typedef struct { size_t n; double *body; } Point;

body 指向堆空间里大小为 n * sizeof(double) 的一段空间。用 Rust 语言如何定义类似的结构体?可使用 Box 指针,例如
struct Point { n: isize, body: Box }

在 Rust 语言里,Box 是泛型的智能指针。在上例中,T[f64],即成员类型为 f64 的数组类型。
为 C 语言版本的 Point 类型构建一个实例:
size_t n = 3; double *body = malloc(n * sizeof(double)); body[0] = 0.1; body[1] = 0.2; body[2] = 0.3; Point x = (Point){.n = n, .body = body}; printf("你好啊,%zu 维点 (%f, %f, %f)!\n", x.n, x.body[0], x.body[1], x.body[2]); free(body);

类似地,上述 Rust 语言版本的 Point 的实例化过程为
let x = Point {n: 3, body: Box::new([0.1, 0.2, 0.3])}; println!("你好啊,{0} 维点 ({1}, {2}, {3})!", x.n, x.body[0], x.body[1], x.body[2]);

在上述示例里,Rust 语言要比 C 语言简约得多。另外,无论是 C 语言还是 Rust 语言,示例中的 x.body 所指向的内存空间位于程序的堆空间,但是前者需要显示回收,而后者可自动回收,故 Box 称为「智能指针」。
数组和向量 与早期的 C 语言类似,Rust 语言里的数组是固定长度的类型,亦即在定义数组实例时需要指定数组的长度,例如
let x: [f64; 3] = [0.1, 0.2, 0.3];

C 语言自 C99 标准开始支持变长数组。不过,由于数组空间位于栈上,对于大量的数据而言,数组是否变长并不重要,因为通常需要在堆空间构造数组。堆空间的内存可以由编程者自行分配和维护,因此实现变长数组仅仅是算法问题,而不是语法问题。
需要注意的是,在 Rust 语言里,若在堆空间为数组分配空间,所用的智能指针 Box 的泛型参数 T 的值虽然作为数组类型,但是不需要提供数组长度,只需提供数组元素的类型,例如 [f64]
Rust 语言提供了堆空间变长数组的实现,即向量(Vec)类型,可直接基于该类型定义 n 维点,例如
let mut x: Vec = Vec::new(); x.push(0.1); x.push(0.2); x.push(0.3); println!("你好啊,{0} 维点 ({1}, {2}, {3})!", x.len(), x[0], x[1], x[2]);

Vec::new 构造的向量实例,若向其中增加元素,则需要将向量实例设定为可变,即 mut
使用 vec! 可将上述代码简化为
let x: Vec = vec![0.1, 0.2, 0.3]; println!("你好啊,{0} 维点 ({1}, {2}, {3})!", x.len(), x[0], x[1], x[2]);

vec! 可在堆空间构造一个向量空间,然后将栈空间里的数组的数据复制到向量空间,若后续不需要修改向量的内容或向其中增加新元素,不需要将向量实例设为可变。
上一节基于 Box 定义的 n 维点,实际上相当于低配版本的 Vec,如无必要,通常应该使用后者,而且可以使用类型别名的形式,例如
type Point = Vec;

特性 下面是一个完整的程序,它能够让一个 n 维点自报家门:
type Point = Vec; fn hello(x: &Point) { let n = x.len(); print!("你好啊,我是 {} 维点 (", x.len()); for i in 0 .. n { if i != (n - 1) { print!("{}, ", x[i]); } else { print!("{}", x[i]); } } println!(")!"); }fn main() { let x: Point = vec![0.1, 0.2, 0.3]; hello(&x); }

程序的输出结果为
你好啊,我是 3 维点 (0.1, 0.2, 0.3)!

能否将 hello 作为 Point 亦即 Vec 实例的方法呢?例如
impl Point { fn hello(&self) { let n = self.len(); print!("你好啊,我是 {} 维点 (", self.len()); for i in 0 .. n { if i != (n - 1) { print!("{}, ", self[i]); } else { print!("{}", self[i]); } } println!(")!"); } }

rustc 说不行。错误信息为
error[E0116]: cannot define inherent `impl` for a type outside of the crate where the type is defined

然后给出修改建议:
define and implement a trait or new type instead

下面定义一个 Hello 特性(Trait)试试,
trait Hello { fn hello(&self); }

然后为 Point 亦即 Vec 类型实现 Hello 特性,
impl Hello for Point { fn hello(&self) { let n = self.len(); print!("你好啊,我是 {} 维点 (", self.len()); for i in 0 .. n { if i != (n - 1) { print!("{}, ", self[i]); } else { print!("{}", self[i]); } } println!(")!"); } }

然后使用 Hello 特性里的 hello 函数,在行为上与类型实例的方法完全一致,例如
fn main() { let x: Point = vec![0.1, 0.2, 0.3]; x.hello(); }

虽然特性与方法有相似之处,但是表达的语义不同。方法面向特定类型,而特性面向不同的类型。不同的类型,可以有相似的行为。人要吃东西,睡觉,生孩子。动物不是人,也要吃东西,睡觉,生孩子。道路上,可以走人,也可以行车。为不同的类型构造相似的行为,这就是特性存在的意义。
小结 【《与 Rust 勾心斗角》·点】几乎将一年前学过的一点 Rust 语言复习了一遍,只有 Box 指针是初学,最大的感触是,rustc 对我友好了许多。

    推荐阅读