前言
和switch语句类似,Swift
中的枚举乍看之下更像是C
语言中枚举的进阶版本,即允许你定义一种类型,用于表示普通事情中某种用例。不过深入挖掘之后,凭借Swift
背后特别的设计理念,相比较C
语言枚举来说其在实际场景中的应用更为广泛。特别是作为强大的工具,Swift
中的枚举能够清晰表达代码的意图。
本文中,我们将首先了解基础语法和使用枚举的可能性,接着通过实战教你如何以及何时使用枚举。最后我们还会大致了解下Swift标准库中枚举是如何被使用的。
正式开始学习之前,先给出枚举的定义。之后我们将回过头再来讨论它。
枚举声明的类型是囊括可能状态的有限集,且可以具有附加值。通过内嵌(nesting),方法(method),关联值(associated values)和模式匹配(pattern matching),枚举可以分层次地定义任何有组织的数据。
定义基本的枚举类型(Defining Basic Enums)
试想我们正在开发一款游戏,玩家能够朝四个方向移动。所以喽,玩家的运动轨迹受到了限制。显然,我们能够使用枚举来表述这一情况:
enum Movement{
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)
因此你无法为枚举分配诸如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
中进行内容更新。倘若我们从函数式编程中获得启发,这将变得更好。
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<Int>)
case Multiple([Range<Int>])
}
// 或者映射不同的标识码
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
值了.
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附加功能,这意味着无须依靠额外函数就能实现
// 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: Error {
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: 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)"
}
}
}