在学习Rust之前,我还真不知道代数数据类型(ADTs)这个概念。从Rust文档中大概了解它包含“求和类型”和“乘积类型”,看似很简单,但我打算展开学习一下代数数据类型。
先看一段很教科书的定义:
抽象数据类型(Abstract Data Types,简称ADTs)是计算机科学中的一种概念,用于在不关心数据表示的具体细节的前提下,定义数据的方法。ADTs常由“构造”或“复合”两种基础方式定义,它们通常称为乘积类型(product types,如结构体或记录)和求和类型(sum types,如联合体或变体)。乘积类型和求和类型分别对应数学概念笛卡尔积和不交并。
白话理解一下:乘积类型就是结构体那一种,组合多个其他类型。而求和类型就是用或关系定义新类型。
用伪代码举个例子:
// 定义圆形
type Circle {
x: Float
y: Float
radius: Float
}
// 定义矩形
type Rectangle {
x: Float
y: Float
width: Float
height: Float
}
// 定义形状,它可以是圆形或矩形
type Shape = Circle | Rectangle
注意:这里千万不要用继承的方式去理解Shape和Circle、Rectangle的关系。
看似很简单的定义,但是背后有类型论(type theory)和范畴论(category theory)是数学领域中的理论支撑,我们不必理解那么深,只需要详细通过构造和复合两种方法就可以定义所有我们需要的类型。这种情况在关系数据库中也很常见,在我们看来就是表、字段的定义,还有外键等就可以描述任意复杂的数据模式,其背后也有数学理论支持。
传统的程序设计结构—顺序(sequence)、判断(selection)和循环(iteration)—确实构成了编程的基本构建块,它们是由图灵完备性理论支持的,这表明只要有了这些结构,就可以计算任何可计算的函数。
代数数据类型的一些好处
避免非法状态
type LoginUserInfo {
id: string;
name: string;
emailInfo: EmailInfo;
};
type GuestUserInfo {
name: string;
};
type UserInfo = LoginUserInfo | GuestUserInfo;
在上面的例子中定义了三个类型UserInfo、LoginUserInfo、GuestUserInfo。你的函数中可以接收UserInfo类型参数,然后用类似Switch的语句处理每一种情况。如果在OOP中,你一般这样用class来描述:
class UserInfo{
boolean isLogin;
String name,
Stirng id,
Stirng emailInfo
}
如果我构造一个对象
new UserInfo(
false,"Jimi","123","jimi@123.com"
)
虽然可以通过编译、运行也是正常的。但是从领域模型来看,这个就是一个错误的状态,因为未登录用户居然设置了id和email信息。只要程序中出现了错误状态的数据,那么就有可能制造bug、或者增加额外的预防代码来检测错误状态。所以求和类型、乘积类型组合在一起可以很好的描述领域知识,避免错误状态。ADT与DDD是一对好朋友。
对编译器的保证
求和类型定义时,一个类型由哪几个类型或关系组合而成,就是确定不可变的,即使在运行时也不可变。这就和枚举类型的作用一样,那么在类似Switch语句中,编译器可以检查代码是否处理了每一种情况。另外我猜测也是因为这种特性,让ADTs类型在模式匹配上,更容易被编译器实现吧。
Rust枚举
struct Circle {
x: f64,
y: f64,
radius: f64,
}
// 堆代码 duidaima.com
// 定义矩形
struct Rectangle {
x: f64,
y: f64,
width: f64,
height: f64,
}
// 定义形状,它可以是圆形或矩形
enum Shape {
Circle(Circle),
Rectangle(Rectangle),
}
fn area(shape: Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(5.0);
let rectangle = Shape::Rectangle(4.0, 3.0);
println!("Circle area: {}", area(circle));
println!("Rectangle area: {}", area(rectangle));
}
上面的Shape用Rust的结构体和枚举来表达。
在许多传统的编程语言中,继承是解决问题的基石。面向对象编程(OOP)利用了这一概念,提供了一个层次结构的设计,在这个层次结构中,子类继承和扩展父类的功能。然而,我们发现继承并不总是理想的解决方案。当要描述一组固定的类型关系时,尤其是当这些类型关系代表客观事实而不是一个变化的层次结构时,Rust 的枚举类型(enum)提供了一个更为安全且表达性强的选择。
Rust 的枚举是一种代数数据类型(ADT),它可以拥有多个变体,每个变体都可以有不同的类型和数量的关联数据。这里的关键优点之一是在编译时枚举的所有可能变体都是已知的,这确保了类型安全。程序员不必担心某个对象不符合预期的类型,因为所有可能的类型已在编译前得到严格的验证。
枚举最常见的应用之一是描述有限集合的状态或类别。例如,在一个网络服务器中,请求的状态可能有 “接收中”、“处理中”、或者 “已完成”。在 Rust 中,这可以简洁地通过枚举来表达,而每个状态都可以携带相关的信息,例如接收到的数据片段、处理的进度,或是完成的响应。
enum RequestState {
Receiving(DataChunk),
Processing(Progress),
Completed(Response),
}
在错误处理方面,Rust 的枚举显示出其真正的优势。通过 Result 和 Option 枚举,它提供了一种表达操作可能的成功或失败的优雅方式,同时还包含了相应的上下文信息或错误消息。
enum Result<T, E> {
Ok(T),
Err(E),
}
enum Option<T> {
Some(T),
None,
}
在使用继承的 OOP 中,子类共享父类的接口,并且可以扩展新的行为。这导致了所谓的 “是一个” 关系。然而,这种关系可能会变得复杂,尤其是当多重继承和深层次结构体系的问题开始浮现时。枚举提供了一个不同的模型,一个 “可以是一个” 的模型,它更适合表达静态多态性,即在编译时决定对象的具体类型。
使用 Rust 枚举,不同的数据类型可以被整齐地封装在一个单一的类型中,通过模式匹配来进行解构和访问。这样避免了层次结构中的自动类型转换和隐藏的复杂性,引导程序员编写明确、直接和可靠的代码。
Rust对ATDs实现,提供了一个可靠、高效且灵活的架构,让程序员能够以一种新的、编译时保障安全的方式思考和解决问题。虽然 Rust 并不直接模仿传统的面向对象模型,它确实提供了一种强有力的多态性替代方案,适用于现代软件设计的潮流。通过枚举,Rust 在编译时确保每个变量的类型都是正确和预期的因此,掌握枚举背后的理论支撑,掌握Rust 的枚举类型是值得信赖和依靠的强大工具,帮你更好理解和使用Rust。