Swift科普文《枚举(enum)》
Swift科普文《枚举(enum)》

Swift科普文《枚举(enum)》

时至今日,已经在做两个简单的应用,还是很少涉及到使用枚举。因为近期在学习throws时,如果尝试使用自定义错误类型,推荐使用枚举来表示错误,它可以涵盖各种错误情况。

// 定义自定义错误类型
enum NetworkError: Error {
    case invalidURL
    case noConnection
    case timeout
}

// 抛出自定义错误
func loadData(from url: String) throws -> String {
    guard url == "https://valid.url" else {
        throw NetworkError.invalidURL
    }
    // 假设还有其他情况可能导致错误
    throw NetworkError.timeout
}

// 捕获并处理自定义错误
do {
    let result = try loadData(from: "https://invalid.url")
    print(result)
} catch NetworkError.invalidURL {
    print("Invalid URL provided.")
} catch NetworkError.timeout {
    print("Request timed out.")
} catch {
    print("An unexpected error occurred: \(error)")
}

什么是枚举

Swift 中的枚举 (enum) 是一种强大的数据类型,可以定义一组相关的值,并为这些值提供更结构化和安全的操作方式。Swift 的枚举比许多其他语言中的枚举更灵活,因为它们可以:

  1. 关联值 – 枚举的每个成员可以存储不同类型的关联数据。
  2. 原始值 – 枚举的成员可以以某种默认类型(如整数、字符串)初始化。
  3. 方法 – 枚举可以定义方法,允许在枚举成员上调用。

定义一个简单的枚举

enum CompassDirection {
    case north
    case south
    case east
    case west
}

使用枚举:

var direction = CompassDirection.north
direction = .west

关联值

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGH")

可以使用 switch 语句处理关联值:

switch productBarcode {
    case .upc(let numberSystem, let manufacturer, let product, let check):
        print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
    case .qrCode(let code):
        print("QR Code: \(code).")
}

原始值

可以为枚举提供默认的初始值,这种情况下称为原始值(Raw Values):

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

let earth = Planet(rawValue: 3) // 返回 .earth

在这个例子中,Planet 枚举的每个 case 都有一个整数作为原始值。因为 mercury 被显式设为 1,后续的 case 会依次自动递增:venus 为 2,earth 为 3,以此类推。

访问原始值

可以通过 rawValue 属性访问枚举 case 的原始值。例如:

let planet = Planet.earth
print("Earth's raw value is \(planet.rawValue)") // 输出: Earth's raw value is 3

根据原始值创建枚举实例

可以使用 init?(rawValue:) 通过原始值创建枚举实例。如果给定的原始值没有对应的枚举 case,则会返回 nil,这是一个可失败的初始化。

if let planet = Planet(rawValue: 3) {
    print("Planet with raw value 3 is \(planet)") // 输出: Planet with raw value 3 is earth
} else {
    print("No planet found with that raw value.")
}

if let unknownPlanet = Planet(rawValue: 9) {
    print("Planet with raw value 9 is \(unknownPlanet)")
} else {
    print("No planet found with that raw value.") // 输出: No planet found with that raw value.
}

使用场景

1、映射和转换:原始值使得枚举 case 和外部数据源(如数据库、API 或文件)之间的映射变得简单。例如,可以将 Planet 的原始值作为数据库中的 ID 存储,并在需要时通过 rawValue 将其还原为枚举 case。

2、简单的数据表示:当你有固定的整数、字符串或其他简单值时,可以将这些值直接映射为枚举的原始值,避免使用魔法数或硬编码的字符串。例如,用枚举表示 HTTP 状态码:

enum HTTPStatusCode: Int {
    case ok = 200
    case notFound = 404
    case internalServerError = 500
}

let status = HTTPStatusCode(rawValue: 404)
if status == .notFound {
    print("Page not found.")
}

3、自动增量值:如果需要为枚举 case 提供自动递增的数值(如计数器),可以从某个特定的值开始,并利用自动递增功能。例如,星期几的定义:

enum DayOfWeek: Int {
    case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday
}

在自动增量值设置方面,当我们不给第一个case,而是给指定的case赋值时,后续的case会自动从这个值开始递增。

enum DayOfWeek: Int {
    case sunday, monday, tuesday, wednesday = 4, thursday, friday, saturday
}
  1. sunday 自动获得 0
  2. monday 自动获得 1
  3. tuesday 自动获得 2
  4. wednesday 被你手动指定为 4
  5. 后面的 thursday 会自动递增为 5
  6. friday 会是 6
  7. saturday 会是 7

输出效果

print(DayOfWeek.sunday.rawValue)     // 输出: 0
print(DayOfWeek.monday.rawValue)     // 输出: 1
print(DayOfWeek.tuesday.rawValue)    // 输出: 2
print(DayOfWeek.wednesday.rawValue)  // 输出: 4
print(DayOfWeek.thursday.rawValue)   // 输出: 5
print(DayOfWeek.friday.rawValue)     // 输出: 6
print(DayOfWeek.saturday.rawValue)   // 输出: 7

当你手动指定 wednesday = 4 后,Swift 会从这个值开始向后递增计算。因此,这种方式适合在你希望有部分 case 手动设置值,其他 case 仍然使用自动递增的场景。

如果你将 wednesday 的原始值设置为 0,那么 sunday 和 wednesday 都会有相同的原始值 0。这是允许的,但需要注意的是,这样会导致两个不同的 case 有相同的原始值,这可能会引发一些问题。

enum DayOfWeek: Int {
    case sunday, monday, tuesday, wednesday = 0, thursday, friday, saturday
}

在这个枚举中:

  1. sunday 的原始值自动设置为 0(因为它是第一个 case,自动递增从 0 开始)
  2. wednesday 被你手动设置为 0
  3. monday 和 tuesday 仍然会自动递增,即 1 和 2
  4. thursday, friday, 和 saturday 从 wednesday 的 0 开始自动递增,变为 1, 2, 3

输出效果

print(DayOfWeek.sunday.rawValue)     // 输出: 0
print(DayOfWeek.monday.rawValue)     // 输出: 1
print(DayOfWeek.tuesday.rawValue)    // 输出: 2
print(DayOfWeek.wednesday.rawValue)  // 输出: 0
print(DayOfWeek.thursday.rawValue)   // 输出: 1
print(DayOfWeek.friday.rawValue)     // 输出: 2
print(DayOfWeek.saturday.rawValue)   // 输出: 3

注意事项

多个相同原始值:sunday 和 wednesday 具有相同的原始值 0。这是允许的,但如果你用 DayOfWeek(rawValue: 0) 初始化一个枚举实例时,Swift 只会返回找到的第一个匹配项。因此,在这个例子中,DayOfWeek(rawValue: 0) 会返回 .sunday,而不会返回 .wednesday。

if let day = DayOfWeek(rawValue: 0) {
    print(day)  // 输出: sunday
}

潜在问题:当你有相同的原始值时,可能会引发歧义或错误结果。如果你想使用 rawValue 从原始值创建枚举实例,就无法区分 sunday 和 wednesday,因为它们都有相同的 0。

无论如何,通过访问原始值,Swift 枚举不仅可以更好地表达常量,还可以方便地与外部系统或数据源进行交互。

递归枚举

递归枚举(Recursive Enumerations) 是一种可以在定义自身的情况下引用自身的枚举。这意味着枚举中的某些 case 可以包含该枚举类型的实例作为关联值。这种特性使得递归结构(如树结构或链表)在枚举中表示变得可能。

由于 Swift 需要知道内存布局,因此在定义递归枚举时需要使用 indirect 关键字。这告诉编译器使用间接方式(即引用类型)来存储关联值,以便支持递归。

使用 indirect 关键字

有两种方式定义递归枚举:

1、在整个枚举上使用 indirect

2、在单个 case 上使用 indirect

示例 1: 简单的递归枚举

假设我们要表示一个基本的数学表达式,可以是一个数字,也可以是两个表达式的加法:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

或者可以在枚举声明的最前面使用 indirect:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

这两个递归枚举的定义在功能上是相同的,但它们在使用 indirect 关键字的方式上有所不同。

第一种定义:局部使用 indirect

在这个定义中,indirect 关键字只应用在具体的 case 上。也就是说,只有 addition 和 multiplication 这两个枚举成员是递归的,使用了间接存储。number 这个 case 不是递归的,因此它不需要 indirect。

第二种定义:在整个枚举上使用 indirect

在这个定义中,indirect 关键字应用在整个枚举上。这意味着所有的枚举成员,包括 number、addition 和 multiplication,都会使用间接存储方式。整个枚举都是递归的,允许 ArithmeticExpression 引用自身。

区别和影响

1、内存使用:

在第一种定义中,只有需要递归的 case 才会使用间接存储,因此在某些情况下可能会节省一些内存。例如,number 直接存储整数,不需要通过间接存储。

在第二种定义中,所有的 case 都使用间接存储,即使 number 只是简单地存储一个整数,这可能会略微增加内存消耗。

2、代码简洁性:

第一种定义更灵活,可以根据需要指定递归的 case,而不必在整个枚举中都使用间接存储。

第二种定义则更加简洁,因为你不需要为每个递归的 case 单独添加 indirect 关键字。这在枚举中的递归 case 较多时显得更简洁。

使用场景

递归枚举适用于需要递归结构的场景,例如:

数学表达式解析:表达式可以递归地包含子表达式,如 (2 + 3) * 4。

树状结构:表示节点之间的递归关系,如文件系统、组织树。

链表:一个节点可以递归地引用下一个节点,直到结束。

示例 2: 计算递归枚举的值

以下代码定义了一个递归枚举,用于计算数学表达式的值:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)

    // 递归计算表达式的值
    func evaluate() -> Int {
        switch self {
        case .number(let value):
            return value
        case .addition(let left, let right):
            return left.evaluate() + right.evaluate()
        case .multiplication(let left, let right):
            return left.evaluate() * right.evaluate()
        }
    }
}

// 构建表达式:(5 + 4) * 2
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let two = ArithmeticExpression.number(2)
let product = ArithmeticExpression.multiplication(sum, two)

print(product.evaluate()) // 输出: 18

在上面的递归枚举当中,输出的内容为18,有人可能不理解为什么定义了evaluate(),但是没有使用并且输出了对应的值。

这是因为我们在输出product.evaluater()时,调用了evaluater()方法。因此看起来从five、four一直到product都没有使用evalute()方法,实际上是从product反向递归调用。

因此,当我们在print中输出product.evaluate ()时,我们调取了product的evaluate()方法:

  1. 在evaluater()方法中,因为product是.multiplication,因此进入case .multiplication,它包含了sum和two两个枚举。
  2. 因此调取sum和two的evaluater()方法,因为sum是.addition,因此进入case .addition,它包含了five和four两个枚举。
  3. 因此调取five和four的evaluater()方法,因为five和four对应的是.number,因此通过evaluater()分别返回5和4两个数值。
  4. 最后sum返回9(5+4),product返回18。

递归调用 evaluate() 是因为 ArithmeticExpression 枚举可以嵌套表达式。每个 addition 和 multiplication 可能包含更多的表达式,而递归调用能够一步步求解每一个子表达式的值,最终合成完整的结果。

递归枚举也是一种强大的工具,可以用于表示嵌套或递归数据结构。通过使用 indirect 关键字,Swift 可以处理引用自身的情况,这样就能够用简单的方式实现诸如树、链表和递归数学表达式等复杂结构。

枚举的方法

在 Swift 中,枚举不仅可以定义简单的值,还可以包含方法。通过在枚举中添加方法,你可以让枚举变得更加强大和灵活。这些方法可以操作枚举的关联值、执行计算、或者提供额外的功能。

枚举中的方法类型

枚举中可以定义的主要方法类型包括:

1、实例方法

2、类型方法

enum LightSwitch {
    case on, off

    mutating func toggle() {
        self = self == .on ? .off : .on
    }
}

var light = LightSwitch.off
light.toggle() // 现在是 .on
实例方法

实例方法是与枚举的某个具体实例关联的方法。你可以在枚举中定义这些方法来执行操作或计算。例如,在交通信号灯的枚举中,你可以定义一个方法来获取下一个信号。

enum TrafficLight {
    case red, yellow, green

    func nextLight() -> TrafficLight {
        switch self {
        case .red:
            return .green
        case .yellow:
            return .red
        case .green:
            return .yellow
        }
    }
}

var currentLight = TrafficLight.red
print(currentLight) // 输出:red

currentLight = currentLight.nextLight()
print(currentLight) // 输出:green

currentLight = currentLight.nextLight()
print(currentLight) // 输出:yellow

在这个例子中,nextLight() 是一个实例方法,它基于当前信号灯的状态,返回下一个状态。

类型方法

类型方法是属于整个枚举类型的方法,而不是某个具体的实例。类型方法用 static 关键字定义。

enum MathConstants {
    static func pi() -> Double {
        return 3.14159
    }
}

print(MathConstants.pi()) // 输出:3.14159

在这个例子中,pi() 是一个类型方法,可以通过 MathConstants.pi() 直接调用,而不需要创建一个实例。

枚举方法的实际应用

以下是一些实际中可能会用到枚举方法的场景:

1、关联值操作:

如果你的枚举有关联值,可以定义方法来操作这些值。例如,解码 QR 码或 UPC 码:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)

    func displayCode() -> String {
        switch self {
        case .upc(let numberSystem, let manufacturer, let product, let check):
            return "UPC: \(numberSystem)-\(manufacturer)-\(product)-\(check)"
        case .qrCode(let code):
            return "QR Code: \(code)"
        }
    }
}

let qr = Barcode.qrCode("ABCDEFG")
print(qr.displayCode()) // 输出:QR Code: ABCDEFG

let upc = Barcode.upc(8, 85909, 51226, 3)
print(upc.displayCode()) // 输出:UPC: 8-85909-51226-3

2、递归计算:

在前面的例子中,我们看到递归枚举如何使用 evaluate() 方法计算数学表达式。

3、状态机:

枚举常用来实现状态机。例如,跟踪网络请求状态:

enum NetworkRequestState {
    case idle
    case loading
    case success(Data)
    case failure(Error)

    func description() -> String {
        switch self {
        case .idle:
            return "Request is idle."
        case .loading:
            return "Request is loading."
        case .success(let data):
            return "Request succeeded with data of size: \(data.count) bytes."
        case .failure(let error):
            return "Request failed with error: \(error.localizedDescription)"
        }
    }
}

枚举方法可以帮助你把枚举的行为封装在一起,使代码更加模块化和易于理解。

通过实例方法和类型方法,枚举可以提供更强大的功能,例如递归计算、关联值处理、状态机管理等。

定义方法时,你可以根据需要使用实例方法(和具体实例关联)或者类型方法(和整个类型关联)。

这种扩展方式让枚举不仅仅是一个数据集合,还能具备特定的行为逻辑,增强代码的表现力。

枚举赋值语句

enum CompassDirection {
    case north
    case south
    case east
    case west
}

var direction = CompassDirection.north
direction = .west

在Swift中,当你给变量 direction 赋值为 .west 时,实际上是一种语法简化。由于 direction 的类型已经被明确为 CompassDirection,因此在后续的赋值中不需要再重复写出完整的 CompassDirection.west,可以直接使用 .west。

在 Swift 中,类型推断机制让代码更加简洁。当编译器已经知道 direction 是 CompassDirection 类型时,你可以省略枚举类型的名称,只写出枚举的成员即可。具体如下:

var direction: CompassDirection = .north // 明确指定类型,使用 .north 简写
direction = .west // 可以省略类型名称,直接使用 .west

等价于:

var direction: CompassDirection = CompassDirection.north // 完整写法
direction = CompassDirection.west // 完整写法

这种语法简化在 Swift 中很常见,让代码看起来更简洁,同时减少冗余,尤其是当同一个枚举类型频繁使用时。

关联值的使用场景

Swift 枚举中的关联值 (Associated Values) 允许枚举的每个成员存储不同类型的相关数据。这种功能使得枚举不仅仅是一个静态的值集合,还能存储具体的数据,使得代码更加灵活和表达性更强。

关联值的使用场景非常广泛,通常用于表达以下几类情况:

1、处理不同的数据格式或结构:例如,条形码可以有多种类型,每种类型包含不同的数据结构。通过关联值,可以为每种情况提供不同的数据类型和数量。例如,上面的 Barcode 可以表示 UPC 或 QR Code,并且每种类型的数据都不同。

2、状态与数据绑定:在许多情况下,你希望状态和相关的数据紧密关联。比如,网络请求的状态可能有 .success 和 .failure,并且每种情况都需要存储不同的信息:

enum NetworkResult {
    case success(data: Data)
    case failure(error: Error)
}
let input = "fangjunyu.com"
let result = NetworkResult.success(data: Data(input.utf8))
switch result {
case .success(let data):
    print("Success: \(String(decoding: data, as: UTF8.self))")
case .failure(let error):
    print("Failure: \(error.localizedDescription)")
}

3、表达多种模式:关联值让你可以用一种统一的结构来表达多种情况。例如,在处理用户输入时,可以定义多种不同的输入模式:

enum InputField {
    case text(String)
    case password(String)
    case email(String)
    case number(Int)
}

4、灵活的参数传递:在需要传递不同类型或数量的参数时,关联值提供了一个方便的方式。例如,可以使用 enum 来定义不同类型的消息或命令,每个命令会带有不同的参数:

enum Command {
    case start
    case stop
    case pause(duration: Int)
    case restart(retries: Int)
}

因此,在使用关联值的过程中,需要通过Switch语句与关联值结合使用,从而解包并操作这些数据。

throws抛出的枚举错误

在Swift中,当throws方法抛出错误并切这些错误由枚举表示时,错误会以enum case的形式返回。通常枚举会遵循Error协议,从而成为可抛出的错误类型。

定义枚举错误类型

假设我们定义一个枚举来表示各种可能的错误:

enum FileError: Error {
    case notFound
    case insufficientPermissions
    case unknownError
}

抛出错误

在使用 throws 的函数中,可以抛出这些枚举类型的错误:

func readFile(filename: String) throws {
    if filename.isEmpty {
        throw FileError.notFound
    } else if filename == "restricted.txt" {
        throw FileError.insufficientPermissions
    } else {
        // 正常读取文件逻辑
    }
}

使用 do-catch 处理错误

在调用抛出错误的方法时,可以用 do-catch 语句来处理这些错误:

do {
    try readFile(filename: "restricted.txt")
} catch FileError.notFound {
    print("Error: File not found.")
} catch FileError.insufficientPermissions {
    print("Error: Insufficient permissions to read the file.")
} catch {
    print("Error: \(error).")
}

错误的呈现

当函数抛出错误时,它返回的错误对象是 enum case。例如,如果 readFile(filename:) 抛出了 .insufficientPermissions 错误,则会匹配到对应的 catch 分支,输出如下:

Error: Insufficient permissions to read the file.

如果错误类型不在 catch 分支中明确指定,则会进入默认的 catch,并通过 error 输出实际的错误枚举:

Error: insufficientPermissions

通过这种方式,enum 错误类型让代码变得更具可读性和灵活性。每个错误情况可以用 enum case 清晰地表达,并通过 catch 分支处理。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注