理解Rust中的类型设计 - Objects and behavior

数据类型是编程语言的基石。数据类型之于编程语言,就像26个字母之于单词、偏旁部首之于汉字,是构成程序语句最基础的元素,是我们编写statement/expressionliteral,从本文开始我会涵盖Rust中常用的数据类型介绍和简单使用。

当今的许多编程语言都有多重编程范式设计,又称多范式编程语言(Multi-paradigm programming language),例如Java/Golang,但他们还停留在聚焦于对象继承原则,这也就是说它们会有classsinterfacesclass inheritance这些,而这些没有一个出现在Rust中,这也是造成很多人觉得Rust的学习曲线很陡的原因之一。Rust作为一个多重范式设计的语言,有很多函数式的概念和范式,而这些会使得应用传统的对象继承模式变得困难。与通过类classs和接口interfaces来组织代码不同的是,Rust使用structtrait(特征)。下面就让我们开始探索,Rust中的独特设计究竟是怎样的一番体验,以及这些设计如何影响我们使用Rust中的数据结构来进行算法编程。

对象和行为

首当其冲的便是传统OOP语言中的类和继承机制。如果你写过OOP那么你对下面的这段代码将了如指掌:

1
2
3
4
5
6
7
class Door {
private bool is_open = false;

public void Open() {
this.is_open = true;
}
}

而在Rust语言中,这个模式定义要求所有Door的实例都拥有可变性,而可变性就涉及显示加锁以保证线程安全。与此同时,如果有新增一个类GlassDoor,这时如果没有类的继承机制那么就会产生重复的is_open定义,而重复代码将使得维护变得困难。

可变性mutability,在此处是Door实例对其内部数据is_open做出修改的能力。在Rust中所有变量默认都是不可变的,参见:rust-lang/rfcs#1

Rust建议使用trait实现共享的行为,Rust中的trait,中文译作特征,很像OOP语言中的虚类(trait的方法可以有默认实现,虚类也支持默认实现),Rust中的struct可以实现多个trait,下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Door {
is_open: bool
}

impl Door {
fn new(is_open: bool) -> Door {
Door { is_open: is_open }
}
}

trait Openable {
fn open(&mut self);
}

impl Openable for Door {
fn open(&mut self) {
self.is_open = true;
}
}

使用类比,struct Door{} 的定义可以看成是OOP编程语言中的class,而 impl Door{} 块则类比class中的方法实现,trait Openable{} 则类比interface的定义,impl Openable for Door{} 在Rust中是为某个struct实现某个trait的功能,可类比OOP中实现一个interface。对比是为了参照理解,但绝不是只是换个名词,它体现的是Rust的设计思想:数据结构定义和行为实现分离,行为实现可插拔。

传统OOP语言中的类,把数据成员和方法实现糅杂在一起,虽然C++可以用头部文件.h把定义和实现区分开但并不是强制性的。而Rust强制把这两者区分开了,做法就是通过声明一个struct,以及一个impl块来承载struct中具体拥有哪些方法和具体实现。与此同时Rust可以为struct实现多个trait,这使得trait的导入、共享、复用变得简单。利用traitgenerics泛型可以为已有struct真正无侵入的增加新trait的实现,下面就来展示一下。

假设我们已经有了一个分页的struct MyPaginate用来保存当前页数,以及对应的trait用来设置当前的页数和每页的条数,它们像下面这样:

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
37
38
39
40
41
42
43
44
45
46
trait Page {
fn set_page(&mut self, p: i32);
}

trait PerPage {
fn set_perpage(&mut self, num: i32);
}

#[derive(Debug)]
struct MyPaginate {
page: i32,
per_page: i32,
}

impl MyPaginate {
fn member_method(&self) {
println!("This is member method for struct which no relevant to trait.");
}
}

impl Page for MyPaginate {
fn set_page(&mut self, p: i32) {
self.page = p;
}
}

impl PerPage for MyPaginate {
fn set_perpage(&mut self, per_page: i32) {
self.per_page = per_page;
}
}

#[cfg(test)]
mod tests {
#[test]
fn test_trait_not_inherit() {
let mut my_paginate = MyPaginate {
page: 1,
per_page: 10,
};
my_paginate.set_page(2);
my_paginate.set_perpage(100);
println!("my_paginate: {:?}", my_paginate);
// output: my_paginate: MyPaginate { page: 2, per_page: 100 }
}
}

上面定义了2个trait并为struct实现了这2个trait,到这里还显示不出Rust中的trait有什么特别的地方,让我们接着往下看。假如这时我们需要引入一个新的trait并增加一个方法set_skip_page,希望能够设置可以跳过的页数,这个时候如何添加到代码中去呢?如果是在C中,接口是由抽象类来实现,而由于C只能单继承,如果要引入新的方法实现,那么我们势必需要在现有的抽象类/子类中新增方法实现,这就会侵入现有代码。而在Java中,接口通过interface来实现,类可以继承多个interface,如果我们要新增一个方法实现,我们可以引入新的interface,这样可以不改动已有的interface代码,但至少还得修改类定义新增一个 extends interface的声明。而在Rust中,上述2种情况都不需要修改!这得益于Rust中的genericstrait,给代码提供了强大的灵活性和表达性,下面看代码示例:

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
// 声明了一个新`trait`
trait Paginate {
fn new_method(&self, page: i32);
}

// 为MyPaginate实现新trait
impl Paginate for MyPaginate {
fn new_method(&self, page: i32) {
println!("In new_method, page: {}", page);
}
}

#[cfg(test)]
mod tests {
#[test]
fn test_new_trait() {
let mut my_paginate = MyPaginate {
page: 1,
per_page: 10,
};
my_paginate.member_method();
my_paginate.new_method(10);
// output: In new_method, page: 10
}
}

上面的例子是新增一个独立的trait实现,可以看到Rust可以在真正零侵入原structtrait的情况下,为struct实现了新trait的方法!这是不是比C++/Java更灵活呢?而且,结合trait inheritenceRust还能实现更强大的功能。下面来看一个需求,假设你的需求是新增一个新trait实现,但是实现这个trait的前提是你需要先实现Page,但是你又希望在设计上将两者区分开而不是糅杂在一起,这个时候trait inheritence就派上用场了,看例子:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 1.第一种语法为MyPaginate声明实现Paginate trait,Paginate需要依赖于Page
impl Paginate for MyPaginate
// where限定符声明了该实现针对的MyPaginate类型需要先实现了Page trait,
// 这是因为新增的方法实现用到了Page trait中的set_page函数
where
MyPaginate: Page,
{
fn set_skip_page(&mut self, page: i32) {
self.set_page(page + 1);
}
}

#[cfg(test)]
mod tests {
#[test]
fn test_trait_inherit() {
let mut my_paginate = MyPaginate {
page: 1,
per_page: 10,
};
my_paginate.set_page(1);
my_paginate.set_perpage(100);
my_paginate.set_skip_page(12);
println!("my_paginate: {:?}", my_paginate);
// output: my_paginate: MyPaginate { page: 13, per_page: 100 }
}
}

// 以上是针对单个具体类型MyPaginate为其实现了Paginate实现,
// 而Rust的泛型提供了更强大便捷的语法,可以为所有符合要求的类型实现Paginate,请看:

// 2.声明trait的时候使用继承语法,表示实现PaginateInherit trait之前需要先实现Page trait
trait PaginateInherit: Page {
fn set_skip_page_inherit(&mut self, page: i32);
}
// 为实现了Page的类型T实现PaginateInherit,也就是为目标类型实现PaginateInherit trait而已
impl<T: Page> PaginateInherit for T {
fn set_skip_page_inherit(&mut self, page: i32) {
self.set_page(page + 1);
}
}
// 而在此处的上下文中我们已实现了 impl Page for MyPaginate {}
// 所以此时MyPaginate就自动实现了PaginateInherit!

#[cfg(test)]
mod tests {
#[test]
fn test_trait_inherit() {
let mut my_paginate = MyPaginate {
page: 1,
per_page: 10,
};
my_paginate.set_page(1);
my_paginate.set_perpage(100);
my_paginate.set_skip_page_inherit(12);
println!("my_paginate: {:?}", my_paginate);
// output: my_paginate: MyPaginate { page: 13, per_page: 100 }
}
}

综上,我们展示了Rust如何使用structtrait(特征)将数据结构定义和行为实现相分离,Rust推荐优先使用组合而不是继承。通过将数据结构和行为实现相分离,不仅仅是编码语法上的改变,更是从设计思想上出发,将现实世界中的数据和行为抽象解耦,使多变的行为得以灵活扩展而不必影响少变的数据结构定义或干扰已有行为定义和实现。这将大大提高我们对现实的抽象灵活表达能力,而这,也将有助于我们打造出更安全可靠的系统,你觉得呢?