枚举定义了一个通用类型的一组相关值,使你可以在你的代码中以一种安全的方式来使用这些值。
如果你熟悉 C语言,你就会知道,在 C语言中枚举将枚举名和一个整型值相对应。Swift中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为“原始”值),则该值的类型可以是字符串,字符,或是一个整型值或浮点数。
此外,枚举成员可以指定任何类型的相关值存储到枚举成员值中,就像其他语言中的联合体(unions)和变体(variants)。你可以定义一组通用的相关成员作为枚举的一部分,每一组都有不同的一组与它相关的适当类型的数值。
在 Swift中,枚举类型是一等公民(first-class)。它们采用了很多传统上只被类(class)所支持的特征,例如计算型属性(computed properties),用于提供关于枚举当前值的附加信息,实例方法(instance methods),用于提供和枚举所代表的值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始的实现基础上扩展它们的功能;可以遵守协议(protocols)来提供标准的功能。
如果你熟悉 C语言,你就会知道,在 C语言中枚举将枚举名和一个整型值相对应。Swift中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为“原始”值),则该值的类型可以是字符串,字符,或是一个整型值或浮点数。
此外,枚举成员可以指定任何类型的相关值存储到枚举成员值中,就像其他语言中的联合体(unions)和变体(variants)。你可以定义一组通用的相关成员作为枚举的一部分,每一组都有不同的一组与它相关的适当类型的数值。
在 Swift中,枚举类型是一等公民(first-class)。它们采用了很多传统上只被类(class)所支持的特征,例如计算型属性(computed properties),用于提供关于枚举当前值的附加信息,实例方法(instance methods),用于提供和枚举所代表的值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始的实现基础上扩展它们的功能;可以遵守协议(protocols)来提供标准的功能。
枚举定义语法
首先,我们来看看在 swift中定义枚举的语法,使用enum关键词来创建枚举并且把它们的整个定义放在一对大括号内:
enum WeekDay {
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
case Saturday
case Sunday
}
一个枚举中被定义的值(例如 Monday,Tuesday等)是枚举的成员值(或者成员)。Swift的每个枚举项前面,都使用一个case关键字来标识。case关键词表明新的一行成员值将被定义。除了每行声明一个枚举项,我们也可以将这些枚举项放在一行中声明,每项之间用逗号分隔,如下:
enum WeekDayInSingleLine {
case Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
}
注意:和C和Objective-C不同,Swift的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的WeekDay例子中,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday不会隐式地赋值为了0,1,2,3,4,5,6。相反的,这些不同的枚举成员在WeekDay的一种显示定义中可以拥有各自不同的值。
枚举类型定义好之后,我们就可以将它的枚举值赋值给某个变量:
var weekday = WeekDay.Tuesday
并且,对于类型明确的变量,我们可以直接省去枚举的类型前缀:
var day:WeekDay = .Wednesday
枚举的使用,枚举值可以在switch语句中进行匹配:
switch weekday {
case .Monday:
print("Monday")
case .Tuesday:
print("Tuesday")
case .Wednesday:
print("Wednesday")
case .Thursday:
print("Thursday")
case .Friday:
print("Friday")
case .Saturday:
print("Saturday")
case .Sunday:
print("Sunday")
}
在switch语句中的每个case中,我们提供各个枚举项的名称:.Monday,.Tuesday等等。在 swift 中switch中匹配枚举项,必须显示的列举出所有的枚举项。也就是对于我们上面表示星期的枚举类型 WeekDay, 我们对它的switch语句中必须将所有的枚举项分支都明确的写出来。否则就会有编译错误,如:error:switch must be exhaustive, consider adding adefault clause。
上面代码可以这样理解,判断weekday的值,当等于.Monday的时候,打印.Monday,等于.Tuesday的时候,打印.Tuesday,其它以此类推。这个机制也体现了Swift类型安全的核心思想。如果我们觉得每个枚举项都要明确的指定行为比较麻烦,我们还可以使用default分支来对于其余的枚举项定义行为:
switch weekday {
case .Saturday:
print("Saturday")
case .Sunday:
print("Sunday")
default:
print("default")
}
总之,无论用default也好,还是明确对每一个枚举项指定行为也好,在 Swift 中,我们都必须对枚举类型下的每个值,指定确定的行为。不能漏掉其中任何一个可能性。
方法和属性(Methods and properties)
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
//静态变量 枚举不能包含存储属性,但是可以包含静态变量和计算属性
static var property = "some value"
//静态计算属性
static var computedProperty: String{
return "computedProperty"
}
//实例方法
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"
}
}
//计算属性
var year: Int{
switch self {
case .iPhone:
return 2007
default:
return 0
}
}
//静态方法
static func fromTerm(term:String) ->Device?{
if term == "iWatch"{
return .AppleWatch
}
return nil
}
//可失败构造方法
init?(term: String) {
if term == "iWatch" {
self = .AppleWatch
}
return nil
}
//可变方法
mutating func changeBrand(){
switch self {
case .iPad:
self = .iPhone
case .iPhone:
self = .AppleTV
case .AppleTV:
self = .AppleWatch
case .AppleWatch:
self = .iPad
}
}
}
使用如下:
print(Device.property) // some value
print(Device.computedProperty) // computedProperty
print(Device.iPhone.year) //2007
print (Device.iPhone.introduced()) // prints: "iPhone was introduced 2007"
//在这个示例中,我们需要考虑用户有时将苹果设备叫错的情况(比如AppleWatch叫成iWatch),需要返回一个合适的名称。
print (Device.fromTerm(term: "iWatch"))
print(Device(term: "appleWatch"))
//方法可以声明为mutating。这样就允许改变隐藏参数self的case值了
var brand = Device.iPad //iPad
brand.changeBrand() //iPhone
brand.changeBrand() //AppleTV
brand.changeBrand() //AppleWatch
嵌套枚举(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
同样地,也能够在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)
协议(Protocols)和协议扩展(Protocol Extension)
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)") //print: this action is We're buying something
枚举也可以进行扩展。最明显的用例就是将枚举的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类型:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
}
直接使用如下,枚举也可以拥有多个泛型参数.
let aValue = Optional<Int>.some(10) //10
let noValue = Optional<Int>.none //nil
关联值(Associated Values)
在 Swift中,我们还可以定义这样的枚举类型,它的每一个枚举项都有一个附加信息,来扩充这个枚举项的信息表示,这叫做关联值。可以定义 Swift的枚举存储任何类型的相关值,如果需要的话,每个成员的数据类型可以是各不相同的。
假如我们有一个枚举类型 Shape来表示形状。这个形状可以是矩形,也可以是圆形,等等。而每种具体的形状又对应了不同的属性,比如矩形有长,宽,圆形有,圆心,半径,等等。那么枚举的关联值就可以帮我们解决这个问题:
enum Shape {
case Rectangle(CGRect)
case Circle(CGPoint,Int)
}
我们看到,每个枚举项的后面,都包含了一对括号,这里面定义了这个枚举项的关联值的类型。对于Rectangle我们使用一个CGRect来表示他的原点和长宽属性。而对于 Circle,我们使用一个包含了CGPoint和Int类型的元组(Tuple)来表示这个圆的圆心和半径。
这样我们在初始化枚举类型的时候,我们就可以根据每个枚举项的关联值类型,为它指定附加信息了:
var rect = Shape.Rectangle(CGRect(x: 0, y: 0, width: 200, height: 200))
var circle = Shape.Circle(CGPoint(x: 25, y: 25) , 20)
关联值的枚举项在switch语句中的用法:
switch(rect) {
case .Rectangle(let rect):
print("this is a rectangle at \(rect)")
case let .Circle(center, radius):
print("this is a circle at \(center) with radius \(radius)")
}
我们在case后面用一对括号来输出枚举项的关联值,可以用let或者var关键字,分别作为常量和变量进行输出。我们这里这样来使用case .Rectangle(let rect)。对于关联值是包含多个值的元组类型的,我们可以将let关键字放置在枚举项类型的前面,这样就可以不用对每个关联值都声明let关键字了,let .Circle(center, radius)。元组参数(Tuple as Arguments)
语法允许将元组当作一个简单的数据结构,稍后元组将自动转换到高级类型,就比如enumcase。想象一个应用程序可以让用户来配置电脑:
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中进行内容更新。倘若我们从函数式编程中获得启发,这将变得更好.
precedencegroup ComparisonPrecedence {
associativity: left
}
infix operator <^> : ComparisonPrecedence
func <^>(a: Config, f: (Config) -> Config) -> Config {
return f(a)
}
//最后,我们可以将不同配置步骤串联起来。这在配置步骤繁多的情况下相当有用。
let config = (0, "", "") <^> selectRAM <^> selectCPU <^> selectGPU
let aCube = Desktop.Cube(config)
原始值(Raw Values)
和关联值不同,它为枚举项提供一个默认值,这个默认值是在编译的时候就确定的。而不像关联值那样,要再实例化枚举值的时候才能确定。这也就是说,原始值对于同一个枚举项都是一样的。而关联值对于同一个枚举项只是值的类型相同,但具体的取值也是不同的。
//定义枚举原始值(Raw Values)的方法
enum WeekDayWithRaw : String {
case Monday = "1. Monday"
case Tuesday = "2. Tuesday"
case Wednesday = "3. Wednesday"
case Thursday = "4. Thursday"
case Friday = "5. Friday"
case Saturday = "6. Saturday"
case Sunday = "7. Sunday"
}
还是表示星期的枚举类型,我们对每个枚举项都定义了一个默认的原始值,注意一下我们定义枚举的第一行代码,enumWeekDayWithRaw : String我们在枚举定义的最后,多加了一个 String关键字,这就表示这个枚举的原始值(Raw Values)是 String类型的。对于所有的枚举项,我们赋给的原始值都是 String类型的。定义好了原始值后,我们就可以用枚举项的 rawValue属性来输出它:
print(WeekDayWithRaw.Friday.rawValue) //5. Friday
//我们还可以通过原始值(Raw Values) 来初始化枚举类型,这个初始化方法的返回值是一个Optionals。可以返回一个具体的值,也可以返回 nil
if let day = WeekDayWithRaw(rawValue: "3. Wednesday"){
print(day)
}else{
print("init fail")
}
//原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。
原始值的隐式赋值(Implicitly Assigned Raw Values)
在使用原始值为整数或者字符串类型的枚举时,不需要显式的为每一个成员赋值,这时,Swift将会自动为你赋值。例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。
在使用原始值为整数或者字符串类型的枚举时,不需要显式的为每一个成员赋值,这时,Swift将会自动为你赋值。例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。
下面是Planet枚举,利用原始整型值来表示每个 planet在太阳系中的顺序:
enum Planet: Int {
case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
在上面的例子中,Plant.Mercury赋了初值1,Planet.Venus会拥有隐式赋值2,依次类推。当使用字符串作为枚举类型的初值时,每个枚举成员的隐式初值则为该成员的名称。下面的例子是CompassPoint枚举,使用字符串作为初值类型,隐式初始化为各个方向的名称:
enum CompassPoint: String {
case North, South, East, West
}
上面例子中,CompassPoint.South拥有隐式初值South,依次类推。使用枚举成员的rawValue属性可以访问该枚举成员的原始值:
let earthsOrder = Planet.Earth.rawValue
// earthsOrder 值为 3
let sunsetDirection = CompassPoint.West.rawValue
// sunsetDirection 值为 "West"
递归枚举(Recursive Enumerations)
递归枚举(recursive enumeration)是一种枚举类型,表示它的枚举中,有一个或多个枚举成员拥有该枚举的其他成员作为相关值。使用递归枚举时,编译器会插入一个中间层。你可以在枚举成员前加上indirect来表示这成员可递归。例如,下面的例子中,枚举类型存储了简单的算数表达式:
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)
}
上面定义的枚举类型可以存储三种算数表达式:纯数字、两个表达式的相加、两个表达式相乘。Addition和 Multiplication成员的相关值也是算数表达式————这些相关值使得嵌套表达式成为可能。递归函数可以很直观地使用具有递归性质的数据结构。例如,下面是一个计算算数表达式的函数:
func evaluate(expression: ArithmeticExpression) -> Int {
switch expression {
case .Number(let value):
return value
case .Addition(let left, let right):
return evaluate(expression: left) + evaluate(expression: right)
case .Multiplication(let left, let right):
return evaluate(expression: left) * evaluate(expression: right)
}
}
// 计算 (5 + 4) * 2
let five = ArithmeticExpression.Number(5)
let four = ArithmeticExpression.Number(4)
let sum = ArithmeticExpression.Addition(five, four)
let product = ArithmeticExpression.Multiplication(sum, ArithmeticExpression.Number(2))
print(evaluate(expression: product))
// 输出 "18"
使用自定义类型作为枚举的值
如果我们忽略关联值,则枚举的值就只能是整型,浮点型,字符串和布尔类型。如果想要支持别的类型,则可以通过实现ExpressibleByStringLiteral协议来完成,这可以让我们通过对字符串的序列化和反序列化来使枚举支持自定义类型。
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)
}
然而,这段代码不能通过编译。因为CGSize并不是一个常量,不能用来定义枚举的值。错误提示如上图,我们需要为想要支持的自定义类型增加一个扩展,让其实现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}"
}
终于,我们可以使用 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)")
//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)才对它们的真实关联值进行判断。
实践用例
UIKit 标识
枚举可以用来将字符串类型的重用标识或者 storyboard标识映射为类型系统可以进行检查的类型。假设我们有一个拥有很多原型 Cell的 UITableView:
enum CellType: String {
case ButtonValueCell = "ButtonValueCell"
case UnitEditCell = "UnitEditCell"
case LabelCell = "LabelCell"
case ResultLabelCell = "ResultLabelCell"
}
状态码
如果我们正在使用一个外部系统,而这个系统使用了状态码(或者错误码)来传递错误信息,类似 HTTP 状态码,这种情况下枚举就是一种很明显并且很好的方式来对信息进行封装7。
enum HttpError: String {
case Code400 = "Bad Request"
case Code401 = "Unauthorized"
case Code402 = "Payment Required"
case Code403 = "Forbidden"
case Code404 = "Not Found"
}
对于更多实际用例内容可以看这里
参考: