时至今日,已经在做两个简单的应用,还是很少涉及到使用枚举。因为近期在学习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 的枚举比许多其他语言中的枚举更灵活,因为它们可以:
- 关联值 – 枚举的每个成员可以存储不同类型的关联数据。
- 原始值 – 枚举的成员可以以某种默认类型(如整数、字符串)初始化。
- 方法 – 枚举可以定义方法,允许在枚举成员上调用。
定义一个简单的枚举
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
}
- sunday 自动获得 0
- monday 自动获得 1
- tuesday 自动获得 2
- wednesday 被你手动指定为 4
- 后面的 thursday 会自动递增为 5
- friday 会是 6
- 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
}
在这个枚举中:
- sunday 的原始值自动设置为 0(因为它是第一个 case,自动递增从 0 开始)
- wednesday 被你手动设置为 0
- monday 和 tuesday 仍然会自动递增,即 1 和 2
- 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()方法:
- 在evaluater()方法中,因为product是.multiplication,因此进入case .multiplication,它包含了sum和two两个枚举。
- 因此调取sum和two的evaluater()方法,因为sum是.addition,因此进入case .addition,它包含了five和four两个枚举。
- 因此调取five和four的evaluater()方法,因为five和four对应的是.number,因此通过evaluater()分别返回5和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 分支处理。