Swift 枚举

枚举为一组相关值定义一个公共类型,并使我们能够在代码中以类型安全的方式使用这些值。

如果我们熟悉C,我们将知道C枚举将相关名称分配给一组整数值。Swift中的枚举要灵活得多,不必为枚举的每种情况提供一个值。如果为每个枚举情况提供了一个值(称为原始值),则该值可以是字符串、字符或任何整数或浮点类型的值。

或者,枚举case可以指定任何类型的关联值,这些值将与每个不同的case值一起存储,就像在其他语言中的联合或变体所做的那样。可以将一组公共的相关case定义为一个枚举的一部分,每个枚举都有一组与之关联的适当类型的不同值。

Swift中的枚举本身就是一级类型。它们采用了许多传统上仅由类支持的特性,例如,计算属性提供有关枚举当前值的附加信息,实例方法提供与枚举表示的值相关的功能。枚举还可以定义初始值设定项以提供初始大小写值;可以扩展以将其功能扩展到原始实现之外;并且可以遵守协议以提供标准功能。

有关这些功能的更多信息,请参阅属性方法初始化扩展协议

枚举语法

使用enum关键字引入枚举,并将其整个定义放在一对大括号中:

enum SomeEnumeration {
    // enumeration definition goes here
}

以下是罗盘四个主要点的示例:

enum CompassPoint {
    case north
    case south
    case east
    case west
}

枚举中定义的值(如north、south、east和west)是其枚举case。我们可以使用case关键字来引入新的枚举案例。

注意
Swift枚举案例在默认情况下不设置整数值,这与C和Objective-C等语言不同。在上面的CompassPoint示例中,north、south、east和west并不隐式地等于0、1、2和3。相反,不同的枚举case本身就是值,具有显式定义的CompassPoint类型。

多个case可以出现在一行中,用逗号分隔:

enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

每个枚举定义定义一个新类型。与Swift中的其他类型一样,它们的名称(如CompassPointPlanet)以大写字母开头。为枚举类型指定单数名称而不是复数名称,以便它们的含义不言而喻:

var directionToHead = CompassPoint.west

directionToHead的类型是在用CompassPoint的一个可能值初始化时推断出来的。directionToHead声明为CompassPoint后,可以使用较短的点语法将其设置为其他CompassPoint值:

directionToHead = .east

directionToHead的类型是已知的,因此在设置其值时可以删除该类型。当使用显式类型化的枚举值时,这使得代码具有高度可读性。

将枚举值与Switch语句匹配

可以使用switch语句匹配各个枚举值:

directionToHead = .south
switch directionToHead {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east:
    print("Where the sun rises")
case .west:
    print("Where the skies are blue")
}
// Prints "Watch out for penguins"

我们可以将此代码理解为:
“考虑directionToHead的值。如果它等于.north,打印“Lots of planets have a north”。如果它等于.south,打印“Watch out for penguins”
……等等。

控制流中所述,在考虑枚举的情况时,switch语句必须是详尽的。如果省略.west的case,则此代码不会编译成功,因为它没有考虑到CompassPoint case的完整列表。要求穷尽性可以确保枚举案例不会被不小心忽略。

如果不适合为每个枚举case提供一个case,则可以提供一个默认case来涵盖任何未明确解决的案例:

let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}
// Prints "Mostly harmless"

迭代枚举案例

对于某些枚举,收集该枚举的所有case是很有用的。我们可以通过在枚举的名称后写入:CaseIterable来启用它。Swift将所有case的集合公开为枚举类型的allCases属性。举个例子:

enum Beverage: CaseIterable {
    case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Prints "3 beverages available"

在上面的例子中,我们写Beverage.allCases访问包含Beverage枚举的所有case的集合。我们可以像使用任何其他集合一样使用allcase集合的元素,因为它的元素是枚举类型的实例,因此在本例中它们是Beverage值。上面的示例统计有多少个case,下面的示例使用for循环迭代所有case。

for beverage in Beverage.allCases {
    print(beverage)
}
// coffee
// tea
// juice

上述示例中使用的语法将枚举标记为遵守CaseIterable协议。有关协议的信息,请参阅协议

关联值

上一节中的示例展示了枚举的case本身是如何定义(并命名)值的。可以将常量或变量设置为Planet.earth,稍后检查此值。但是,有时能够将其他类型的值(附加信息)与这些case值一起存储是有用的。这个附加信息称为关联值,每次将case用作代码中的值时,它都会发生变化。

我们可以定义Swift枚举来存储任何给定类型的关联值,如果需要,值类型对于枚举的每个case都可以不同。这些枚举类似于其他编程语言中的有区别的联合、有标记的联合或其变体。

例如,假设库存跟踪系统需要通过两种不同类型的条形码跟踪产品。有些产品用UPC格式的1D条形码进行标记,使用数字0到9。每个条形码有一个数字系统的数字,后跟五个制造商代码数字和五个产品代码数字。后面是一个校验位,用于验证代码是否已正确扫描:


截屏2021-03-14 下午7.46.16.png

其他产品采用二维条码 QR 码格式进行标记,二维条码可以使用任何ISO 8859-1字符,并且可以对长度不超过2953个字符的字符串进行编码:


截屏2021-03-14 下午7.46.21.png

对于库存跟踪系统来说,将UPC条码存储为四个整数的元组,将QR条码存储为任意长度的字符串非常方便。

在Swift中,用于定义任一类型产品条形码的枚举可能如下所示:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

这可以理解为:
定义一个名为Barcode的枚举类型,它可以采用带有(Int,Int,Int,Int)类型关联值的upc值,也可以采用带有String类型关联值的qrCode值。

这个定义不提供任何实际的Int或String值,它只定义了当Barcode常量和变量等于Barcode.upc或者Barcode.qrCode时,可以存储的关联值的类型。

然后可以使用以下任一类型创建新条形码:

var productBarcode = Barcode.upc(8, 85909, 51226, 3)

本例创建了一个名为productBarcode的新变量,并为其赋值为关联元组值为(88590951226,3)的Barcode.upc值。

我们可以为同一产品分配不同类型的条形码:

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

此时,原来的Barcode.upc和其整数值将替换为新的Barcode.qrCode以及它的字符串值。Barcode类型的常量和变量可以存储.upc.qrCode(及其关联值),但在任何给定时间只能存储其中一个。

可以使用switch语句检查不同的条形码类型,类似于用switch语句匹配枚举值的case。但是,这一次,关联值被提取为switch语句的一部分。将每个关联值提取为常量(带let前缀)或变量(带var前缀),以便在switch case的主体中使用:

switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

如果枚举case的所有关联值都提取为常量,或者所有值都提取为变量,以简洁起见,则可以在case名称前放置单个var或let注释:

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

原始值

关联值中的条形码示例显示枚举的case如何声明它们存储不同类型的关联值。作为关联值的替代方法,枚举case可以预先填充默认值(称为原始值),这些值都是相同类型的。

下面是一个将原始ASCII值与命名枚举case一起存储的示例:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

这里,名为ASCIIControlCharacter的枚举的原始值被定义为Character类型,并被设置为一些更常见的ASCII控制字符。Character值在字符串和字符来描述。

原始值可以是字符串、字符或任何整数或浮点数类型。每个原始值在其枚举声明中必须是唯一的。

注意
原始值与关联值不同。当我们第一次在代码中定义枚举时,原始值被设置为预先填充的值,就像上面的三个ASCII代码一样。特定枚举case的原始值始终相同。关联值是基于枚举的一个case创建新的常量或变量时设置的,并且每次创建时都可能不同。

隐式指定的原始值

当使用存储整数或字符串原始值的枚举时,不必为每种情况显式指定原始值。这种情况下如果没有指定原始值,Swift会自动为我们指定值。

例如,当整数用于原始值时,每个case的隐式值都比前一个case加1。如果第一个case没有设置值,则其值为0。

下面的枚举是对早期行Planet枚举的改进,使用整数原始值来表示每个行星距离太阳的顺序:

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

在上面的例子中,Planet.mercury 显式原始值为1,Planet.venus具有隐式原始值2,依此类推。

当字符串用于原始值时,每个case的隐式值都是该case名称的文本。

下面的枚举是对早期CompassPoint枚举的改进,使用字符串原始值来表示每个方向的名称:

enum CompassPoint: String {
    case north, south, east, west
}

在上面的例子中,CompassPoint.south具有隐含的原始值“south”,以此类推。

可以使用枚举case的rawValue属性访问其原始值:

let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"

根据原始值初始化

如果定义具有原始值类型的枚举,枚举将自动接收一个初始值设定项,该初始值设置项接受原始值类型的值(作为一个名为rawValue的参数),并返回case或为nil。我们可以使用此初始值设置项尝试创建枚举的新case。

本示例根据其原始值7中创建了天王星:

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus

然而,并非所有可能的Int值都能找到匹配的行星。因此,原始值的初始值设定项总是返回可选的枚举case。在上面的例子中,possiblePlanetPlanet?,或“可选Planet

注意
原始值初始值设定项是一个可能失败的初始值设定项,因为不是每个原始值都会返回枚举case。有关详细信息,请参阅失败的初始值设定项

如果尝试查找位置为11的行星,则原始值初始值设定项返回的可选行星值将为nil

let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
    switch somePlanet {
    case .earth:
        print("Mostly harmless")
    default:
        print("Not a safe place for humans")
    }
} else {
    print("There isn't a planet at position \(positionToFind)")
}
// Prints "There isn't a planet at position 11"

本例使用可选绑定来尝试访问原始值为11的行星。如果let somePlanet = Planet(rawValue: 11)语句创建一个可选Planet,并将somePlanet设置为该可选Planet的值(如果可以检索)。在本例中,无法检索位置为11的Planet,因此执行else分支。

递归枚举

递归枚举是将枚举的另一个实例作为一个或多个枚举case的关联值的枚举。我们可以通过在枚举case之前写入indirect来指示它是递归的,这会告诉编译器插入必要的间接层。

例如,以下是存储简单算术表达式的枚举:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

也可以在枚举开始之前写入indirect,以便为具有关联值的枚举的所有case启用间接寻址:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

此枚举可以存储三种算术表达式:普通数、两个表达式的加法和两个表达式的乘法。additionmultiplication都有关联的值,这些值也是算术表达式。这些关联值使嵌套表达式成为可能。例如,表达式(5 + 4) * 2在乘号的右侧有一个数字,在乘号的左侧有另一个表达式。因为数据是嵌套的,所以用于存储数据的枚举也需要支持嵌套这意味着枚举需要是递归的。下面的代码显示了为(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))

递归函数是处理具有递归结构的数据的简单方法。例如,下面是一个计算算术表达式的函数:

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))
// Prints "18"

此函数通过简单地返回关联的值来计算普通数。它通过计算左侧的表达式,计算右侧的表达式,然后将它们相加或相乘来计算加法或乘法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持...
    小驴拉磨阅读 290评论 0 0
  • 枚举定义一组相关值的公共类型,并允许您在代码中以类型安全的方式使用这些值。 如果你熟悉C,你会知道C枚举分配相关的...
    Joker_King阅读 246评论 0 0
  • 枚举为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。 如果你熟悉 C 语言,...
    CDLOG阅读 330评论 0 0
  • 枚举为一组相关的值定义了一个共同的类型,使得可以在代码中以类型安全的方式来使用这些值。 如果熟悉 C 语言,会知道...
    穷人家的孩纸阅读 844评论 1 4
  • 枚举语法 使用 enum 关键词来创建枚举 指南针四个方向 与 C 和 Objective-C 不同,north,...
    Sunday_David阅读 191评论 0 0