枚举是一种自定义的数据类型,在 Swift 中枚举类型拥有至关高的自由度。在 Swift 语言中枚举是一级类型,它拥有在其余语言中只有类才拥有的一些特性,好比实例方法,实例构造器等。html
枚举声明的类型是囊括可能状态的有限集,且能够具备附加值,并在你的代码中以一个安全的方式使用它们。经过内嵌(nesting),方法(method),关联值(associated values) 和模式匹配(pattern matching) 枚举能够分层次地定义任何有组织的数据。node
和 switch
语句相似,Swift 中的枚举乍看之下更像是 C 语言中枚举的进阶版本,即容许你定义一种类型,用于表示普通事情中某种用例。不过深刻挖掘以后,凭借 Swift 背后特别的设计理念,相比较 C 语言枚举来讲其在实际场景中的应用更为普遍。特别是做为强大的工具,Swift 中的枚举可以清晰表达代码的意图。ios
在一般状况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b }
实现默认支持相等性判断 T.a == T.b, T.b != T.a
。然而,一旦咱们为枚举增长了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,须要咱们本身实现 ==
运行符。git
基于整型的枚举,如 enum Bit: Int { case zero = 0; case one = 1 }
能够经过 @objc
标识来将其桥接到 Objective-C 当中。然而,一旦使用整型以外的类型(如 String
)或者开始使用关联值,咱们就没法在 Objective-C 当中使用这些枚举了。github
枚举是值类型,而且只有在赋予变量或常量,或者被函数调用时才被赋值。objective-c
枚举基本结构spring
enum 枚举名: 字段类型 { case 字段名 case 字段名 = 原始值 case 字段名(关联值类型) }
enum Seasons: Int { case spring = 0 case summer = 1 case autumn = 2 case winter = 3 } var sean = Seasons.spring /// enum 枚举关键字 /// Seasons 枚举名称 /// Int 枚举字段类型 /// Seasons.spring 建立一个枚举变量
枚举的定义编程
Swift 的每一个枚举项前面,都使用一个 case 关键字来标识,从 Swift 3.0 开始,全部枚举的 case
都改用小写的形式。swift
enum Movement { case left case right case top case bottom }
除了每行声明一个枚举项,也能够将这些枚举项放在一行中声明,每项之间用逗号分隔。api
enum CompassPoint { case north, south, east, west // 使用逗号分隔多个成员值 }
和 C 语言不一样的是,Swift 标准的枚举定义方式成功定义枚举后,成员值并不会隐式被指定为 0、一、二、…… 这种形式。
枚举的使用
使用时,咱们能够无须明确指出 enum 的实际名称(即 case Move.left: print("Left")
)。由于类型检查器可以自动为此进行类型推算。这对于那些 UIKit
以及 AppKit
中错综复杂的枚举是很是有用的。
/// 若是 switch 的条件声明在同一个函数内,这时会提示 Switch condition evaluates /// to a constant,要去除这个,只须要将声明的变量放在函数外就能够 let aMovement = Movement.left /// aDirection 的类型是已知的,因此在设定它的值时,能够不写该类型 let aDirection: CompassPoint = .south
可使用多种模式匹配结构获取到枚举的值,或者按照特定状况执行操做。
/// 不管用 default 也好,仍是明确对每个枚举项指定行为也好,在 Swift 中,咱们都必须对 /// 枚举类型下的每一个值,指定肯定的行为,不能漏掉其中任何一个可能性。 /// switch 分状况处理 switch aMovement { // print left case .left: print("left") case .right: print("right") default: break } /// 枚举的全部成员值都列出来时,能够不用写 default: break 分支语句,若是写了会提示 /// Default will never be executed switch aDirection { // print Watch out for penguins case .east: print("Where the sun rises") case .west: print("Where the skies are blue") case .south: print("Watch out for penguins") case .north: print("Lots of planets have a north") }
/// 明确的 case 状况 if case .left = aMovement { // print left print("left") }
/// 条件语句特定状况判断 if aMovement == .left { // print left print("left") }
枚举的定义
固然,你可能想要为 enum 中每一个 case
分配一个值。这至关有用,好比枚举自身实际与某事或某物挂钩时,每每这些东西又须要使用不一样类型来表述。在 C 语言中,你只能为枚举 case
分配整型值,而 Swift 则提供了更多的灵活性。
Swift 枚举中支持如下四种关联值类型,所以一般状况下你没法为枚举分配诸如 CGPoint
类型的值。
/// 映射到整型 enum Seasons: Int { case spring = 0 case summer = 1 case autumn = 2 case winter = 3 }
/// 映射到 float double, 注意枚举中的花式 unicode enum Constants: Double { case π = 3.14159 case e = 2.71828 case φ = 1.61803398874 case λ = 1.30357 }
/// 映射到字符串 enum House: String { case baratheon = "Ours is the Fury" case greyjoy = "We Do Not Sow" case martell = "Unbowed, Unbent, Unbroken" case stark = "Winter is Coming" case tully = "Family, Duty, Honor" case tyrell = "Growing Strong" }
对于 String
和 Int
类型来讲,甚至能够忽略为枚举中的 case
赋值,Swift 编译器也能正常工做。
/// CompassPointStr 枚举中 north = "north", ... west = "west" enum CompassPointStr: String { case north, south, east, west }
/// Planet 枚举中 mercury = 1, venus = 2, ... neptune = 8 enum Planet: Int { case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune }
若是想要以底层 C 二进制编码形式呈现某物或某事,使得更具可读性,能够看一下 BSD kqeue library
中的 VNode Flags
标志位的编码方式,如此即可以使你的 delete
或 write
用例声明一目了然,稍后一旦须要,只需将 raw value
传入 C 函数中便可。
enum VNodeFlags: UInt32 { case delete = 0x00000001 case write = 0x00000002 case extended = 0x00000004 case attrib = 0x00000008 case link = 0x00000010 case rename = 0x00000020 case revoke = 0x00000040 case none = 0x00000080 }
枚举的使用
若是想要读取枚举的值,能够经过 rawValue
属性来实现。
let bestHouse = House.stark /// 读取枚举的值,经过 rawValue 属性来实现 let houseValue = bestHouse.rawValue print(houseValue) // print Winter is Coming
若是想要经过一个已有的 raw value 来建立一个 enum case
。这种状况下,枚举提供了一个指定构造方法来实现。假若使用 rawValue
构造器,切记它是一个可失败构造器 (failable initializer)。换言之,构造方法返回值为可选类型值,由于有时候传入的值可能与任意一个 case
都不匹配。好比 Seasons(rawValue: 10)
。
/// 经过构造方法来建立 enum case let springSeasons: Seasons? = Seasons(rawValue: 0) print(String(describing: springSeasons)) // print Optional(Swift_Enum.Enum2.Seasons.spring)
若是咱们忽略关联值,则枚举的值就只能是整型,浮点型,字符串和布尔类型。若是想要支持别的类型,则能够经过实现 ExpressibleByStringLiteral
协议(Swift 4 以前名称为 StringLiteralConvertible
)来完成,这可让咱们经过对字符串的序列化和反序列化来使枚举支持自定义类型。
做为一个例子,假设咱们要定义一个枚举来保存不一样的 iOS 设备的屏幕尺寸。然而下面这段代码不能经过编译。由于 CGSize
并非一个常量,不能用来定义枚举的值。
enum Devices: CGSize { case iPhone3GS = CGSize(width: 320, height: 480) case iPhone5 = CGSize(width: 320, height: 568) case iPhone6 = CGSize(width: 375, height: 667) case iPhone6Plus = CGSize(width: 414, height: 736) }
咱们须要为想要支持的自定义类型增长一个扩展,让其实现 ExpressibleByStringLiteral
协议。这个协议要求咱们实现三个构造方法,这三个方法都须要使用一个 String
类型的参数,而且咱们须要将这个字符串转换成咱们须要的类型(此处是 CGSize
)。
extension CGSize: ExpressibleByStringLiteral { public init(stringLiteral value: String) { let size = CGSizeFromString(value) self.init(width: size.width, height: size.height) } public init(extendedGraphemeClusterLiteral value: String) { let size = CGSizeFromString(value) self.init(width: size.width, height: size.height) } public init(unicodeScalarLiteral value: String) { let size = CGSizeFromString(value) self.init(width: size.width, height: size.height) } }
如今就能够来实现咱们须要的枚举了,不过这里有一个缺点:初始化的值必须写成字符串形式,由于这就是咱们定义的枚举须要接受的类型(记住,咱们实现了 ExpressibleByStringLiteral
,所以 String
能够转化成 CGSize
类型)。
enum Devices: CGSize { case iPhone3GS = "{320, 480}" case iPhone5 = "{320, 568}" case iPhone6 = "{375, 667}" case iPhone6Plus = "{414, 736}" }
终于,咱们可使用 CGSize
类型的枚举了。须要注意的是,当要获取真实的 CGSize
的值的时候,咱们须要访问枚举的是 rawValue
属性。
let aDevice = Devices.iPhone5 let size: CGSize = aDevice.rawValue print("the phone size string is \(aDevice)") print("width is \(size.width), height is \(size.height)") // print the phone size string is iPhone5 // width is 320.0, height is 568.0
使用字符串序列化的形式,会让使用自定义类型的枚举比较困难,然而在某些特定的状况下,这也会给咱们增长很多便利(好比使用 NSColor
/ UIColor
的时候)。不只如此,咱们彻底能够对本身定义的类型使用这个方法。
枚举的定义
若是有特定子类型的需求,能够对 enum 进行嵌套。这样就容许为实际的 enum 中包含其余明确信息的 enum。
/// 以 RPG 游戏中的每一个角色为例,每一个角色可以拥有武器,所以全部角色均可以获取同一个 /// 武器集合。而游戏中的其余实例则没法获取这些武器(好比食人魔,它们仅使用棍棒) enum Character { enum Weapon { case bow case sword case lance case dagger } enum Helmet { case wooden case iron case diamond } case thief case warrior case knight }
枚举的使用
能够经过层级结构来获取枚举的值
/// 能够经过层级结构来描述角色容许访问的项目条 let character = Character.thief let weapon = Character.Weapon.bow let helmet = Character.Helmet.iron print(character) // print thief print(weapon) // print bow print(helmet) // print iron
枚举的定义
可以在 结构体(structs)或 类(classes)中内嵌枚举,这也将有助于咱们将相关的信息集中在一个位置。
/// 在结构体中定义枚举 struct Characters { enum CharacterType { case thief case warrior case knight } enum Weapon { case bow case sword case lance case dagger } let type: CharacterType let weapon: Weapon }
枚举的使用
使用在结构体(structs)或 类(classes)中定义的枚举。
let warrior = Characters(type: .warrior, weapon: .sword) print(warrior) // print Characters(type: Swift_Enum.Enum4.Characters.CharacterType.warrior, // weapon: Swift_Enum.Enum4.Characters.Weapon.sword)
枚举的定义
Swift 的 enum 类型能够存储值, 每一个枚举成员设定一个或多个关联值,关联值是将额外信息附加到 enum case
中的一种极好的方式。
关联值附加标签的声明
/// 打个比方,你正在开发一款交易引擎,可能存在买和卖两种不一样的交易类型。除此以外每手交易 /// 还要制定明确的股票名称和交易数量,然而股票的价值和数量显然从属于交易,让他们做为独立 /// 的参数显得模棱两可。你可能已经想到要 struct 中内嵌一个枚举了,不过关联值提供了一种 /// 更清爽的解决方案 enum Trade { case buy(stock: String, amount: Int) case sell(stock: String, amount: Int) }
关联值不附加标签的声明
enum Barcode { case UPCA(Int, Int, Int) case QRCode(String) }
关联值能够以多种方式使用
/// 拥有不一样值的用例 enum UserAction { case openURL(url: NSURL) case switchProcess(processId: UInt32) case restart(time: NSDate?, intoCommandLine: Bool) }
/// 假设你在实现一个功能强大的编辑器,这个编辑器容许多重选择, /// 正如 Sublime Text : https://www.youtube.com/watch?v=i2SVJa2EGIw enum Selection { case none case single(Range<Int>) case multiple([Range<Int>]) }
/// 或者假设你在封装一个 C 语言库,正如 Kqeue BSD/Darwin 通知系统: /// https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 enum KqueueEvent { case userEvent(identifier: UInt, fflags: [UInt32], data: Int) case readFD(fd: UInt, data: Int) case writeFD(fd: UInt, data: Int) case vnodeFD(fd: UInt, fflags: [UInt32], data: Int) case errorEvent(code: UInt, message: String) }
/// 又或者一个 RPG 游戏中的全部可穿戴装备可使用一个枚举来进行映射,能够为一个装备 /// 增长重量和持久两个属性 /// 如今能够仅用一行代码来增长一个"钻石"属性,如此一来咱们即可以增长几件新的镶嵌钻石 /// 的可穿戴装备 enum Wearable { enum Weight: Int { case light = 1 case mid = 4 case heavy = 10 } enum Armor: Int { case light = 2 case strong = 8 case heavy = 20 } case Helmet(weight: Weight, armor: Armor) case Breastplate(weight: Weight, armor: Armor) case Shield(weight: Weight, armor: Armor) } let woodenHelmet = Wearable.Helmet(weight: .light, armor: .light)
枚举的使用
若是全部的枚举成员的关联值的提取为常数,或者当全部被提取为变量,为了简洁起见,能够放置一个 let
或 var
标注在成员名称前。
let trade1 = Trade.buy(stock: "APPL", amount: 500) let trade2 = Trade.sell(stock: "TSLA", amount: 100) if case let Trade.buy(stock, amount) = trade1 { print("buy \(amount) of \(stock)") // print buy 500 of APPL } if case let Trade.sell(stock, amount) = trade2 { print("sell \(amount) of \(stock)") // print sell 100 of TSLA }
let productBarcode1: Barcode = .UPCA(8, 85909_51226, 3) let productBarcode2: Barcode = Barcode.QRCode("ABCDEFGHIJKLMNOP") switch productBarcode1 { // print UPC-A with value of 8, 8590951226, 3. case .UPCA(let numberSystem, let identifier, let check): print("UPC-A with value of \(numberSystem), \(identifier), \(check).") case .QRCode(let productCode): print("QR code with value of \(productCode).") } switch productBarcode2 { // print QR code with value of ABCDEFGHIJKLMNOP. /// 放置一个 let 或 var 标注在成员名称前 case let .UPCA(numberSystem, identifier, check): print("UPC-A with value of \(numberSystem), \(identifier), \(check).") case let .QRCode(productCode): print("QR code with value of \(productCode).") }
在 Swift 中,带有关联值的 enum 不提供 ==
运算符的操做,正确使用方法 使用 switch
去判断类型。
let code3: Barcode = .QRCode("123") let code4: Barcode = .QRCode("456") /// 若是像下面这样使用时,系统会提示: /// Binary operator '==' cannot be applied to two 'Enum.Barcode' operands if code3 == code4 { } /// 使用 switch 去判断类型 switch (code3, code4) { // print code3 == code4: false case (.QRCode(let a), .QRCode(let b)) where a == b: print("code3 == code4: true") default: print("code3 == code4: false") }
间接类型是 Swift 2.0 新增的一个类型。它们容许将枚举中一个 case 的关联值再次定义为枚举。
举个例子,假设咱们想定义一个文件系统,用来表示文件以及包含文件的目录。若是将文件和目录定义为枚举的 case,则目录 case 的关联值应该再包含一个文件的数组做为它的关联值。由于这是一个递归的操做,编译器须要对此进行一个特殊的准备。Swift 文档中是这么写的:枚举和 case 能够被标记为间接的(indrect),这意味它们的关联值是被间接保存的,这容许咱们定义递归的数据结构。
因此,若是咱们要定义 FileNode
的枚举,它应该会是这样的
enum FileNode { case file(name: String) indirect case folder(name: String, files: [FileNode]) }
此处的 indrect
关键字告诉编译器间接地处理这个枚举的 case。
也能够对整个枚举类型使用这个关键字。这是一个很强大的特性,可让咱们用很是简洁的方式来定义一个有着复杂关联的数据结构。
做为例子,咱们来定义一个二叉树:
indirect enum Tree<Element: Comparable> { case empty case node(Tree<Element>, Element, Tree<Element>) }
在一般状况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b }
实现默认支持相等性判断 T.a == T.b, T.b != T.a
。
然而,一旦咱们为枚举增长了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,须要咱们本身实现 ==
运行符。
enum Trade1 { case buy(stock: String, amount: Int) case sell(stock: String, amount: Int) } func ==(lhs: Trade1, rhs: Trade1) -> Bool { switch (lhs, rhs) { case let (.buy(stock1, amount1), .buy(stock2, amount2)) where stock1 == stock2 && amount1 == amount2: return true case let (.sell(stock1, amount1), .sell(stock2, amount2)) where stock1 == stock2 && amount1 == amount2: return true default: return false } }
正如咱们所见,咱们经过 switch 语句对两个枚举的 case
进行判断,而且只有当它们的 case
是匹配的时候(好比 buy
和 buy
)才对它们的真实关联值进行判断。
let buy1 = Trade1.buy(stock: "buy1", amount: 10) let buy2 = Trade1.buy(stock: "buy1", amount: 11) print(buy1 == buy2) // print false
一个枚举一般包含多个枚举成员,枚举成员能够包括计算型属性、类型别名,甚至其它枚举、结构体和类。枚举声明中,每个事件块都由一个 case 关键字开始。多个成员的值能够出如今一行上,用逗号分隔。
尽管增长一个存储属性到枚举中不被容许,但你依然可以建立计算属性。固然,计算属性的内容都是创建在枚举值下或者枚举关联值获得的。
枚举属性的定义
enum Device { case iPad, iPhone var year: Int { // 定义计算属性 switch self { case .iPhone: return 2007 case .iPad: return 2010 } } }
枚举属性的使用
let device = Device.iPhone.year print(device) // print 2007
枚举中的方法为每个 enum case
而 “生”。因此假若想要在特定状况执行特定代码的话,须要分支处理或采用 switch
语句来明确正确的代码路径。
枚举的方法
可变方法:方法能够声明为 mutating
,这样就容许改变隐藏参数 self
的 case
值了。
方法和静态方法的添加容许咱们为 enum 附加功能,这意味着无须依靠额外函数就能实现。
枚举方法的定义
普通方法,分支处理或采用 switch
语句来明确正确的代码路径。
enum Wearable { enum Weight: Int { case light = 1 } enum Armor: Int { case light = 2 } case helmet(weight: Weight, armor: Armor) func attributes() -> (weight: Int, armor: Int) { // 定义方法 switch self { case .helmet(let w, let a): return (weight: w.rawValue * 2, armor: a.rawValue * 4) } } }
enum Device1 { case iPad, iPhone, AppleTV, AppleWatch func introduced() -> String { // 定义方法 switch self { case .iPad: return "\(self) was introduced 2010" case .iPhone: return "\(self) was introduced 2007" case .AppleTV: return "\(self) was introduced 2006" case .AppleWatch: return "\(self) was introduced 2014" } } }
静态方法, static
修饰,经过一个非枚举类型来建立一个枚举。
enum Device2 { case AppleWatch static func fromSlang(term: String) -> Device2? { // 定义静态方法 if term == "iWatch" { return .AppleWatch } return nil } }
可变方法,mutating
修饰,改变隐藏参数 self
的 case
值了。
enum TriStateSwitch { case off, low, high mutating func next() { // 定义可变方法 switch self { case .off: self = .low case .low: self = .high case .high: self = .off } } }
枚举方法的使用
普通方法
let woodenHelmetProps = Wearable.helmet(weight: .light, armor: .light).attributes() print(woodenHelmetProps) // print (weight: 2, armor: 8) let device1 = Device1.iPhone.introduced() print(device1) // print iPhone was introduced 2007
静态方法
let device2 = Device2.fromSlang(term: "iWatch") print(String(describing: device2)) // print Optional(Swift_Enum.Enum7.Device2.AppleWatch)
可变方法
var ovenLight = TriStateSwitch.low ovenLight.next() // ovenLight 如今等于 .high print(ovenLight) // print high ovenLight.next() // ovenLight 如今等于 .off print(ovenLight) // print off
咱们也可使用自定义构造方法来替换静态方法。
枚举与结构体和类的构造方法最大的不一样在于,枚举的构造方法须要将隐式的 self
属性设置为正确的 case
。
可失败构造方法
enum Device3 { case AppleWatch init?(term: String) { // 可失败(failable)的构造方法 if term == "iWatch" { self = .AppleWatch return } return nil } }
普通构造方法
enum NumberCategory { case small case medium case big case huge init(number n: Int) { // 普通的构造方法 if n < 10000 { self = .small } else if n < 1000000 { self = .medium } else if n < 100000000 { self = .big } else { self = .huge } } }
使用
let device = Device3(term: "iWatch") print(String(describing: device)) // print Optional(Swift_Enum.Enum7.Device3.AppleWatch) let aNumber = NumberCategory(number: 100) print(aNumber) // print small
case
和 method
分离,这样阅读你的代码可以简单快速地消化掉 enum 内容。枚举
enum Entities { case soldier(x: Int, y: Int) case tank(x: Int, y: Int) case player(x: Int, y: Int) }
枚举的扩展
枚举扩展的定义
/// 为 enum 扩展方法 extension Entities { mutating func move(dist: CGVector) {} mutating func attack() {} }
/// 你一样能够经过写一个扩展来遵循一个特定的协议 extension Entities: CustomStringConvertible { var description: String { switch self { case let .soldier(x, y): return "\(x), \(y)" case let .tank(x, y): return "\(x), \(y)" case let .player(x, y): return "\(x), \(y)" } } }
枚举扩展的使用
var entities = Entities.tank(x: 2, y: 5) print(entities.attack()) // print () print(entities.description) // print 2, 5
Swift 协议定义一个接口或类型以供其余数据结构来遵循,enum 固然也不例外。
除了附加方法的能力以外,Swift 也容许你在枚举中使用协议(Protocols)和协议扩展(Protocol Extension)。
枚举
enum Account { case empty case funds(remaining: Int) enum Error1: Error { case overdraft(amount: Int) } var remainingFunds: Int { switch self { case .empty: return 0 case .funds(let remaining): return remaining } } }
var remainingFuns: Int
。那么你会如何构造呢?答案是你可使用关联值完美解决枚举的协议
枚举协议的定义
咱们先从 Swift 标准库中的一个例子开始。CustomStringConvertible
是一个以打印为目的的自定义格式化输出的类型。该协议只有一个要求,即一个只读(getter)类型的字符串(String
类型)。咱们能够很容易为 enum 实现这个协议。
protocol CustomStringConvertible { var description: String { get } }
自定义协议
/// 一些协议的实现可能须要根据内部状态来相应处理要求。例如定义一个管理银行帐号的协议 protocol AccountCompatible { var remainingFunds: Int { get } mutating func addFunds(amount: Int) throws mutating func removeFunds(amount: Int) throws }
枚举协议的实现
实现系统定义的协议
enum Trade: CustomStringConvertible { case buy, sell var description: String { // 实现协议 switch self { case .buy: return "We're buying something" case .sell: return "We're selling something" } } }
实现自定义的协议,为了保持代码清爽,咱们能够在 enum 的扩展(protocl extension)中定义必须的协议函数。
extension Account: AccountCompatible { mutating func addFunds(amount: Int) throws { // 实现协议 var newAmount = amount if case let .funds(remaining) = self { newAmount += remaining } if newAmount < 0 { throw Error1.overdraft(amount: -newAmount) } else if newAmount == 0 { self = .empty } else { self = .funds(remaining: newAmount) } } mutating func removeFunds(amount: Int) throws { // 实现协议 try self.addFunds(amount: amount * -1) } }
枚举协议的使用
使用
let action = Trade.buy print("this action is \(action)") // print this action is We're buying something
var account = Account.funds(remaining: 20) print("add: ", try? account.addFunds(amount: 10)) // print add: Optional(()) print("remove 1: ", try? account.removeFunds(amount: 15)) // print remove 1: Optional(()) print("remove 2: ", try? account.removeFunds(amount: 55)) // print remove 2: nil
枚举也支持泛型参数定义。你可使用它们以适应枚举中的关联值。
就拿直接来自 Swift 标准库中的简单例子来讲,即 Optional
类型。你主要可能经过如下几种方式使用它:
if let
可选绑定guard let
语句switch
语句可是从语法角度来讲你也能够这么使用 Optional
let aValue = Optional<Int>.some(5) let noValue = Optional<Int>.none print(aValue) // print Optional(5) if noValue == Optional.none { // print No value print("No value") }
这是 Optional
最直接的用例,并未使用任何语法糖,可是不能否认 Swift 中语法糖的加入使得你的工做更简单。若是你观察上面的实例代码,你恐怕已经猜到 Optional
内部实现是这样的。
// Simplified implementation of Swift's Optional enum MyOptional<T> { case some(T) case none }
这里有啥特别呢?注意枚举的关联值采用泛型参数 T
做为自身类型,这样可选类型构造任何你想要的返回值。枚举能够拥有多个泛型参数。就拿熟知的 Either
类为例,它并不是是 Swift 标准库中的一部分,而是实现于众多开源库以及其余函数式编程语言,好比 Haskell
或 F#
。设计想法是这样的: 相比较仅仅返回一个值或没有值(née Optional),你更指望返回一个成功值或者一些反馈信息(好比错误值)。
// The well-known either type is, of course, an enum that allows you to // return either value one (say, a successful value) or value two (say // an error) from a function enum Either<T1, T2> { case left(T1) case right(T2) }
Swift 中全部在 class 和 struct 中奏效的类型约束,在 enum 中一样适用。
// Totally nonsensical example. A bag that is either full (has an array with // contents) or empty. // Totally nonsensical example. A bag that is either full (has an array with // contents) or empty. enum Bag<T: Sequence> where T.Iterator.Element == Equatable { case empty case full(contents: T) }
一个特别常常被问到的问题就是如何对枚举中的 case
进行迭代。惋惜的是,枚举并无遵照 SequenceType
协议,所以没有一个官方的作法来对其进行迭代。
取决于枚举的类型,对其进行迭代可能也简单,也有可能很困难。
在 StackOverflow 上有一个很好的讨论贴。贴子里面讨论到的不一样状况太多了,若是只在这里摘取一些会有片面性,而若是将所有状况都列出来,则会太多。
基于整型的枚举,如 enum Bit: Int { case zero = 0; case one = 1 }
能够经过 @objc
标识来将其桥接到 Objective-C 当中。然而,一旦使用整型以外的类型(如 String
)或者使用关联值,咱们就没法在 Objective-C 当中使用这些枚举了。
Swift 中定义的整型枚举
@objc enum Seasons: Int { // 使用 @objc 标识 case spring = 0 case summer = 1 case autumn = 2 case winter = 3 }
在 Objective-C 中使用时,Swift 中定义的整型枚举被隐式的转换成了以下形式
typedef SWIFT_ENUM(NSInteger, Seasons) { // Objective-C 类型的枚举 SeasonsSpring = 0, SeasonsSummer = 1, SeasonsAutumn = 2, SeasonsWinter = 3, };
在 Objective-C 中使用
Seasons se = SeasonsSummer; switch (se) { // print Summer case SeasonsSpring: NSLog(@"Spring"); break; case SeasonsSummer: NSLog(@"Summer"); break; case SeasonsAutumn: NSLog(@"Autumn"); break; case SeasonsWinter: NSLog(@"Winter"); break; default: break; }
有一个名为 _ObjectiveCBridgeable
的隐藏协议,可让规范咱们以定义合适的方法,如此一来,Swift 即可以正确地将枚举转成 Objective-C 类型。然而,从理论上来说,这个协议仍是容许咱们将枚举(包括其实枚举值)正确地桥接到 Objective-C 当中。
可是,咱们并不必定非要使用上面提到的这个方法。为枚举添加两个方法,使用 @objc
定义一个替代类型,如此一来咱们即可以自由地将枚举进行转换了,而且这种方式不须要遵照私有协议。
这个方法有一个的缺点,咱们须要将枚举映射为 Objective-C 中的 NSObject
基础类型(咱们也能够直接使用 NSDictionary
),可是,当咱们碰到一些确实须要在 Objective-C 当中获取有关联值的枚举时,这是一个可使用的方法。
Swift 中定义的枚举
enum Trade { case buy(stock: String, amount: Int) case sell(stock: String, amount: Int) }
定义 Objective-C 中可使用的 Trade
的替代类型,这个类型也能够定义在 Objective-C 的代码中
/// 在 Swift 代码中定义 @objc class OTrade: NSObject { var type: Int var stock: String var amount: Int init(type: Int, stock: String, amount: Int) { self.type = type self.stock = stock self.amount = amount } }
/// 在 Objective-C 代码中定义 @property (nonatomic, assign) NSInteger type; @property (nonatomic, strong) NSString *stock; @property (nonatomic, assign) NSInteger amount; - (instancetype)initWithType:(NSInteger)type stock:(NSString *)stock amount:(NSInteger)amount; - (instancetype)initWithType:(NSInteger)type stock:(NSString *)stock amount:(NSInteger)amount { self = [super init]; if (self) { _type = type; _stock = stock; _amount = amount; } return self; }
Swift 中定义的枚举须要实现的两个方法
extension Trade { func toObjc() -> OTrade { switch self { case let .buy(stock, amount): return OTrade(type: 0, stock: stock, amount: amount) case let .sell(stock, amount): return OTrade(type: 1, stock: stock, amount: amount) } } static func fromObjc(source: OTrade) -> Trade? { switch (source.type) { case 0: return Trade.buy(stock: source.stock, amount: source.amount) case 1: return Trade.sell(stock: source.stock, amount: source.amount) default: return nil } } }
在 Objective-C 中使用
OTrade *trade = [[OTrade alloc] initWithType:0 stock:@"A" amount:100]; NSLog(@"%@", trade.stock); // print A NSLog(@"%ld", (long)trade.amount); // print 100
Erica Sadun 写过一篇关于枚举底层的博客,涉及到枚举底层的方方面面。在生产代码中毫不应该使用到这些东西,可是学习一下仍是至关有趣的。
在这里,只提到那篇博客中一条,若是想了解更多,请移步到原文:枚举一般都是一个字节长度。[...] 若是你真的很傻很天真,你固然能够定义一个有成百上千个 case
的枚举,在这种状况下,取决于最少所须要的比特数,枚举可能占据两个字节或者更多。
FloatingPointClassification
这个枚举定义了一系列 IEEE 754 可能的类别,好比 NegativeInfinity
, PositiveZero
或 SignalingNaN
。
/// The IEEE 754 floating-point classes. public enum FloatingPointClassification { case signalingNaN case quietNaN case negativeInfinity case negativeNormal case negativeSubnormal case negativeZero case positiveZero case positiveSubnormal case positiveNormal case positiveInfinity }
Mirror.AncestorRepresentation
和 Mirror.DisplayStyle
这两个枚举被用在 Swift 反射 API 的上下文当中。
/// Representation of ancestor classes. public enum AncestorRepresentation { case generated case customized(() -> Mirror) case suppressed }
/// A suggestion of how a `Mirror`'s `subject` is to be interpreted. public enum DisplayStyle { case `struct` case `class` case `enum` case tuple case optional case collection case dictionary case set }
Optional
这个就不用多说了
/// A type that represents either a wrapped value or `nil`, the absence of a value. public enum Optional<Wrapped> : ExpressibleByNilLiteral { case none case some(Wrapped) public init(_ some: Wrapped) public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U? public init(nilLiteral: ()) public var unsafelyUnwrapped: Wrapped { get } }
在不少场合,使用枚举要赛过使用结构体和类。通常来说,若是问题能够被分解为有限的不一样类别,则使用枚举应该就是正确的选择。
即便只有两种 case
,这也是一个使用枚举的完美场景,正如 Optional
和 Either
类型所展现的。
说到枚举的实践使用,固然少不了在 Swift 2.0 当中新推出的错误处理。标记为可抛出的函数能够抛出任何遵照了 Error
空协议(Swift 4 以前名称为 ErrorType
)的类型。正如 Swift 官方文档中所写的:Swift 的枚举特别适用于构建一组相关的错误状态,能够经过关联值来为其增长额外的附加信息。
username
的属性,可是 JSON 中缺乏了)存在类型不匹配,好比说 username
须要的是 String
类型,而 JSON 中包含的是 NSNull6
。
除此以外,Argo 还为不包含在上述两个类别中的错误提供了自定义错误。它们的 Error
枚举是相似这样的,全部的 case
都有一个关联值用来包含关于错误的附加信息。
enum DecodeError: Error { case TypeMismatch(expected: String, actual: String) case MissingKey(String) case Custom(String) }
一个更加通用的用于完整 HTTP / REST API 错误处理的 Error
应该是相似这样的
enum APIError: Error { // Can't connect to the server (maybe offline?) case ConnectionError(error: NSError) // The server responded with a non 200 status code case ServerError(statusCode: Int, error: NSError) // We got no data (0 bytes) back from the server case NoDataError // The server response can't be converted from JSON to a Dictionary case JSONSerializationError(error: Error) // The Argo decoding Failed case JSONMappingError(converstionError: DecodeError) }
Error
实现了完整的 REST 程序栈解析有可能出现的错误,包含了全部在解析结构体与类时会出现的错误。JSONMappingError
中,咱们将 Argo 中的 DecodeError
封装到了咱们的 APIError
类型当中,由于咱们会用 Argo 来做实际的 JSON 解析。更多关于 Error
以及此种枚举类型的示例能够参看官方文档。
在 Swift 当中,有许多方法来构建观察模式。若是使用 @objc
兼容标记,则咱们可使用 NSNotificationCenter
或者 KVO。即便不用这个标记,didSet
语法也能够很容易地实现简单的观察模式。在这里可使用枚举,它可使被观察者的变化更加清晰明了。
设想咱们要对一个集合进行观察。若是咱们稍微思考一下就会发现这只有几种可能的状况:一个或多个项被插入,一个或多个项被删除,一个或多个项被更新。这听起来就是枚举能够完成的工做。
enum Change { case Insertion(items: [Item]) case Deletion(items: [Item]) case Update(items: [Item]) }
oldValue
和 newValue
的简单方法来扩展它的功能。若是咱们正在使用一个外部系统,而这个系统使用了状态码(或者错误码)来传递错误信息,相似 HTTP 状态码,这种状况下枚举就是一种很明显而且很好的方式来对信息进行封装。
enum HttpError: String { case Code400 = "Bad Request" case Code401 = "Unauthorized" case Code402 = "Payment Required" case Code403 = "Forbidden" case Code404 = "Not Found" }
枚举也常常被用于将 JSON 解析后的结果映射成 Swift 的原生类型。相似地,若是咱们解析了其它的东西,也可使用这种方式将解析结果转化咱们 Swift 的类型。
enum JSON { case JSONString(Swift.String) case JSONNumber(Double) case JSONObject([String : JSONValue]) case JSONArray([JSONValue]) case JSONBool(Bool) case JSONNull }
枚举能够用来将字符串类型的重用标识或者 storyboard 标识映射为类型系统能够进行检查的类型。
假设咱们有一个拥有不少原型 Cell
的 UITableView
。
enum CellType: String { case ButtonValueCell = "ButtonValueCell" case UnitEditCell = "UnitEditCell" case LabelCell = "LabelCell" case ResultLabelCell = "ResultLabelCell" }
单位以及单位转换是另外一个使用枚举的绝佳场合。能够将单位及其对应的转换率映射起来,而后添加方法来对单位进行自动的转换。另外一个示例是货币的转换。以及数学符号(好比角度与弧度)也能够从中受益。
enum Liquid: Float { case ml = 1.0 case l = 1000.0 func convert(amount amount: Float, to: Liquid) -> Float { if self.rawValue < to.rawValue { return (self.rawValue / to.rawValue) * amount } else { return (self.rawValue * to.rawValue) * amount } } } // Convert liters to milliliters print(Liquid.l.convert(amount: 5, to: Liquid.ml))
游戏也是枚举中的另外一个至关好的用例,屏幕上的大多数实体都属于一个特定种族的类型(敌人,障碍,纹理,...)。相对于本地的 iOS 或者 Mac 应用,游戏更像是一个白板。即开发游戏咱们可使用全新的对象以及全新的关联创造一个全新的世界,而 iOS 或者 macOS 须要使用预约义的 UIButtons
,UITableViews
,UITableViewCells
或者 NSStackView
。
不只如此,因为枚举能够遵照协议,咱们能够利用协议扩展和基于协议的编程为不一样为游戏定义的枚举增长功能。
enum FlyingBeast { case Dragon, Hippogriff, Gargoyle } enum Horde { case Ork, Troll } enum Player { case Mage, Warrior, Barbarian } enum NPC { case Vendor, Blacksmith } enum Element { case Tree, Fence, Stone } protocol Hurtable {} protocol Killable {} protocol Flying {} protocol Attacking {} protocol Obstacle {} extension FlyingBeast: Hurtable, Killable, Flying, Attacking {} extension Horde: Hurtable, Killable, Attacking {} extension Player: Hurtable, Obstacle {} extension NPC: Hurtable {} extension Element: Obstacle {}
在一个稍微大一点的 Xcode 项目中,咱们很快就会有一大堆经过字符串来访问的资源。在前面的小节中,咱们已经提太重用标识和 storyboard 的标识,可是除了这两样,还存在不少资源:图像,Segues,Nibs,字体以及其它资源。一般状况下,这些资源均可以分红不一样的集合。若是是这样的话,一个类型化的字符串会是一个让编译器帮咱们进行类型检查的好方法。
enum DetailViewImages: String { case Background = "bg1.png" case Sidebar = "sbg.png" case ActionButton1 = "btn1_1.png" case ActionButton2 = "btn2_1.png" }
对于 iOS 开发者,R.swift
这个第三方库能够为以上提到的状况自动生成结构体。可是有些时候你可能须要有更多的控制(或者你多是一个 Mac 开发者)。
Rest API 是枚举的绝佳用例。它们都是分组的,它们都是有限的 API 集合,而且它们也可能会有附加的查询或者命名的参数,而这可使用关联值来实现。
这里有个 Instagram API 的简化版。
enum Instagram { enum Media { case Popular case Shortcode(id: String) case Search(lat: Float, min_timestamp: Int, lng: Float, max_timestamp: Int, distance: Int) } enum Users { case User(id: String) case Feed case Recent(id: String) } }
Ash Furrow 的 Moya 框架就是基本这个思想,使用枚举对 rest 端点进行映射。
Airspeed Velocity 有一篇极好的文章说明了如何使用枚举来实现一个链表。那篇文章中的大多数代码都超出了枚举的知识,并涉及到了大量其它有趣的主题,可是,链表最基本的定义是相似这样的(对其进行了一些简化)。
enum List { case End indirect case Node(Int, next: List) }
case
都指向了下一个 case
,经过使用枚举而非其它类型,咱们能够避免使用一个可选的 next
类型以用来表示链表的结束。Airspeed Velocity 还写过一篇超赞的博客,关于如何使用 Swift 的间接枚举类型来实现红黑树,因此若是你已经阅读过关于链表的博客,你可能想继续阅读这篇关于红黑树的博客。
David Owens 写过一篇文章,他以为当前的关联值提取方式是很笨重的:为了从一个枚举中获取关联值,咱们必须使用模式匹配。然而,关联值就是关联在特定枚举 case
的高效元组。而元组是可使用更简单的方式来获取它内部值,即 .keyword
或者 .0
。
// Enum(枚举) enum Ex { case Mode(ab: Int, cd: Int) } if case Ex.Mode(let ab, let cd) = Ex.Mode(ab: 4, cd: 5) { print(ab) } // vs tuples(元组) let tp = (ab: 4, cd: 5) print(tp.ab)
若是你也一样以为咱们应该使用相同的方法来对枚举进行解构(deconstruct),这里有个 rdar: rdar://22704262。
拥有关联值的枚举没有遵照 equatable
协议。这是一个遗憾,由于它为不少事情增长了没必要要的复杂和麻烦。深层的缘由多是由于关联值的底层是使用了元组,而元组并无遵照 equatable
协议。然而,对于限定的 case
子集,若是这些关联值的类型都遵照了 equatable
类型,编译器应该默认为其生成 equatable
扩展。
// Int 和 String 是可判等的, 因此 Mode 应该也是可判等的 enum Ex { case Mode(ab: Int, cd: String) } // Swift 应该可以自动生成这个函数 func ==(lhs: Ex.Mode, rhs: Ex.Mode) -> Bool { switch (lhs, rhs) { case (.Mode(let a, let b), .Mode(let c, let d)): return a == c && b == d default: return false } }
最大的问题就是对元组的支持。他们目前还处于无文档状态而且在不少场合都没法使用。在枚举当中,咱们没法使用元组做为枚举的值。
enum Devices: (intro: Int, name: String) { case iPhone = (intro: 2007, name: "iPhone") case AppleTV = (intro: 2006, name: "Apple TV") case AppleWatch = (intro: 2014, name: "Apple Watch") }
case
的集合以使咱们能够对其进行迭代。另外一个会碰到的事是枚举的关联值老是类型,可是咱们却没法为这些类型指定默认值。
enum Characters { case Mage(health: Int = 70, magic: Int = 100, strength: Int = 30) case Warrior(health: Int = 100, magic: Int = 0, strength: Int = 100) case Neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80) }
咱们依然可使用不一样的值建立新的 case
,可是角色的默认设置依然会被映射。