枚举类型
自语言版本 2.0 起
枚举类型与结构体类型相似,但支持定义数据布局的多种变体。每个变体都有其独特的字段集合。枚举变体可在表达式、测试工具、匹配和解构中使用。
枚举类型声明
枚举类型声明会列出不同变体的数量,如下例所示:
enum Shape {
Circle{radius: u64},
Rectangle{width: u64, height: u64}
}
枚举变体可以有零个或多个字段。如果没有参数,大括号也可以省略,声明为简单值:
enum Color {
Red, Blue, Green
}
与结构体类型类似,枚举类型也可以具备能力。例如,Color
枚举类型可以声明为可复制、可丢弃和可存储的,就像原始数字类型:
enum Color has copy, drop, store, key { Red, Blue, Green }
枚举类型也可以拥有 key
能力,并作为全局存储中的数据根。在此上下文中,枚举的一个常见用途是数据版本控制:
enum VersionedData has key {
V1{name: String}
V2{name: String, age: u64}
}
与结构体类似,枚举类型可以是泛型的,并接受位置参数。例如,下面的类型表示一个泛型结果类型,其中变体构造函数使用位置参数而非命名参数(另见位置结构体)。
enum Result<T> has copy, drop, store {
Err(u64),
Ok(T)
}
构造枚举值
枚举值的构造方式与结构体值类似:
let s: String;
let data = VersionedData::V1{name: s};
如果枚举变体没有字段,大括号也可以省略:
let color = Color::Blue;
枚举变体的名称解析
枚举的变体名称需要通过枚举类型名称来限定,如 VersionedData::V1
。
注意:目前不支持通过
use
子句为枚举变体设置别名,但将在后续语言版本中添加
在某些情况下(如下面的匹配表达式),Move 编译器可以从上下文中推断出枚举类型,此时可以省略类型名称限定:
fun f(data: VersionedData) {
match (data) { V1{..} => .., ..} // 简单的变体名称即可
}
匹配枚举值
可以使用匹配表达式来检查枚举值。例如:
fun area(self: &Shape): u64 {
match (self) {
Circle{radius} => mul_with_pi(*radius * *radius),
Rectangle{width, height} => *width * *height
}
}
注意上面的匹配值是对枚举值的不可变引用。匹配表达式也可以消耗一个值,或通过可变引用来匹配以进行内部更新:
fun scale_radius(self: &mut Shape, factor: u64) {
match (self) {
Circle{radius: r} => *r = *r * factor,
_ => {} // 如果不是 Circle 则不做任何操作
}
}
```匹配表达式中的模式会按照文本出现的顺序依次求值,直到找到匹配项。如果未覆盖所有已知模式,将产生编译时错误。
模式可以嵌套并包含条件,如下例所示:
```move
let r : Result<Result<u64>> = Ok(Err(42));
let v = match (r) {
Ok(Err(c)) if c < 42 => 0,
Ok(Err(c)) if c >= 42 => 1,
Ok(_) => 2,
_ => 3
};
assert!(v == 1);
注意上例中,最后一个匹配子句(_
)同时覆盖了模式 Ok(Err(_))
和 Err(_)
。虽然在运行时,前面的子句会匹配所有 c
值的 Ok(Err(c))
,但由于条件限制,编译器无法确定所有情况都被覆盖:匹配表达式中的条件在覆盖率检查时不被考虑。因此前两个匹配子句不足以确保匹配完整性,需要额外子句来避免编译器报错。
测试枚举变体
使用 is
操作符可以检查枚举值是否属于特定变体:
let data: VersionedData;
if (data is VersionedData::V1) { .. }
该操作符允许指定以”|
”分隔的变体列表。如果被测试表达式的类型已知,变体无需通过枚举名限定:
assert!(data is V1|V2);
从枚举值中选择字段
可以直接从枚举值中选择字段。回顾版本化数据的定义:
enum VersionedData has key {
V1{name: String}
V2{name: String, age: u64}
}
可以编写如下代码直接选择变体字段:
let s: String;
let data1 = VersionedData::V1{name: s};
let data2 = VersionedData::V2{name: s, age: 20};
assert!(data1.name == data2.name)
assert!(data2.age == 20);
注意当枚举值没有对应字段的变体时(如 data1.age
),字段选择会中止。该情况使用的中止码为 0xCA26CBD9BE0B0001
。根据 std::error
规范,此代码的类别为 std::error::INTERNAL
,原因为 1
。
仅当字段在所有变体中具有唯一名称和类型时才能进行选择。因此以下代码会产生编译时错误:
enum VersionedData has key {
V1{name: String}
V2{name: u64}
}
data.name
// ^^^^^ 编译时错误:`name` 字段选择存在歧义
在Let语句中使用枚举模式
枚举变体模式可用于 let
语句:
let data: VersionData;
let V1{name} = data;
如果变体不符合预期,解包枚举值会中止。为确保处理所有枚举变体,建议使用 match
表达式而非 let
。match
在编译时检查确保覆盖所有变体。某些情况下,可使用 Move Prover 等工具验证 let
不会发生意外中止。
通过模式匹配销毁枚举
与结构体类似,可以通过显式解包来销毁枚举值。枚举可通过以下方式解包:
match
表达式中的模式匹配let
绑定中的枚举模式- 赋值语句中的枚举模式移动
// 注意:`Shape` 没有 `drop` 能力,因此必须通过显式解构来销毁。
enum Shape {
Circle{radius: u64},
Rectangle{width: u64, height: u64}
}
fun destroy_empty(self: Shape) {
match (self) {
Shape::Circle{radius} => assert!(radius == 0),
Shape::Rectangle{width, height: _} => assert!(width == 0),
}
}
fun example_destroy_shapes() {
let c = Shape::Circle{radius: 0};
let r = Shape::Rectangle{width: 0, height: 0};
c.destroy_empty();
r.destroy_empty();
}
枚举类型升级兼容性
当且仅当新类型仅在变体列表末尾添加新变体时,枚举类型可以被另一个枚举类型升级。旧枚举类型中的所有变体都必须出现在新类型中,且顺序必须保持一致并从开头开始排列。考虑 VersionedData
类型,它最初可能只有一个版本:
enum VersionedData has key {
V1{name: String}
}
该类型可以升级到本文目前使用的版本:
enum VersionedData has key {
V1{name: String}
V2{name: String, age: u64}
}
以下升级将不被允许,因为必须保持变体顺序:
enum VersionedData has key {
V2{name: String, age: u64} // 非兼容性升级
V1{name: String}
}