Swift Rust Kotlin 三种语言的枚举类型比较

2,250 阅读10分钟

本文比较一下 Swift Rust Kotlin 三种语言中枚举用法的异同。

定义比较

基本定义

  • Swift:
enum LanguageType {
    case rust
    case swift
    case kotlin
}
  • Rust:
enum LanguageType {
    Rust,
    Swift,
    Kotlin,
}
  • Kotlin:
enum class LanguageType {
    Rust,
    Swift,
    Kotlin,
}

三种语言都使用 enum 作为关键字,Swift 枚举的选项叫 case 一般以小写开头,Rust 枚举的选项叫 variant 通常以大写开头,Kotlin 枚举的选项叫 entry 以大写开头。kotlin 还多了一个 class 关键字,表示枚举类,具有类的一些特殊特性。在定义选项时,Swift 需要一个 case 关键字,而另外两种语言只需要逗号隔开即可。

带值的定义

三种语言的枚举在定义选项时都可以附带一些值。

  • 在 Swift 中这些值叫关联值(associated value),每个选项可以使用同一种类型,也可以使用不同类型,可以有一个值,也可以有多个值,值还可以进行命名。这些值在枚举创建时再进行赋值:
enum Language {
    case rust(Int)
    case swift(Int)
    case kotlin(Int)
}

enum Language {
    case rust(Int, String)
    case swift(Int, Int, String)
    case kotlin(Int)
}

enum Language {
    case rust(year: Int, name: String)
    case swift(year: Int, version: Int, name: String)
    case kotlin(year: Int)
}

let language = Language.rust(2015)
  • 在 Rust 中这些值也叫关联值(associated value),和 Swift 类似每个选项可以定义一个或多个值,使用相同或不同类型,但不能直接进行命名,同样也是在枚举创建时赋值::
enum Language {
    Rust(u16),
    Swift(u16),
    Kotlin(u16),
}

enum Language {
    Rust(u16, String),
    Swift(u16, u16, String),
    Kotlin(u16),
}

let language = Language::Rust(2015);

如果需要给属性命名可以将关联值定义为匿名结构体:

enum Language {
    Rust { year: u16, name: String },
    Swift { year: u16, version: u16, name: String },
    Kotlin { year: u16 },
}
  • 在 Kotlin 中选项的值称为属性或常量,所有选项的属性类型和数量都相同(因为它们都是枚举类的实例),而且值需要在枚举定义时就提供
enum class Language(val year: Int) {
    Rust(2015),
    Swift(2014),
    Kotlin(2016),
}

所以这些值如果是通过 var 定义的就可以修改:

enum class Language(var year: Int) {
    Rust(2015),
    Swift(2014),
    Kotlin(2016),
}

val language = Language.Kotlin
language.year = 2011

范型定义

Swift 和 Rust 定义枚举时可以使用范型,最常见的就是可选值的定义。

  • Swift:
enum Option<T> {
    case none
    case some(value: T)
}
  • Rust:
enum Option<T> {
    None,
    Some(T),
}

但是 Kotlin 不支持定义枚举时使用范型。

递归定义

Swift 和 Rust 定义枚举的选项时可以使用递归,即选项的类型是枚举自身。

  • Swift 定义带递归的选项时需要添加关键字 indirect:
enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
  • Rust 定义带递归的选项时需要使用间接访问的方式,包括这些指针类型: Box, Rc, Arc, &&mut,因为 Rust 需要在编译时知道类型的大小,而递归枚举类型的大小在没有引用的情况下是无限的。
enum ArithmeticExpression {
    Number(u8),
    Addition(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
    Multiplication(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
}

Kotlin 同样不支持递归枚举。

模式匹配

模式匹配是枚举最常见的用法,最常见的是使用 switch / match / when 语句进行模式匹配:

  • Swift:
switch language {
case .rust(let year):
    print(year)
case .swift(let year):
    print(year)
case .kotlin(let year):
    print(year)
}
  • Rust:
match language {
    Language::Rust(year) => println!("Rust was first released in {}", year),
    Language::Swift(year) => println!("Swift was first released in {}", year),
    Language::Kotlin(year) => println!("Kotlin was first released in {}", year),
}
  • Kotlin:
when (language) {
    Language.Kotlin -> println("Kotlin")
    Language.Rust -> println("Rust")
    Language.Swift -> println("Swift")
}

也可以使用 if 语句进行模式匹配:

  • Swift:
if case .rust(let year) = language {
    print(year)
}

// 或者使用 guard
guard case .rust(let year) = language else {
    return
}
print(year)

// 或者使用 while case
var expression = ArithmeticExpression.multiplication(
    ArithmeticExpression.multiplication(
        ArithmeticExpression.number(1),
        ArithmeticExpression.number(2)
    ),
    ArithmeticExpression.number(3)
)
while case ArithmeticExpression.multiplication(let left, _) = expression {
    if case ArithmeticExpression.number(let value) = left {
        print("Multiplied \(value)")
    }
    expression = left
}
  • Rust:
if let Language::Rust(year) = language {
    println!("Rust was first released in {}", year);
}

// 或者使用 let else 匹配
let Language::Rust(year) = language else {
    return;
};
println!("Rust was first released in {}", year);

// 还可以使用 while let 匹配
let mut expression = ArithmeticExpression::Multiplication(
    Box::new(ArithmeticExpression::Multiplication(
        Box::new(ArithmeticExpression::Number(2)),
        Box::new(ArithmeticExpression::Number(3)),
    )),
    Box::new(ArithmeticExpression::Number(4)),
);

while let ArithmeticExpression::Multiplication(left, _) = expression {
    if let ArithmeticExpression::Number(value) = *left {
        println!("Multiplied: {}", value);
    }
    expression = *left;
}

Kotlin 不支持使用 if 语句进行模式匹配。

枚举值集合

有时需要获取枚举的所有值。

  • Swift 枚举需要实现 CaseIterable 协议,通过 AllCases() 方法可以获取所有枚举值。同时带有关联值的枚举无法自动实现 CaseIterable 协议,需要手动实现。
enum Language: CaseIterable {
    case rust
    case swift
    case kotlin
}

let cases = Language.AllCases()
  • Rust 没有内置获取所有枚举值的方法,需要手动实现,另外带有关联值的枚举类型提供所有枚举值可能意义不大:
enum Language {
    Rust,
    Swift,
    Kotlin,
}

impl Language {
    fn all_variants() -> Vec<Language> {
        vec![
            Language::Rust,
            Language::Swift,
            Language::Kotlin,
        ]
    }
}
  • Kotlin 提供了 values() 方法获取所有枚举值,同时支持带属性和不带属性的:
val allEntries: Array<Language> = Language.values()

原始值

枚举类型还有一个原始值,或者叫整型表示的概念。

  • Swift 可以为枚举的每个选项提供原始值,在声明时进行指定。

Swift 枚举的原始值可以是以下几种类型:

  1. 整数类型:如 Int, UInt, Int8, UInt8 等。
  2. 浮点数类型:如 Float, Double
  3. 字符串类型:String
  4. 字符类型:Character
enum Language: Int {
    case rust = 1
    case swift = 2
    case kotlin = 3
}

带关联值的枚举类型不能同时提供原始值,而提供原始值的枚举可以直接从原始值创建枚举实例,不过实例是可选类型:

let language: Language? = Language(rawValue: 1)

在 Swift 中,提供枚举的原始值主要有以下作用:

  1. 数据映射:原始值允许枚举与基础数据类型(如整数或字符串)直接关联。这在需要将枚举值与外部数据(例如从 API 返回的字符串)匹配时非常有用。
  2. 简化代码:使用原始值可以简化某些操作,例如从原始值初始化枚举或获取枚举的原始值,而无需编写额外的代码。
  3. 可读性和维护性:在与外部系统交互时,原始值可以提供清晰、可读的映射,使代码更容易理解和维护。
  • Rust 使用 #[repr(…)] 属性来指定枚举的整数类型表示,并为每个变体分配一个可选的整数值,带和不带关联值的枚举都可以提供整数表示。
#[repr(u32)]
enum Language {
    Rust(u16) = 2015,
    Swift(u16) = 2014,
    Kotlin(u16) = 2016,
}

在 Rust 中,为枚举提供整数表示(整数值或整数类型标识)主要有以下作用:

  1. 与外部代码交互:整数表示允许枚举与 C 语言或其他低级语言接口,因为这些语言通常使用整数来表示枚举。
  2. 内存效率:指定整数类型可以控制枚举占用的内存大小,这对于嵌入式系统或性能敏感的应用尤为重要。
  3. 值映射:通过整数表示,可以将枚举直接映射到整数值,这在处理像协议代码或状态码等需要明确值的情况下很有用。
  4. 序列化和反序列化:整数表示简化了将枚举序列化为整数值以及从整数值反序列化回枚举的过程。
  • Kotlin 没有单独的原始值概念,因为它已经提供了属性。

方法实现

三种枚举都支持方法实现。

  • Swift
enum Language {
    case rust(Int)
    case swift(Int)
    case kotlin(Int)
    
    func startYear() -> Int {
        switch self {
        case .kotlin(let year): return year
        case .rust(let year): return year
        case .swift(let year): return year
        }
    }
}
  • Rust
enum Language {
    Rust(u16),
    Swift(u16),
    Kotlin(u16),
}

impl Language {
    fn start_year(&self) -> u16 {
        match self {
            Language::Rust(year) => *year,
            Language::Swift(year) => *year,
            Language::Kotlin(year) => *year,
        }
    }
}
  • Kotlin
enum class Language(var year: Int) {
    Rust(2015),
    Swift(2014),
    Kotlin(2016);

    fun yearsSinceRelease(): Int {
        val year: Int = java.time.Year.now().value
        return year - this.year
    }
}

它们还支持对协议、接口的实现:

  • Swift
protocol Versioned {
    func latestVersion() -> String
}

extension Language: Versioned {
    func latestVersion() -> String {
        switch self {
        case .kotlin(_): return "1.9.0"
        case .rust(_): return "1.74.0"
        case .swift(_): return "5.9"
        }
    }
}
  • Rust
trait Version {
    fn latest_version(&self) -> String;
}

impl Version for Language {
    fn latest_version(&self) -> String {
        match self {
            Language::Rust(_) => "1.74.0".to_string(),
            Language::Swift(_) => "5.9".to_string(),
            Language::Kotlin(_) => "1.9.0".to_string(),
        }
    }
}
  • Kotlin
interface Versioned {
    fun latestVersion(): String
}

enum class Language(val year: Int): Versioned {
    Rust(2015) {
        override fun latestVersion(): String {
            return "1.74.0"
        }
    },
    Swift(2014) {
        override fun latestVersion(): String {
            return "5.9"
        }
    },
    Kotlin(2016) {
        override fun latestVersion(): String {
            return "1.9.0"
        }
    };
}

这三者对接口的实现还有一个区别,Swift 和 Rust 可以在枚举定义之后再实现某个接口,而 Kotlin 必须在定义时就完成对所有接口的实现。不过它们都能通过扩展增加新的方法:

  • Swift
extension Language {
    func name() -> String {
        switch self {
        case .kotlin(_): return "Kotlin"
        case .rust(_): return "Rust"
        case .swift(_): return "Swift"
        }
    }
}
  • Rust
impl Language {
    fn name(&self) -> String {
        match self {
            Language::Rust(_) => "Rust".to_string(),
            Language::Swift(_) => "Swift".to_string(),
            Language::Kotlin(_) => "Kotlin".to_string(),
        }
    }
}
  • Kotlin
fun Language.name(): String {
    return when (this) {
        Language.Kotlin -> "Kotlin"
        Language.Rust -> "Rust"
        Language.Swift -> "Swift"
    }
}

内存大小

  • Swift 的枚举大小取决于其最大的成员和必要的标签空间。若包含关联值,枚举的大小会增加以容纳这些值。可以使用 MemoryLayout 来估算大小。
enum Language {
    case rust(Int, String)
    case swift(Int, Int, String)
    case kotlin(Int)
}

print(MemoryLayout<Language>.size) // 33
  • Rust 中的枚举大小通常等于其最大变体的大小加上一个用于标识变体的额外空间。使用 std::mem::size_of 来获取大小,提供了整型表示的枚举类型大小等于整型值大小加上最大变体大小,同时还要考虑内存对齐的因素。
enum Language1 {
    Rust,
    Swift,
    Kotlin,
}
println!("{} bytes", size_of::<Language1>()); // 1 bytes

#[repr(u32)]
enum Language2 {
    Rust,
    Swift,
    Kotlin,
}
println!("{} bytes", size_of::<Language2>()); // 4 bytes

#[repr(u32)]
enum Language3 {
    Rust(u16),
    Swift(u16),
    Kotlin(u16),
}
println!("{} bytes", size_of::<Language3>()); // 8 bytes

对于上面例子中 Language3 的内存大小做一个简单解释:

  • 枚举的变体标识符(因为 #[repr(u32)])占用 4 字节。
  • 最大的变体是一个 u16 类型,占用 2 字节。
  • 可能还需要额外的 2 字节的填充,以确保整个枚举的内存对齐,因为它的对齐要求由最大的 u32 决定。

因此,总大小是 4(标识符)+ 2(最大变体)+ 2(填充)= 8 字节。

  • Kotlin:在 JVM 上,Kotlin 枚举的大小包括对象开销、枚举常量的数量和任何附加属性。Kotlin 本身不提供直接的内存布局查看工具,需要依赖 JVM 工具或库。

兼容性

Swift 枚举有时需要考虑与 Objective-C 的兼容,Rust 需要考虑与 C 的兼容。

  • 在 Swift 中,要使枚举兼容 Objective-C,你需要满足以下条件:
  1. 原始值类型:Swift 枚举必须有原始值类型,通常是 Int,因为 Objective-C 不支持 Swift 的关联值特性。

  2. 遵循 @objc 协议:在枚举定义前使用 @objc 关键字来标记它。这使得枚举可以在 Objective-C 代码中使用。

    @objc
    enum Language0: Int {
        case rust = 1
        case swift = 2
        case kotlin = 3
        
        func startYear() -> Int {
            switch self {
            case .kotlin: return 2016
            case .rust: return 2015
            case .swift: return 2014
            }
        }
    }
    
  3. 限制:使用 @objc 时,枚举不能包含关联值,必须是简单的值列表。

这样定义的枚举可以在 Swift 和 Objective-C 之间交互使用,适用于混合编程环境或需要在 Objective-C 项目中使用 Swift 代码的场景。

  • Rust 枚举与 C 兼容,只需要使用 #[repr(C)] 属性来指定枚举的内存布局。这样做确保枚举在内存中的表示与 C 语言中的枚举相同。
#[repr(C)]
enum Language0 {
    Rust,
    Swift,
    Kotlin,
}

注意:在 Rust 中,你不能同时在枚举上使用 #[repr(C)]#[repr(u32)]。每个枚举只能使用一个 repr 属性来确定其底层的数据表示。如果你需要确保枚举与 C 语言兼容,并且具有特定的基础整数类型,你应该选择一个符合你需求的 repr 属性。例如,如果你想让枚举在内存中的表示与 C 语言中的 u32 类型的枚举相同,你可以使用 #[repr(u32)]

  • 在 Kotlin 中,枚举类(Enum Class)是与 Java 完全兼容的。Kotlin 枚举可以自然地在 Java 代码中使用,反之亦然。