You will be learning about a lot of new tools and concepts in the chapters included in this part of the book. You will be adding features to projects that you will change in later projects. This will simulate the act of writing code in a real way: sometimes you start developing an application with one solution in mind and then have to modify your code when you learn a better pattern or a feature has changed. That does not mean the first code or tools were bad - just that they would be better for other circumstances. Projects often evolve and develop, and decisions that are ideal at one stage may become inadequate as requirements change. Learning to be flexible in the face of these changes is part of the trade. 在本书这一部分的章节中,您将学习许多新的工具和概念。您将为项目添加功能,而这些项目将在后续的项目中进行更改。这将模拟真实的编码过程:有时您开始开发一个应用程序时心中有一个解决方案,但当您学习到更好的模式或功能发生变化时,您需要修改代码。这并不意味着最初的代码或工具不好——只是它们在其他情况下会更好。项目通常会演变和发展,在一个阶段理想的决策可能会随着需求的变化而变得不够充分。学会在这些变化面前保持灵活是这一行业的一部分。
This page intentionally left blank 此页故意留空
14
Enumerations 枚举
As you have worked through the book up to this point, you have been using all the built-in types that Swift provides, like integers, strings, arrays, and dictionaries. The next couple of chapters will show the capabilities the language provides to create your own types. The focus of this chapter is enumerations (or enums), which allow you to create instances that are one of a predefined list of cases. If you have used enumerations in other languages, much of this chapter will be familiar. But Swift’s enums also have some advanced features that make them unique. 随着你在本书中学习到这一点,你已经使用了 Swift 提供的所有内置类型,如整数、字符串、数组和字典。接下来的几章将展示该语言提供的创建自定义类型的能力。本章的重点是枚举(或称为 enums),它允许你创建一个预定义案例列表中的实例。如果你在其他语言中使用过枚举,本章的内容将会很熟悉。但 Swift 的枚举还有一些独特的高级特性。
Basic Enumerations 基本枚举
Create a new playground called Enumerations.playground. Define an enumeration of possible text alignments: 创建一个名为 Enumerations.playground 的新游乐场。定义一个可能的文本对齐方式的枚举:
Listing 14.1 Defining an enumeration 清单 14.1 定义枚举
enum TextAlignment {
case Left
case Right
case Center
}
You define an enumeration with the enum keyword followed by the name of the enumeration. The opening brace ( {\{ ) opens the body of the enum, and it must contain at least one case statement that declares the possible values for the enum. Here, you include three. The name of the enumeration (TextAlignment in this case) is now usable as a type, just like Int or String or the various other types you have used so far. 您使用 enum 关键字定义一个枚举,后面跟着枚举的名称。开括号 ( {\{ ) 打开枚举的主体,必须至少包含一个 case 语句来声明枚举的可能值。在这里,您包含了三个。枚举的名称(在这种情况下为 TextAlignment)现在可以作为类型使用,就像您迄今为止使用的 Int、String 或其他各种类型一样。
Types (and, therefore, enums) are named with a capital first letter by convention. If multiple words are needed, use camel-casing: UpperCamelCasedType. Variables and functions begin with a lowercase first letter, and also use camel-casing as needed. 类型(因此,枚举)按照惯例以大写字母开头。如果需要多个单词,则使用驼峰命名法:UpperCamelCasedType。变量和函数以小写字母开头,并根据需要使用驼峰命名法。
Because the enumeration declares a new type, you can now create instances of that type. 因为枚举声明了一个新类型,所以您现在可以创建该类型的实例。
Listing 14.2 Creating an instance of TextAlignment 清单 14.2 创建 TextAlignment 的实例
enum TextAlignment {
case Left
case Right
case Center
}
var alignment: TextAlignment = TextAlignment.Left
Although TextAlignment is a type that you have defined, the compiler can still infer the type for alignment. Therefore, you can omit the explicit type of the alignment variable: 尽管 TextAlignment 是您定义的一个类型,但编译器仍然可以推断对齐的类型。因此,您可以省略对齐变量的显式类型:
Listing 14.3 Taking advantage of type inference 清单 14.3 利用类型推断
...
var alignment+ TextAlignment = TextAlignment.Left
The compiler’s ability to infer the type of enumerations is not limited to variable declarations. If you have a variable known to be of a particular enum type, you can omit the type from the case statement when assigning a new value to the variable. 编译器推断枚举类型的能力不限于变量声明。如果您有一个已知为特定枚举类型的变量,在给该变量赋新值时,可以在 case 语句中省略类型。
Listing 14.4 Inferring the enum type 清单 14.4 推断枚举类型
...
var alignment = TextAlignment.Left
alignment = .Right
Notice that you had to specify the enum’s type and value when initially creating the alignment variable, because that line gives alignment both its type and its value. In the next line, you can omit the type and simply reassign alignment to be equal to a different value within its type. You can also omit the enum type when passing its values to functions or comparing them. 请注意,在最初创建对齐变量时,您必须指定枚举的类型和值,因为该行同时为对齐提供了类型和值。在下一行中,您可以省略类型,只需将对齐重新赋值为其类型内的不同值。当将枚举值传递给函数或进行比较时,您也可以省略枚举类型。
Listing 14.5 Type inference when comparing values 清单 14.5 比较值时的类型推断
...
alignment = .Right
if alignment == .Right {
print("we should right-align the text!")
}
While enum values can be compared in if statements, switch statements are typically used to handle enum values. Use switch to print the alignment in a human-readable way. 虽然枚举值可以在 if 语句中进行比较,但通常使用 switch 语句来处理枚举值。使用 switch 以人类可读的方式打印对齐。
Listing 14.6 Switching to switch 清单 14.6 切换到开关
...
alignment = .Right
if alignnent = - Right-{
print("we-should right-align-the-text!")
}
switch alignment {
case .Left:
print("left aligned")
case .Right:
print("right aligned")
case .Center:
print("center aligned")
}
Recall from Chapter 5 that all switch statements must be exhaustive. In that chapter, you wrote switch statements that included a default case. When switching on enumeration values, that is not necessary: The compiler knows all possible values the enumeration can check. If you have included a case for each one, the switch is exhaustive. 回想一下第五章,所有的 switch 语句必须是穷尽的。在那一章中,你编写了包含默认情况的 switch 语句。当对枚举值进行切换时,这并不是必要的:编译器知道枚举可以检查的所有可能值。如果你为每个值都包含了一个 case,那么这个 switch 就是穷尽的。
You could include a default case when switching on an enum type. 您可以在对枚举类型进行切换时包含一个默认情况。
Listing 14.7 Making center the default case 清单 14.7 将中心设置为默认情况
...
switch alignment {
case .Left:
print("left aligned")
case .Right:
print("right aligned")
default:
print("center aligned")
}
ease.Center:
This code works, but we recommend avoiding default clauses when switching on enum types, because using a default is not as “future proof.” Suppose you later want to add another alignment option for justified text. 这段代码可以正常工作,但我们建议在切换枚举类型时避免使用默认子句,因为使用默认子句并不是“面向未来”。假设您稍后想为对齐文本添加另一个选项。
Listing 14.8 Adding a case 清单 14.8 添加一个案例
enum TextAlignment {
case Left
case Right
case Center
case Justify
}
var alignment = TextAlignment.teftJustify
alignment = Right
...
Notice that your program still runs, but it now prints the wrong value. The alignment variable is set to Justify, but the switch statement prints “center aligned.” This is what we mean when we say that using a default is not future proof: it adds complication to modifying your code in the future. 请注意,您的程序仍然运行,但现在打印了错误的值。对齐变量设置为“对齐”,但开关语句打印“居中对齐”。这就是我们所说的使用默认值并不具备未来保障的原因:它增加了将来修改代码的复杂性。
Change your switch back to listing each case explicitly. 将您的开关更改回逐个列出每个案例。
Listing 14.9 Returning to explicit cases 清单 14.9 返回到显式案例
...
switch alignment {
case .Left:
print("left aligned")
case .Right:
print("right aligned")
default+
case .Center:
print("center aligned")
}
Now, instead of your program running and printing the wrong answer, you have a compile-time error that your switch statement is not exhaustive. It may seem odd to say that a compiler error is desirable, but that is exactly the situation here. 现在,您的程序不是运行并打印错误答案,而是出现了编译时错误,提示您的 switch 语句不够全面。说编译器错误是可取的可能听起来奇怪,但这正是这里的情况。
If you use a default clause when switching on an enum, your switch statement will always be exhaustive and satisfy the compiler. If you add a new case to the enum without updating the switch, the switch statement will fall to the default when it encounters the new case. Your code will compile, but it will not do what you intended, as you saw in Listing 14.8. 如果在切换枚举时使用默认分支,您的 switch 语句将始终是详尽的,并满足编译器的要求。如果您向枚举添加了一个新案例而没有更新 switch,当 switch 遇到新案例时,它将转到默认分支。您的代码将编译,但它不会按您预期的那样工作,正如您在清单 14.8 中看到的。
By listing each enum case in the switch, you ensure that the compiler will help you find all of the places in your code that need to be updated if you add cases to your enum. That is what is happening here: the compiler is telling you that your switch statement does not include all of the cases defined in your enum. 通过在 switch 中列出每个枚举案例,您可以确保编译器会帮助您找到代码中需要更新的所有地方,如果您向枚举中添加案例。这就是这里发生的事情:编译器告诉您,您的 switch 语句没有包含枚举中定义的所有案例。
Let’s fix that. 让我们解决这个问题。
Listing 14.10 Including all cases 清单 14.10 包含所有情况
...
switch alignment {
case .Left:
print("left aligned")
case .Right:
print("right aligned")
case .Center:
print("center aligned")
case .Justify:
print("justified")
}
Raw Value Enumerations 原始值枚举
If you have used enumerations in a language like C or C++\mathrm{C}++, you may be surprised to learn that Swift enums do not have an underlying integer type. You can, however, choose to get the same behavior by using what Swift refers to as a raw value. To use Int raw values for your text alignment enumeration, change the declaration of the enum. 如果您在像 C 或 C++\mathrm{C}++ 这样的语言中使用过枚举,您可能会惊讶地发现 Swift 的枚举没有底层整数类型。然而,您可以选择通过使用 Swift 所称的原始值来获得相同的行为。要为您的文本对齐枚举使用 Int 原始值,请更改枚举的声明。
Listing 14.11 Using raw values 清单 14.11 使用原始值
enum TextAlignment: Int {
case Left
case Right
case Center
case Justify
}
...
Specifying a raw value type for TextAlignment gives a distinct raw value of that type (Int) to each case. The default behavior for integral raw values is that the first case gets raw value 0 , the next case gets raw value 1 , and so on. Confirm this by printing some interpolated strings. 指定 TextAlignment 的原始值类型为原始值类型(Int)为每个案例提供一个独特的原始值。整型原始值的默认行为是第一个案例获得原始值 0,第二个案例获得原始值 1,依此类推。通过打印一些插值字符串来确认这一点。
Listing 14.12 Confirming the raw values 清单 14.12 确认原始值
...
var alignment = TextAlignment.Justify
print("Left has raw value \(TextAlignment.Left.rawValue)")
print("Right has raw value \(TextAlignment.Right.rawValue)")
print("Center has raw value \(TextAlignment.Center.rawValue)")
print("Justify has raw value \(TextAlignment.Justify.rawValue)")
print("The alignment variable has raw value \(alignment.rawValue)")
...
You are not limited to the default behavior for raw values. If you prefer, you can specify the raw value for each case. 您不必局限于原始值的默认行为。如果您愿意,可以为每种情况指定原始值。
Listing 14.13 Specifying raw values 清单 14.13 指定原始值
enum TextAlignment: Int {
case Left = 20
case Right = 30
case Center = 40
case Justify = 50
}
...
When is a raw value enumeration useful? The most common reason for using a raw value is to store or transmit the enum. Instead of writing functions to transform a variable holding an enum, you can use rawValue to convert the variable to its raw value. 原始值枚举何时有用?使用原始值的最常见原因是存储或传输枚举。您可以使用 rawValue 将持有枚举的变量转换为其原始值,而不是编写函数来转换该变量。
This brings up another question: if you have a raw value, how do you convert it back to the enum type? Every enum type with a raw value can be created with a rawValue: argument, which returns an optional enum. 这引出了另一个问题:如果你有一个原始值,如何将其转换回枚举类型?每个具有原始值的枚举类型都可以通过 rawValue: 参数创建,该参数返回一个可选的枚举。
Listing 14.14 Converting raw values to enum types 清单 14.14 将原始值转换为枚举类型
:-'
print("Justify has raw value \(TextAlignment.Justify.rawValue)")
print("The alignment variable has raw value \(alignment.rawValue)")
// Create a raw value.
let myRawValue = 20
// Try to convert the raw value into a TextAlignment
if let myAlignment = TextAlignment(rawValue: myRawValue) {
// Conversion succeeded!
print("successfully converted \(myRawValue) into a TextAlignment")
} else {
// Conversion failed.
print("\(myRawValue) has no corresponding TextAlignment case")
}
...
What is going on here? You start with myRawValue, a variable of type Int. Then you try to convert that raw value into a TextAlignment case using TextAlignment(rawValue: ). Because TextAlignment(rawValue:) has a return type of TextAlignment?, you use optional binding to determine whether you get a TextAlignment value or nil back. 这里发生了什么?你从我的原始值 myRawValue 开始,这是一个 Int 类型的变量。然后你尝试使用 TextAlignment(rawValue: ) 将该原始值转换为 TextAlignment 枚举的一个案例。因为 TextAlignment(rawValue:) 的返回类型是 TextAlignment?,所以你使用可选绑定来确定你是得到一个 TextAlignment 值还是 nil。
The raw value you used here corresponds to TextAlignment. Left, so the conversion succeeds. Try changing it to a raw value that does not exist to see the message that conversion is not possible. 您在这里使用的原始值对应于文本对齐方式。左侧,因此转换成功。尝试将其更改为不存在的原始值,以查看无法转换的消息。
Listing 14.15 Trying a bad value 清单 14.15 尝试一个错误的值
let myRawValue = 20 100
…
Figure 14.1 shows the else block being executed: 图 14.1 显示了 else 块正在执行:
Figure 14.1 Result of failed TextAlignment conversion 图 14.1 文本对齐转换失败的结果
} else {
// Conversion failed
println("\(myRawValue) has no corresponding TextAlignment case") "100 has no corresponding TextAlignment case"
}
So far, you have been using Int as the type for your raw values. Swift allows a variety of types to be used, including all the built-in numeric types and String. Create a new enum that uses String as its raw value type. 到目前为止,您一直使用 Int 作为原始值的类型。Swift 允许使用多种类型,包括所有内置的数字类型和 String。创建一个新的枚举,使用 String 作为其原始值类型。
Listing 14.16 Creating an enum with strings 清单 14.16 创建一个带字符串的枚举
...
enum ProgrammingLanguage: String {
case Swift = "Swift"
case ObjectiveC = "Objective-C"
case C = "C"
case Cpp = "C++"
case Java = "Java"
}
let myFavoriteLanguage = ProgrammingLanguage.Swift print(“My favorite programming language is let myFavoriteLanguage = ProgrammingLanguage.Swift print(“我最喜欢的编程语言是
(myFavoriteLanguage.rawValue)”) (myFavoriteLanguage.rawValue)
You did not have to specify values when you first used a raw value of type Int - the compiler automatically set the first case to 0 , the second case to 1 , and so on. Here, you specified the corresponding raw String value for each case. This is not necessary: if you omit the raw value, Swift will use the name of the case itself! Modify ProgrammingLanguage to take out the raw values that match their case names. 您在第一次使用 Int 类型的原始值时不必指定值 - 编译器会自动将第一个案例设置为 0,第二个案例设置为 1,依此类推。在这里,您为每个案例指定了相应的原始字符串值。这不是必需的:如果您省略原始值,Swift 将使用案例本身的名称!修改 ProgrammingLanguage 以去掉与其案例名称匹配的原始值。
Listing 14.17 Using default string raw values 清单 14.17 使用默认字符串原始值
enum ProgrammingLanguage: String {
case Swift = "Swift"
case ObjectiveC = "Objective-C"
case C = "C"
case Cpp = "C++"
case Java = "Java"
}
let myFavoriteLanguage = ProgrammingLanguage. Swift let myFavoriteLanguage = 编程语言. Swift
print(“My favorite programming language is print("我最喜欢的编程语言是")
(myFavoriteLanguage.rawValue)”) (myFavoriteLanguage.rawValue)
Your declaration of devotion to Swift does not change. 你对 Swift 的忠诚宣言没有改变。
Methods 方法
A method is a function that is associated with a type. In some languages, methods can only be associated with classes (which we will discuss in Chapter 15). In Swift, methods can also be associated with enums. Create a new enum that represents the state of a lightbulb. 方法是与类型相关联的函数。在某些语言中,方法只能与类相关联(我们将在第 15 章讨论)。在 Swift 中,方法也可以与枚举相关联。创建一个新的枚举,表示灯泡的状态。
Listing 14.18 Lightbulbs can be on or off 清单 14.18 灯泡可以是开或关
enum Lightbulb {
case On
case Off
}
One of the things you might want to know is the temperature of the lightbulb. (For simplicity, assume that the bulb heats up immediately when it is turned on and cools off to the ambient temperature immediately when it is turned off.) Add a method for computing the surface temperature. 您可能想知道的一件事是灯泡的温度。(为简单起见,假设灯泡在打开时立即加热,并在关闭时立即冷却到环境温度。)添加一个计算表面温度的方法。
Listing 14.19 Establishing temperature behaviors 清单 14.19 建立温度行为
...
enum Lightbulb {
case On
case Off
func surfaceTemperatureForAmbientTemperature(ambient: Double) -> Double {
switch self {
case .On:
return ambient + 150.0
case .Off:
return ambient
}
}
}
Here, you add a function inside the definition of the Lightbulb enumeration. Because of the location of the definition of this function, it is now a method associated with the Lightbulb type. We would call it “a method on Lightbulb.” The function appears to take a single argument (ambient), but because it is a method, it also takes an implicit argument named self of type Lightbulb. All Swift methods have a self argument, which is used to access the instance on which the method is called - in this case, the instance of Lightbulb. 在这里,您在 Lightbulb 枚举的定义中添加了一个函数。由于该函数的定义位置,它现在是与 Lightbulb 类型相关联的方法。我们称之为“Lightbulb 上的方法”。该函数似乎接受一个参数(ambient),但由于它是一个方法,它还隐式接受一个名为 self 的参数,类型为 Lightbulb。所有 Swift 方法都有一个 self 参数,用于访问调用该方法的实例——在这种情况下,是 Lightbulb 的实例。
Create a variable to represent a lightbulb and call your new method. 创建一个变量来表示灯泡,并调用你的新方法。
Listing 14.20 Turning on the light 清单 14.20 打开灯光
enum Lightbulb {
case On
case Off
func surfaceTemperatureForAmbientTemperature(ambient: Double) -> Double {
switch self {
case .On:
return ambient + 150.0
case .Off:
return ambient
}
}
}
var bulb = Lightbulb.On
let ambientTemperature = 77.0
var bulbTemperature = bulb.surfaceTemperatureForAmbientTemperature(ambientTemperature)
print("the bulb's temperature is \(bulbTemperature)")
First you create bulb, an instance of the Lightbulb type. When you have an instance of the type, you can call methods on that instance using the syntax instance.methodName(parameters). You do exactly that here when you call bulb.surfaceTemperatureForAmbientTemperature(ambientTemperature). The bulb variable is an instance of Lightbulb, surfaceTemperatureForAmbientTemperature is the name of the method you are calling, and ambientTemperature is a parameter you pass in to the method. You store the result of the method call, a Double, in the bulbTemperature variable. Finally, you print a string with the bulb’s temperature to the console. 首先,您创建了一个 Lightbulb 类型的实例 bulb。当您拥有该类型的实例时,可以使用语法 instance.methodName(parameters) 在该实例上调用方法。您在这里正是这样做的,当您调用 bulb.surfaceTemperatureForAmbientTemperature(ambientTemperature) 时。bulb 变量是 Lightbulb 的一个实例,surfaceTemperatureForAmbientTemperature 是您正在调用的方法的名称,而 ambientTemperature 是您传递给该方法的参数。您将方法调用的结果(一个 Double)存储在 bulbTemperature 变量中。最后,您将包含灯泡温度的字符串打印到控制台。
Another method that seems like it might be useful is one that would toggle the lightbulb. To toggle the lightbulb, you need to modify self to change it from On to Off or Off to On. Try to add a toggle() method that takes no arguments and does not return anything. 另一个看起来可能有用的方法是切换灯泡。要切换灯泡,您需要修改 self 以将其从开切换到关或从关切换到开。尝试添加一个不带参数且不返回任何内容的 toggle() 方法。
Listing 14.21 Trying to toggle
enum Lightbulb {
case On
case Off
func surfaceTemperatureForAmbientTemperature(ambient: Double) -> Double {
switch self {
case .On:
return ambient + 150.0
case .Off:
return ambient
}
}
func toggle() {
switch self {
case .On:
self = .Off
case .Off:
self = .On
}
}
}
...
After typing this, you will get a compiler error that states that you cannot assign to self inside a method. In Swift, an enumeration is a value type, and methods on value types are not allowed to make changes to self (there will be more discussion of value types in Chapter 15). If you want to allow a method to change self, you need to mark it as a mutating method. Add this to your code. 在输入此内容后,您将收到一个编译器错误,指出您无法在方法内部对 self 进行赋值。在 Swift 中,枚举是值类型,值类型的方法不允许对 self 进行更改(第 15 章将对此进行更多讨论)。如果您希望允许方法更改 self,则需要将其标记为可变方法。将此添加到您的代码中。
Listing 14.22 Making toggle a mutating method 清单 14.22 使切换成为一个变异方法
mutating func toggle() {
switch self {
case .On:
self = .0ff
case .Off:
self = .On
}
}
Now you can toggle your lightbulb and see what the temperature is when the bulb is off. 现在您可以切换灯泡,查看灯泡关闭时的温度。
Listing 14.23 Turning off the light 清单 14.23 关闭灯光
var bulbTemperature = bulb.surfaceTemperatureForAmbientTemperature(ambientTemperature)
print("the bulb's temperature is \(bulbTemperature)")
bulb.toggle()
bulbTemperature = bulb.surfaceTemperatureForAmbientTemperature(ambientTemperature)
print("the bulb's temperature is \(bulbTemperature)")
Associated Values 相关值
Everything you have done so far with enumerations falls into the same general category of defining static cases that enumerate possible values or states. Swift also offers a much more powerful flavor of enumeration: cases with associated values. Associated values allow you to attach data to instances of an enumeration, and different cases can have different types of associated values. 到目前为止,您所做的所有枚举操作都属于定义静态情况的同一大类,这些情况列举了可能的值或状态。Swift 还提供了一种更强大的枚举类型:带有关联值的情况。关联值允许您将数据附加到枚举的实例上,并且不同的情况可以具有不同类型的关联值。
Create an enumeration that allows for tracking the dimensions of a couple of basic shapes. Each kind of shape has different types of properties. To represent a square, you need a single value (the length of one side). To represent a rectangle, you need two values: a width and a height. 创建一个枚举,以便跟踪几种基本形状的维度。每种形状具有不同类型的属性。要表示一个正方形,您需要一个值(一个边的长度)。要表示一个矩形,您需要两个值:宽度和高度。
Listing 14.24 Setting up ShapeDimensions 清单 14.24 设置形状维度
...
enum ShapeDimensions {
// Square's associated value is the length of one side
case Square(Double)
// Rectangle's associated value defines its width and height
case Rectangle(width: Double, height: Double)
}
You have defined a new enumeration type, ShapeDimensions, with two cases. The Square case has an associated value of type Double. The Rectangle case has an associated value with the type (width:Double, height:Double), a named tuple (first seen in Chapter 12). 您定义了一个新的枚举类型 ShapeDimensions,包含两个案例。Square 案例具有一个类型为 Double 的关联值。Rectangle 案例具有一个类型为 (width:Double, height:Double) 的关联值,这是一个命名元组(在第 12 章首次出现)。
To create instances of ShapeDimensions, you must specify both the case and an appropriate associated value for the case. 要创建 ShapeDimensions 的实例,您必须同时指定案例和该案例的适当关联值。
Listing 14.25 Creating shapes 清单 14.25 创建形状
enum ShapeDimensions {
// Square's associated value is the length of one side
case Square(Double)
// Rectangle's associated value defines its width and height
case Rectangle(width: Double, height: Double)
}
var squareShape = ShapeDimensions.Square(10.0)
var rectShape = ShapeDimensions.Rectangle(width: 5.0, height: 10.0)
Here, you create a square with sides 10 units long and a rectangle that is 5 units by 10 units. 在这里,您创建一个边长为 10 单位的正方形和一个长 5 单位、宽 10 单位的矩形。
You can use a switch statement to unpack an associated value and make use of it. Add a method to ShapeDimensions that computes the area of a shape. 您可以使用 switch 语句来解包关联值并加以利用。向 ShapeDimensions 添加一个方法,用于计算形状的面积。
Listing 14.26 Using associated values to compute area 清单 14.26 使用关联值计算面积
...
enum ShapeDimensions {
// Square's associated value is the length of one side
case Square(Double)
// Rectangle's associated value defines its width and height
case Rectangle(width: Double, height: Double)
func area() -> Double {
switch self {
case let .Square(side):
return side * side
case let .Rectangle(width: w, height: h):
return w * h
}
}
}
...
In your implementation of area(), you switch on self just as you did earlier in the chapter. Here, the switch cases use Swift’s pattern matching to bind self’s associated value with a new variable (or variables). 在你对 area() 的实现中,你像本章早些时候那样切换 self。在这里,switch 案例使用 Swift 的模式匹配将 self 的关联值绑定到一个新变量(或多个变量)。
Call the area() method on the instances you created earlier to see it in action. 在您之前创建的实例上调用 area() 方法以查看其效果。
Listing 14.27 Computing areas 清单 14.27 计算面积
...
var squareShape = ShapeDimensions.Square(10.0)
var rectShape = ShapeDimensions.Rectangle(width: 5.0, height: 10.0)
print("square's area = \(squareShape.area())")
print("rectangle's area = \(rectShape.area())")
Not all enum cases have to have associated values. For example, you could add a Point case. 并不是所有的枚举案例都必须具有关联值。例如,您可以添加一个点(Point)案例。
Geometric points do not have any dimensions. To add a Point, leave off the associated value type. 几何点没有任何维度。要添加一个点,请省略相关的值类型。
Add a Point and update the area() method to include its area. 添加一个点并更新 area() 方法以包含它的面积。
Listing 14.28 Setting up a Point 清单 14.28 设置一个点
...
enum ShapeDimensions {
// Point has no associated value - it is dimensionless
case Point
// Square's associated value is the length of one side
case Square(Double)
// Rectangle's associated value defines its width and height
case Rectangle(width: Double, height: Double)
func area() -> Double {
switch self {
case .Point:
return 0
case let .Square(side):
return side * side
case let .Rectangle(width: w, height: h):
return w * h
}
}
}
...
Now, create an instance of a point and confirm that area() works as expected. 现在,创建一个点的实例,并确认 area() 按预期工作。
Listing 14.29 What is the area of a point? 列表 14.29 一个点的面积是多少?
...
var squareShape = ShapeDimensions.Square(10.0)
var rectShape = ShapeDimensions.Rectangle(width: 5.0, height: 10.0)
var pointShape = ShapeDimensions.Point
print("square's area = \(squareShape.area())")
print("rectangle's area = \(rectShape.area())")
print("point's area = \(pointShape.area())")
Recursive Enumerations 递归枚举
You now know how to attach associated values to enum cases. This brings up a curious question. Can you attach an associated value of an enum’s own type to one of its cases? (Perhaps this question brings up another: why would you want to?) 您现在知道如何将关联值附加到枚举案例。这引出了一个有趣的问题。您能否将枚举自身类型的关联值附加到其某个案例?(也许这个问题引出了另一个问题:您为什么想这样做?)
A data structure that comes up frequently in computer science is a tree. Most hierarchical data can naturally be represented as a tree. Think of a family tree: it contains people (the “nodes” of the tree) and ancestral relationships (the “edges” of the tree). The family tree branching stops when you reach an ancestor you do not know, as in Figure 14.2. 在计算机科学中,常见的一种数据结构是树。大多数层次数据可以自然地表示为树。想想家谱:它包含人(树的“节点”)和祖先关系(树的“边”)。当你到达一个你不知道的祖先时,家谱的分支就停止了,如图 14.2 所示。
Figure 14.2 A family tree 图 14.2 家谱
Modeling a family tree can be difficult because for any given person, you may know zero, one, or both of their parents. If you know one or both parents, you would like to keep track of their ancestors as well. Try to create an enum that will let you build up as much of your family tree as you know. 建模家谱可能很困难,因为对于任何给定的人,你可能只知道零、一个或两个父母。如果你知道一个或两个父母,你也希望能够追踪他们的祖先。尝试创建一个枚举,以便让你尽可能多地构建你所知道的家谱。
Listing 14.30 Incorrect attempt at FamilyTree 清单 14.30 家谱的错误尝试
...
enum FamilyTree {
case NoKnownParents
case OneKnownParent(name: String, ancestors: FamilyTree)
case TwoKnownParents(fatherName: String, fatherAncestors: FamilyTree,
motherName: String, motherAncestors: FamilyTree)
}
Once you have typed this in, Xcode gives you an error that contains a suggested fix: “Recursive enum ‘FamilyTree’ is not marked ‘indirect’.” FamilyTree is “recursive” because its cases have an associated value that is also of type FamilyTree. Why does the language care if an enum is recursive, though? 一旦你输入了这个,Xcode 会给你一个包含建议修复的错误:“递归枚举‘FamilyTree’没有标记为‘间接’。” FamilyTree 是“递归的”,因为它的案例有一个与之相关的值,该值也是 FamilyTree 类型。然而,为什么语言会关心枚举是否是递归的呢?
The answer to that question requires a little bit of understanding of how enumerations work under the hood. The Swift compiler has to know how much memory every instance of every type in your program will occupy. You do not (usually) have to worry about this, as the compiler figures it all out for you when it builds your program. Enumerations, however, are a little tricky. 回答这个问题需要对枚举的底层工作原理有一点了解。Swift 编译器必须知道程序中每种类型的每个实例将占用多少内存。通常你不需要担心这个,因为编译器在构建程序时会为你解决所有问题。然而,枚举有点棘手。
The compiler knows that any instance of an enum will only ever be in one case at a time, although it may change cases as your program runs. Therefore, when the compiler is deciding how much memory an instance of enum requires, it will look at each case and figure out which case requires the most memory. The enum will require that much memory (plus a little bit more that the compiler will use to keep track of which case is currently assigned). 编译器知道,枚举的任何实例在任何时候只会处于一个状态,尽管它可能会在程序运行时更改状态。因此,当编译器决定一个枚举实例需要多少内存时,它会查看每个状态并计算出哪个状态需要最多的内存。枚举将需要那么多内存(加上编译器用来跟踪当前分配的状态的额外一点内存)。
Look back at your ShapeDimensions enum. The . Point case has no associated data, so it requires no extra memory. The . Square case has an associated Double, so it requires one Double’s worth of memory ( 8 bytes). The . Rectangle case has two associated Doubles, so it requires 16 bytes of memory. The actual size of an instance of ShapeDimensions is 17 bytes: enough room to store . Rectangle, if necessary, plus one byte to keep track of which case the instance actually is. 回顾一下你的 ShapeDimensions 枚举。 .Point 情况没有关联数据,因此不需要额外的内存。 .Square 情况有一个关联的 Double,因此需要一个 Double 的内存(8 字节)。 .Rectangle 情况有两个关联的 Doubles,因此需要 16 字节的内存。 ShapeDimensions 实例的实际大小是 17 字节:足够存储 .Rectangle(如果需要),加上一个字节来跟踪实例实际上是哪种情况。
Now consider your FamilyTree enum. How much memory is required for the .OneKnownParent case? Enough memory for a String plus enough memory for an instance of FamilyTree. See the problem? The compiler cannot determine how big a FamilyTree is without knowing how big a FamilyTree is. Looking at it another way, FamilyTree would require an infinite amount of memory! 现在考虑你的 FamilyTree 枚举。对于 .OneKnownParent 情况,需要多少内存?足够存储一个字符串的内存加上一个 FamilyTree 实例所需的内存。明白问题了吗?编译器无法在不知道 FamilyTree 大小的情况下确定 FamilyTree 的大小。从另一个角度来看,FamilyTree 将需要无限的内存!
To solve this issue, Swift can introduce a layer of indirection. Instead of deciding how much memory .OneKnownParent will require (which would lead back into infinite recursion), you can use the keyword indirect to instruct the compiler to instead store the enum’s data behind a pointer. We do not discuss pointers much in this book because Swift does not make you deal with them. Even in this case, you do not have to do anything except opt in to making FamilyTree use pointers under the hood. Do that now, and you are well on your way to modeling a family tree. 为了解决这个问题,Swift 可以引入一个间接层。您可以使用关键字 indirect 来指示编译器将枚举的数据存储在指针后面,而不是决定.OneKnownParent 将需要多少内存(这将导致无限递归)。在本书中,我们不多讨论指针,因为 Swift 不要求您处理它们。即使在这种情况下,您也不需要做任何事情,只需选择让 FamilyTree 在底层使用指针。现在这样做,您就可以很好地开始建模家谱。
...
indirect enum FamilyTree {
case NoKnownParents
case OneKnownParent(name: String, ancestors: FamilyTree)
case TwoKnownParents(fatherName: String, fatherAncestors: FamilyTree,
motherName: String, motherAncestors: FamilyTree)
}
How does using a pointer solve the “infinite memory” problem? The compiler now knows to store a pointer to the associated data, putting the data somewhere else in memory rather than making the instance of FamilyTree big enough to hold the data. The size of an instance of FamilyTree is now 8 bytes on a 64-bit architecture - the size of one pointer. 使用指针如何解决“无限内存”问题?编译器现在知道要存储指向相关数据的指针,将数据放在内存中的其他地方,而不是让 FamilyTree 的实例足够大以容纳数据。在 64 位架构上,FamilyTree 实例的大小现在是 8 字节——一个指针的大小。