Swift 中枚举高级用法及实践

title: "Swift 中枚举高级用法及实践" date: 2015-11-20 tags: [APPVENTURE] categories: [Swift 进阶] permalink: advanced-practical-enum-examplesnode

原文连接=http://appventure.me/2015/10/17/advanced-practical-enum-examples/ 做者=Benedikt Terhechte 原文日期=2015-10-17 译者=pmst+小锅 校对=shanks 定稿=shanksgit

译者注:做为一个走心且有逼格的翻译组,咱们对本篇文章中的代码都进行了验证,而且写了将代码分为上下两篇作成了 playground,代码中有详尽的注释。能够到这个github地址上进行下载,这个代码由翻译组的另外一位小伙伴 ppt 提供。 本文是一篇详细且具备实战意义的教程,涵盖几乎全部枚举(Enum)知识点,为你解答Swift中枚举的应用场合以及使用方法。github

和switch语句相似,Swift中的枚举乍看之下更像是C语言中枚举的进阶版本,即容许你定义一种类型,用于表示普通事情中某种用例。不过深刻挖掘以后,凭借Swift背后特别的设计理念,相比较C语言枚举来讲其在实际场景中的应用更为普遍。特别是做为强大的工具,Swift中的枚举可以清晰表达代码的意图。编程

本文中,咱们将首先了解基础语法和使用枚举的可能性,接着经过实战教你如何以及什么时候使用枚举。最后咱们还会大体了解下Swift标准库中枚举是如何被使用的。swift

正式开始学习以前,先给出枚举的定义。以后咱们将回过头再来讨论它。数组

枚举声明的类型是囊括可能状态的有限集,且能够具备附加值。经过内嵌(nesting),方法(method),关联值(associated values)和模式匹配(pattern matching),枚举能够分层次地定义任何有组织的数据。 深刻理解(Diving In)bash

简要概述如何定义和使用枚举。数据结构

定义基本的枚举类型(Defining Basic Enums)app

试想咱们正在开发一款游戏,玩家可以朝四个方向移动。因此喽,玩家的运动轨迹受到了限制。显然,咱们可以使用枚举来表述这一状况:框架

    case Left
    case Right
    case Top
    case Bottom
}```
紧接着,你可使用多种模式匹配结构获取到Movement的枚举值,或者按照特定状况执行操做:
复制代码

let aMovement = Movement.Left

// switch 分状况处理 switch aMovement{ case .Left: print("left") default:() }

// 明确的case状况 if case .Left = aMovement{ print("left") }

if aMovement == .Left { print("left") }``` 案例中,咱们无须明确指出enum的实际名称(即case Move.Left:print("Left"))。由于类型检查器可以自动为此进行类型推算。这对于那些UIKit以及AppKit中错综复杂的枚举是灰常有用的。

枚举值(Enum Values)

固然,你可能想要为enum中每一个case分配一个值。这至关有用,好比枚举自身实际与某事或某物挂钩时,每每这些东西又须要使用不一样类型来表述。在C语言中,你只能为枚举case分配整型值,而Swift则提供了更多的灵活性。

// 映射到整型
enum Movement: Int {
    case Left = 0
    case Right = 1
    case Top = 2
    case Bottom = 3
}
复制代码

// 一样你能够与字符串一一对应

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"
}
复制代码

// 或者float double均可以(同时注意枚举中的花式unicode)

enum Constants: Double {
    case π = 3.14159
    case e = 2.71828
    case φ = 1.61803398874
    case λ = 1.30357
}```
对于String和Int类型来讲,你甚至能够忽略为枚举中的case赋值,Swift编译器也能正常工做。
复制代码

// Mercury = 1, Venus = 2, ... Neptune = 8 enum Planet: Int { case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune }

// North = "North", ... West = "West" // 译者注: 这个是swift2.0新增语法 enum CompassPoint: String { case North, South, East, West }``` Swift枚举中支持如下四种关联值类型: 整型(Integer) 浮点数(Float Point) 字符串(String) 布尔类型(Boolean) 所以你没法1为枚举分配诸如CGPoint类型的值。

假若你想要读取枚举的值,能够经过rawValue属性来实现:

let bestHouse = House.Stark print(bestHouse.rawValue) // prints "Winter is coming" 不过某种情形下,你可能想要经过一个已有的raw value来建立一个enum case。这种状况下,枚举提供了一个指定构造方法:

enum Movement: Int { case Left = 0 case Right = 1 case Top = 2 case Bottom = 3 } // 建立一个movement.Right 用例,其raw value值为1 let rightMovement = Movement(rawValue: 1) 假若使用rawValue构造器,切记它是一个可失败构造器(failable initializer)。换言之,构造方法返回值为可选类型值,由于有时候传入的值可能与任意一个case都不匹配。好比Movement(rawValue:42)。

若是你想要以底层 C 二进制编码形式呈现某物或某事,使得更具可读性,这是一个很是有用的功能。例如,能够看一下BSD kqeue library中的VNode Flags标志位的编码方式:

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 } 如此即可以使你的Delete或Write用例声明一目了然,稍后一旦须要,只需将raw value传入 C 函数中便可。

嵌套枚举(Nesting Enums)

若是你有特定子类型的需求,能够对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 包含枚举(Containing Enums)

一样地,你也可以在structs或classes中内嵌枚举。接着上面的例子:

struct Character { enum CharacterType { case Thief case Warrior case Knight } enum Weapon { case Bow case Sword case Lance case Dagger } let type: CharacterType let weapon: Weapon }

let warrior = Character(type: .Warrior, weapon: .Sword) 一样地,这也将有助于咱们将相关的信息集中在一个位置。

关联值(Associated Value)

关联值是将额外信息附加到enum case中的一种极好的方式。打个比方,你正在开发一款交易引擎,可能存在买和卖两种不一样的交易类型。除此以外每手交易还要制定明确的股票名称和交易数量:

简单例程(Simple Example)

enum Trade { case Buy case Sell } func trade(tradeType: Trade, stock: String, amount: Int) {} 然而股票的价值和数量显然从属于交易,让他们做为独立的参数显得模棱两可。你可能已经想到要往struct中内嵌一个枚举了,不过关联值提供了一种更清爽的解决方案:

enum Trade { case Buy(stock: String, amount: Int) case Sell(stock: String, amount: Int) } func trade(type: Trade) {} 模式匹配(Pattern Mathching)

若是你想要访问这些值,模式匹配再次救场:

let trade = Trade.Buy(stock: "APPL", amount: 500) if case let Trade.Buy(stock, amount) = trade { print("buy (amount) of (stock)") } 标签(Labels)

关联值不须要附加标签的声明:

enum Trade { case Buy(String, Int) case Sell(String, Int) } 假若你添加了,那么,每当建立枚举用例时,你都须要将这些标签标示出来。

(元组参数)Tuple as Arguments

更重要的是,Swift内部相关信息实际上是一个元组,因此你能够像下面这样作:

let tp = (stock: "TSLA", amount: 100) let trade = Trade.Sell(tp)

if case let Trade.Sell(stock, amount) = trade { print("buy (amount) of (stock)") } // Prints: "buy 100 of TSLA" 语法容许您将元组看成一个简单的数据结构,稍后元组将自动转换到高级类型,就好比enum case。想象一个应用程序可让用户来配置电脑:

typealias Config = (RAM: Int, CPU: String, GPU: String)

// Each of these takes a config and returns an updated config func selectRAM(_ config: Config) -> Config {return (RAM: 32, CPU: config.CPU, GPU: config.GPU)} func selectCPU(_ config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2GHZ", GPU: config.GPU)} func selectGPU(_ config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2GHZ", GPU: "NVidia")}

enum Desktop { case Cube(Config) case Tower(Config) case Rack(Config) }

let aTower = Desktop.Tower(selectGPU(selectCPU(selectRAM((0, "", "") as Config)))) 配置的每一个步骤均经过递交元组到enum中进行内容更新。假若咱们从函数式编程2中得到启发,这将变得更好。

infix operator <^> { associativity left }

func <^>(a: Config, f: (Config) -> Config) -> Config { return f(a) } 最后,咱们能够将不一样配置步骤串联起来。这在配置步骤繁多的状况下至关有用。

let config = (0, "", "") <^> selectRAM <^> selectCPU <^> selectGPU let aCube = Desktop.Cube(config) 使用案例(Use Case Example)

关联值能够以多种方式使用。常言道:一码胜千言, 下面就上几段简单的示例代码,这几段代码没有特定的顺序。

// 拥有不一样值的用例 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) case Multiple([Range]) }

// 或者映射不一样的标识码 enum Barcode { case UPCA(numberSystem: Int, manufacturer: Int, product: Int, check: Int) case QRCode(productCode: String) }

// 又或者假设你在封装一个 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) 方法和属性(Methods and properties)

你也能够在enum中像这样定义方法:

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: w.rawValue * 4) } } } let woodenHelmetProps = Wearable.Helmet(weight: .Light, armor: .Light).attributes() print (woodenHelmetProps) // prints "(2, 4)" 枚举中的方法为每个enum case而“生”。因此假若想要在特定状况执行特定代码的话,你须要分支处理或采用switch语句来明确正确的代码路径。

enum Device { case iPad, iPhone, AppleTV, AppleWatch func introduced() -> String { switch self { case AppleTV: return "(self) was introduced 2006" case iPhone: return "(self) was introduced 2007" case iPad: return "(self) was introduced 2010" case AppleWatch: return "(self) was introduced 2014" } } } print (Device.iPhone.introduced()) // prints: "iPhone was introduced 2007" 属性(Properties)

尽管增长一个存储属性到枚举中不被容许,但你依然可以建立计算属性。固然,计算属性的内容都是创建在枚举值下或者枚举关联值获得的。

enum Device { case iPad, iPhone var year: Int { switch self { case iPhone: return 2007 case iPad: return 2010 } } } 静态方法(Static Methods)

你也可以为枚举建立一些静态方法(static methods)。换言之经过一个非枚举类型来建立一个枚举。在这个示例中,咱们须要考虑用户有时将苹果设备叫错的状况(好比AppleWatch叫成iWatch),须要返回一个合适的名称。

enum Device { case AppleWatch static func fromSlang(term: String) -> Device? { if term == "iWatch" { return .AppleWatch } return nil } } print (Device.fromSlang("iWatch")) 可变方法(Mutating Methods)

方法能够声明为mutating。这样就容许改变隐藏参数self的case值了3。

enum TriStateSwitch { case Off, Low, High mutating func next() { switch self { case Off: self = Low case Low: self = High case High: self = Off } } } var ovenLight = TriStateSwitch.Low ovenLight.next() // ovenLight 如今等于.On ovenLight.next() // ovenLight 如今等于.Off 小结(To Recap)

至此,咱们已经大体了解了Swift中枚举语法的基本用例。在开始迈向进阶之路以前,让咱们从新审视文章开篇给出的定义,看看如今是否变得更清晰了。

枚举声明的类型是囊括可能状态的有限集,且能够具备附加值。经过内嵌(nesting),方法(method),关联值(associated values)和模式匹配(pattern matching),枚举能够分层次地定义任何有组织的数据。 如今咱们已经对这个定义更加清晰了。确实,若是咱们添加关联值和嵌套,enum就看起来就像一个封闭的、简化的struct。相比较struct,前者优点体如今可以为分类与层次结构编码。

// Struct Example struct Point { let x: Int, let y: Int } struct Rect { let x: Int, let y: Int, let width: Int, let height: Int }

// Enum Example enum GeometricEntity { case Point(x: Int, y: Int) case Rect(x: Int, y: Int, width: Int, height: Int) } 方法和静态方法的添加容许咱们为enum附加功能,这意味着无须依靠额外函数就能实现4。

// C-Like example enum Trade { case Buy case Sell } func order(trade: Trade)

// Swift Enum example enum Trade { case Buy case Sell func order() } 枚举进阶(Advanced Enum Usage)

协议(Protocols)

我已经说起了structs和enums之间的类似性。除了附加方法的能力以外,Swift也容许你在枚举中使用协议(Protocols)和协议扩展(Protocol Extension)。

Swift协议定义一个接口或类型以供其余数据结构来遵循。enum固然也不例外。咱们先从Swift标准库中的一个例子开始.

CustomStringConvertible是一个以打印为目的的自定义格式化输出的类型。

protocol CustomStringConvertible { var description: String { get } } 该协议只有一个要求,即一个只读(getter)类型的字符串(String类型)。咱们能够很容易为enum实现这个协议。

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" } } }

let action = Trade.Buy print("this action is (action)") // prints: this action is We're buying something 一些协议的实现可能须要根据内部状态来相应处理要求。例如定义一个管理银行帐号的协议。

protocol AccountCompatible { var remainingFunds: Int { get } mutating func addFunds(amount: Int) throws mutating func removeFunds(amount: Int) throws } 你也许会简单地拿struct实现这个协议,可是考虑应用的上下文,enum是一个更明智的处理方法。不过你没法添加一个存储属性到enum中,就像var remainingFuns:Int。那么你会如何构造呢?答案灰常简单,你可使用关联值完美解决:

enum Account { case Empty case Funds(remaining: Int)

enum Error: ErrorType { case Overdraft(amount: Int) }

var remainingFunds: Int { switch self { case Empty: return 0 case Funds(let remaining): return remaining } } } 为了保持代码清爽,咱们能够在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 Error.Overdraft(amount: -newAmount) } else if newAmount == 0 { self = .Empty } else { self = .Funds(remaining: newAmount) } }

mutating func removeFunds(amount: Int) throws { try self.addFunds(amount * -1) }

} var account = Account.Funds(remaining: 20) print("add: ", try? account.addFunds(10)) print ("remove 1: ", try? account.removeFunds(15)) print ("remove 2: ", try? account.removeFunds(55)) // prints: // : add: Optional(()) // : remove 1: Optional(()) // : remove 2: nil 正如你所看见的,咱们经过将值存储到enum cases中实现了协议全部要求项。如此作法还有一个妙趣横生的地方:如今整个代码基础上你只须要一个模式匹配就能测试空帐号输入的状况。你不须要关心剩余资金是否等于零。

同时,咱们也在帐号(Accout)中内嵌了一个遵循ErrorType协议的枚举,这样咱们就可使用Swift2.0语法来进行错误处理了。这里给出更详细的使用案例教程。

扩展(Extensions)

正如刚才所见,枚举也能够进行扩展。最明显的用例就是将枚举的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)" } } } 枚举泛型(Generic Enums)

枚举也支持泛型参数定义。你可使用它们以适应枚举中的关联值。就拿直接来自Swift标准库中的简单例子来讲,即Optional类型。你主要可能经过如下几种方式使用它:可选链(optional chaining(?))、if-let可选绑定、guard let、或switch,可是从语法角度来讲你也能够这么使用Optional:

let aValue = Optional.Some(5) let noValue = Optional.None if noValue == Optional.None { print("No value") } 这是Optional最直接的用例,并未使用任何语法糖,可是不能否认Swift中语法糖的加入使得你的工做更简单。若是你观察上面的实例代码,你恐怕已经猜到Optional内部实现是这样的5:

// Simplified implementation of Swift's Optional enum MyOptional { 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. enum Bag<T: SequenceType where T.Generator.Element==Equatable> { case Empty case Full(contents: T) } 递归 / 间接(Indirect)类型

间接类型是 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,Tree ) } 这是一个很强大的特性,可让咱们用很是简洁的方式来定义一个有着复杂关联的数据结构。

使用自定义类型做为枚举的值

若是咱们忽略关联值,则枚举的值就只能是整型,浮点型,字符串和布尔类型。若是想要支持别的类型,则能够经过实现 StringLiteralConvertible 协议来完成,这可让咱们经过对字符串的序列化和反序列化来使枚举支持自定义类型。

做为一个例子,假设咱们要定义一个枚举来保存不一样的 iOS 设备的屏幕尺寸:

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) } 然而,这段代码不能经过编译。由于 CGPoint 并非一个常量,不能用来定义枚举的值。咱们须要为想要支持的自定义类型增长一个扩展,让其实现 StringLiteralConvertible 协议。这个协议要求咱们实现三个构造方法,这三个方法都须要使用一个String类型的参数,而且咱们须要将这个字符串转换成咱们须要的类型(此处是CGSize)。

extension CGSize: StringLiteralConvertible { 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)
}
复制代码

} 如今就能够来实现咱们须要的枚举了,不过这里有一个缺点:初始化的值必须写成字符串形式,由于这就是咱们定义的枚举须要接受的类型(记住,咱们实现了 StringLiteralConvertible,所以String能够转化成CGSize类型)

enum Devices: CGSize { case iPhone3GS = "{320, 480}" case iPhone5 = "{320, 568}" case iPhone6 = "{375, 667}" case iPhone6Plus = "{414, 736}" } 终于,咱们可使用 CGPoint 类型的枚举了。须要注意的是,当要获取真实的 CGPoint 的值的时候,咱们须要访问枚举的是 rawValue 属性。

let a = Devices.iPhone5 let b = a.rawValue print("the phone size string is (a), width is (b.width), height is (b.height)") // prints : the phone size string is iPhone5, width is 320.0, height is 568.0 使用字符串序列化的形式,会让使用自定义类型的枚举比较困难,然而在某些特定的状况下,这也会给咱们增长很多便利(比较使用NSColor / UIColor的时候)。不只如此,咱们彻底能够对本身定义的类型使用这个方法。

对枚举的关联值进行比较

在一般状况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b } 实现默认支持相等性判断 T.a == T.b, T.b != T.a

然而,一旦咱们为枚举增长了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,须要咱们本身实现 == 运行符。这并非很困难:

enum Trade { case Buy(stock: String, amount: Int) case Sell(stock: String, amount: Int) } func ==(lhs: Trade, rhs: Trade) -> 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)才对它们的真实关联值进行判断。

自定义构造方法

在 静态方法 一节当中咱们已经提到它们能够做为从不一样数据构造枚举的方便形式。在以前的例子里也展现过,对出版社常常误用的苹果设备名返回正确的名字:

enum Device { case AppleWatch static func fromSlang(term: String) -> Device? { if term == "iWatch" { return .AppleWatch } return nil } } 咱们也可使用自定义构造方法来替换静态方法。枚举与结构体和类的构造方法最大的不一样在于,枚举的构造方法须要将隐式的 self 属性设置为正确的 case。

enum Device { case AppleWatch init?(term: String) { if term == "iWatch" { self = .AppleWatch } return nil } } 在这个例子中,咱们使用了可失败(failable)的构造方法。可是,普通的构造方法也能够工做得很好:

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 aNumber = NumberCategory(number: 100) print(aNumber) // prints: "Small" 对枚举的 case 进行迭代

一个特别常常被问到的问题就是如何对枚举中的 case 进行迭代。惋惜的是,枚举并无遵照SequenceType协议,所以没有一个官方的作法来对其进行迭代。取决于枚举的类型,对其进行迭代可能也简单,也有可能很困难。在StackOverflow上有一个很好的讨论贴。贴子里面讨论到的不一样状况太多了,若是只在这里摘取一些会有片面性,而若是将所有状况都列出来,则会太多。

对 Objective-C 的支持

基于整型的枚举,如 enum Bit: Int { case Zero = 0; case One = 1 } 能够经过 @objc 标识来将其桥接到 Objective-C 当中。然而,一旦使用整型以外的类型(如 String)或者开始使用关联值,咱们就没法在 Objective-C 当中使用这些枚举了。

有一个名为_ObjectiveCBridgeable的隐藏协议,可让规范咱们以定义合适的方法,如此一来,Swift 即可以正确地将枚举转成 Objective-C 类型,但我猜这个协议被隐藏起来必定是有缘由的。然而,从理论上来说,这个协议仍是容许咱们将枚举(包括其实枚举值)正确地桥接到 Objective-C 当中。

可是,咱们并不必定非要使用上面提到的这个方法。为枚举添加两个方法,使用 @objc 定义一个替代类型,如此一来咱们即可以自由地将枚举进行转换了,而且这种方式不须要遵照私有协议:

enum Trade { case Buy(stock: String, amount: Int) case Sell(stock: String, amount: Int) }

// 这个类型也能够定义在 Objective-C 的代码中 @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 } }

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 中的 NSObject 基础类型(咱们也能够直接使用 NSDictionary),可是,当咱们碰到一些确实须要在 Objective-C 当中获取有关联值的枚举时,这是一个可使用的方法。

枚举底层

Erica Sadun 写过一篇很流弊的关于枚举底层的博客,涉及到枚举底层的方方面面。在生产代码中毫不应该使用到这些东西,可是学习一下仍是至关有趣的。在这里,我准备只提到那篇博客中一条,若是想了解更多,请移步到原文:

枚举一般都是一个字节长度。[...]若是你真的很傻很天真,你固然能够定义一个有成百上千个 case 的枚举,在这种状况下,取决于最少所须要的比特数,枚举可能占据两个字节或者更多。 Swift 标准库中的枚举

在咱们准备继续探索枚举在项目中的不一样用例以前,先看一下在 Swift 标准库当中是如何使用枚举可能会更诱人,因此如今让咱们先来看看。

Bit 这个枚举有两个值,One 和 Zero。它被做为 CollectionOfOne 中的 Index 类型。

FloatingPointClassification 这个枚举定义了一系列 IEEE 754 可能的类别,好比 NegativeInfinity, PositiveZero 或 SignalingNaN。

Mirror.AncestorRepresentation 和 Mirror.DisplayStyle 这两个枚举被用在 Swift 反射 API 的上下文当中。

Optional 这个就不用多说了

Process 这个枚举包含了当前进程的命令行参数(Process.argc, Process.arguments)。这是一个至关有趣的枚举类型,由于在 Swift 1.0 当中,它是被做为一个结构体来实现的。

实践用例

咱们已经在前面几个小节当中看过了许多有用的枚举类型。包括 Optional,Either, FileNode 还有二叉树。然而,还存在不少场合,使用枚举要赛过使用结构体和类。通常来说,若是问题能够被分解为有限的不一样类别,则使用枚举应该就是正确的选择。即便只有两种 case,这也是一个使用枚举的完美场景,正如 Optional 和 Either 类型所展现的。

如下列举了一些枚举类型在实战中的使用示例,能够用来点燃你的创造力。

错误处理

说到枚举的实践使用,固然少不了在 Swift 2.0 当中新推出的错误处理。标记为可抛出的函数能够抛出任何遵照了 ErrorType 空协议的类型。正如 Swift 官方文档中所写的:

Swift 的枚举特别适用于构建一组相关的错误状态,能够经过关联值来为其增长额外的附加信息。 做为一个示例,咱们来看下流行的JSON解析框架 Argo。当 JSON 解析失败的时候,它有多是如下两种主要缘由:

JSON 数据缺乏某些最终模型所须要的键(好比你的模型有一个 username 的属性,可是 JSON 中缺乏了) 存在类型不匹配,好比说 username 须要的是 String 类型,而 JSON 中包含的是 NSNull6。 除此以外,Argo 还为不包含在上述两个类别中的错误提供了自定义错误。它们的 ErrorType 枚举是相似这样的:

enum DecodeError: ErrorType { case TypeMismatch(expected: String, actual: String) case MissingKey(String) case Custom(String) } 全部的 case 都有一个关联值用来包含关于错误的附加信息。

一个更加通用的用于完整 HTTP / REST API 错误处理的ErrorType应该是相似这样的:

enum APIError : ErrorType { // 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: ErrorType) // The Argo decoding Failed case JSONMappingError(converstionError: DecodeError) } 这个 ErrorType 实现了完整的 REST 程序栈解析有可能出现的错误,包含了全部在解析结构体与类时会出现的错误。

若是你看得够仔细,会发如今JSONMappingError中,咱们将Argo中的DecodeError封装到了咱们的APIError类型当中,由于咱们会用 Argo 来做实际的 JSON 解析。

更多关于ErrorType以及此种枚举类型的示例能够参看官方文档。

观察者模式

在 Swift 当中,有许多方法来构建观察模式。若是使用 @objc 兼容标记,则咱们可使用 NSNotificationCenter 或者 KVO。即便不用这个标记,didSet语法也能够很容易地实现简单的观察模式。在这里可使用枚举,它可使被观察者的变化更加清晰明了。设想咱们要对一个集合进行观察。若是咱们稍微思考一下就会发现这只有几种可能的状况:一个或多个项被插入,一个或多个项被删除,一个或多个项被更新。这听起来就是枚举能够完成的工做:

enum Change { case Insertion(items: [Item]) case Deletion(items: [Item]) case Update(items: [Item]) } 以后,观察对象就可使用一个很简洁的方式来获取已经发生的事情的详细信息。这也能够经过为其增长 oldValue 和 newValue 的简单方法来扩展它的功能。

状态码

若是咱们正在使用一个外部系统,而这个系统使用了状态码(或者错误码)来传递错误信息,相似 HTTP 状态码,这种状况下枚举就是一种很明显而且很好的方式来对信息进行封装7 。

enum HttpError: String { case Code400 = "Bad Request" case Code401 = "Unauthorized" case Code402 = "Payment Required" case Code403 = "Forbidden" case Code404 = "Not Found" } 结果类型映射(Map Result Types)

枚举也常常被用于将 JSON 解析后的结果映射成 Swift 的原生类型。这里有一个简短的例子:

enum JSON { case JSONString(Swift.String) case JSONNumber(Double) case JSONObject([String : JSONValue]) case JSONArray([JSONValue]) case JSONBool(Bool) case JSONNull } 相似地,若是咱们解析了其它的东西,也可使用这种方式将解析结果转化咱们 Swift 的类型。

UIKit 标识

枚举能够用来将字符串类型的重用标识或者 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 或者 OSX 须要使用预约义的 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开发者8)。

API 端点

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有一篇极好的文章说明了如何使用枚举来实现一个链表。那篇文章中的大多数代码都超出了枚举的知识,并涉及到了大量其它有趣的主题9,可是,链表最基本的定义是相似这样的(我对其进行了一些简化):

enum List { case End indirect case Node(Int, next: List) } 每个节点(Node) case 都指向了下一个 case, 经过使用枚举而非其它类型,咱们能够避免使用一个可选的 next 类型以用来表示链表的结束。

Airspeed Velocity 还写过一篇超赞的博客,关于如何使用 Swift 的间接枚举类型来实现红黑树,因此若是你已经阅读过关于链表的博客,你可能想继续阅读这篇关于红黑树的博客。

设置字典(Setting Dictionaries)

这是 Erica Sadun 提出的很是很是机智的解决方案。简单来说,就是任何咱们须要用一个属性的字典来对一个项进行设置的时候,都应该使用一系列有关联值的枚举来替代。使用这方法,类型检查系统能够确保配置的值都是正确的类型。

关于更多的细节,以及合适的例子,能够阅读下她的文章。

局限

与以前相似,我将会用一系列枚举的局限性来结束本篇文章。

提取关联值

David Owens写过一篇文章,他以为当前的关联值提取方式是很笨重的。我墙裂推荐你去看一下他的原文,在这里我对它的要旨进行下说明:为了从一个枚举中获取关联值,咱们必须使用模式匹配。然而,关联值就是关联在特定枚举 case 的高效元组。而元组是可使用更简单的方式来获取它内部值,即 .keyword 或者 .0。

// Enums 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 (译者注:一开始我不明白 rdar 是啥意思,后来我 google 了下,若是你也有兴趣,也能够本身去搜索一下)

相等性

拥有关联值的枚举没有遵照 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 } } 元组(Tuples)

最大的问题就是对元组的支持。我喜欢使用元组,它们可使不少事情变得更简单,可是他们目前还处于无文档状态而且在不少场合都没法使用。在枚举当中,咱们没法使用元组做为枚举的值:

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

这个咱们已经在前面讨论过了。目前尚未一个很好的方法来得到枚举中的全部 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,可是角色的默认设置依然会被映射。

变化

10/26/2015

增长局限性示例(相等性 & 获取关联值) 增长 Erica Sadun 的关联枚举示例 10/22/2015

合并来自 #6 @mabidakun的PR 增长枚举底层的连接 将账号示例拆分为两个更容易理解的片断。 10/21/2015

合并来自 #4 @blixt和#2 @kandelvijayavolare和#3 @sriniram以及#5 @SixFiveSoftware的PR 为账号示例添加调用代码 增长 ErrorType 示例 解释

一、可使用一些小技术来达到这个目的,具体的请参照下面的文章内容

二、为了演示的缘故,这个示例的实现通过的简化。在真实的开发中,应该使用可选类型以及反向顺序的参数。能够参考一下如今十分流行的函数式编程库,如 Swiftz 和 Dollar

三、这个示例直接采用了Swift 官方文档的示例

四、常常使得他们定义的位置很难被发现

五、这是一个简化版的,固然,Swift 为咱们加了不少的语法糖

六、若是你在应用中使用过 JSON,应该也曾经碰到过这个问题

七、顺便一提,不能直接使用数字作为枚举 case 的名称,所以直接使用 400 是不行的

八、虽然如此,不过支持 Mac 版的 R.swift 好像就快推出了

九、这句话能够解释为: 打开连接,并开始阅读文章

相关文章
相关标签/搜索