Merge remote-tracking branch 'upstream/gh-pages' into gh-pages
This commit is contained in:
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [SketchK, numbbbbb]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@ -28,6 +28,7 @@
|
||||
* [扩展](chapter2/20_Extensions.md)
|
||||
* [协议](chapter2/21_Protocols.md)
|
||||
* [泛型](chapter2/22_Generics.md)
|
||||
* [不透明类型](chapter2/27_Opaque_Types.md)
|
||||
* [自动引用计数](chapter2/23_Automatic_Reference_Counting.md)
|
||||
* [内存安全](chapter2/24_Memory_Safety.md)
|
||||
* [访问控制](chapter2/25_Access_Control.md)
|
||||
|
||||
@ -65,7 +65,7 @@ let fruitSummary = "I have \(apples + oranges) pieces of fruit."
|
||||
>
|
||||
> 使用 `\()` 来把一个浮点计算转换成字符串,并加上某人的名字,和他打个招呼。
|
||||
|
||||
使用一对三个单引号(`"""`)来包含多行字符串内容,字符串中的内容(包括引号、空格、换行符等)都会保留下来。举个例子:
|
||||
使用三个双引号(`"""`)来包含多行字符串内容,字符串中的内容(包括引号、空格、换行符等)都会保留下来。举个例子:
|
||||
|
||||
```swift
|
||||
let quotation = """
|
||||
|
||||
@ -481,7 +481,7 @@ print("The status message is \(http200Status.description)")
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 元组在临时组织值的时候很有用,但是并不适合创建复杂的数据结构。如果你的数据结构并不是临时使用,请使用类或者结构体而不是元组。请参考 [类和结构体](./09_Classes_and_Structures.md)。
|
||||
> 当遇到一些相关值的简单分组时,元组是很有用的。元组不适合用来创建复杂的数据结构。如果你的数据结构比较复杂,不要使用元组,用类或结构体去建模。欲获得更多信息,请参考 [结构体和类](./09_Classes_and_Structures.md)。
|
||||
|
||||
## 可选类型 {#optionals}
|
||||
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
# 集合类型
|
||||
|
||||
Swift 语言提供 `Arrays`、`Sets` 和 `Dictionaries` 三种基本的*集合类型*用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。
|
||||
Swift 语言提供数组(Array)、集合(Set)和字典(Dictionary)三种基本的*集合类型*用来存储集合数据。数组是有序数据的集。集合是无序无重复数据的集。字典是无序的键值对的集。
|
||||
|
||||

|
||||
|
||||
Swift 语言中的 `Arrays`、`Sets` 和 `Dictionaries` 中存储的数据值类型必须明确。这意味着我们不能把错误的数据类型插入其中。同时这也说明你完全可以对取回值的类型非常放心。
|
||||
Swift 中的数组、集合和字典必须明确其中保存的键和值类型,这样就可以避免插入一个错误数据类型的值。同理,对于获取到的值你也可以放心,其数据类型是确定的。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `Arrays`、`Sets` 和 `Dictionaries` 类型被实现为*泛型集合*。更多关于泛型类型和集合,参见 [泛型](./23_Generics.md) 章节。
|
||||
> Swift 的数组、集合和字典类型被实现为*泛型集合*。更多关于泛型类型和集合,参见 [泛型](./22_Generics.md) 章节。
|
||||
|
||||
## 集合的可变性 {#mutability-of-collections}
|
||||
|
||||
如果创建一个 `Arrays`、`Sets` 或 `Dictionaries` 并且把它分配成一个变量,这个集合将会是*可变的*。这意味着你可以在创建之后添加更多或移除已存在的数据项,或者改变集合中的数据项。如果我们把 `Arrays`、`Sets` 或 `Dictionaries` 分配成常量,那么它就是*不可变的*,它的大小和内容都不能被改变。
|
||||
如果创建一个数组、集合或字典并且把它分配成一个变量,这个集合将会是*可变的*。这意味着可以在创建之后添加、修改或者删除数据项。如果把数组、集合或字典分配成常量,那么它就是*不可变的*,它的大小和内容都不能被改变。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 在我们不需要改变集合的时候创建不可变集合是很好的实践。如此 Swift 编译器可以优化我们创建的集合。
|
||||
> 在不需要改变集合的时候创建不可变集合是很好的实践。这样做便于你理解自己的代码,也能让 Swift 编译器优化集合的性能。
|
||||
|
||||
## 数组(Arrays) {#arrays}
|
||||
|
||||
@ -24,15 +24,17 @@ Swift 语言中的 `Arrays`、`Sets` 和 `Dictionaries` 中存储的数据值类
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `Array` 类型被桥接到 `Foundation` 中的 `NSArray` 类。更多关于在 `Foundation` 和 `Cocoa` 中使用 `Array` 的信息,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中 [使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6) 部分。
|
||||
> Swift 的 `Array` 类型被桥接到 Foundation 中的 `NSArray` 类。
|
||||
>
|
||||
> 更多关于在 Foundation 和 Cocoa 中使用 `Array` 的信息,参见 [Bridging Between Array and NSArray](https://developer.apple.com/documentation/swift/array#2846730)。
|
||||
|
||||
### 数组的简单语法 {#array-type-shorthand-syntax}
|
||||
|
||||
写 Swift 数组应该遵循像 `Array<Element>` 这样的形式,其中 `Element` 是这个数组中唯一允许存在的数据类型。我们也可以使用像 `[Element]` 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。
|
||||
Swift 中数组的完整写法为 `Array<Element>`,其中 `Element` 是这个数组中唯一允许存在的数据类型。也可以使用像 `[Element]` 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。
|
||||
|
||||
### 创建一个空数组 {#creating-an-empty-array}
|
||||
|
||||
我们可以使用构造语法来创建一个由特定数据类型构成的空数组:
|
||||
你可以使用构造语法来创建一个由特定数据类型构成的空数组:
|
||||
|
||||
```swift
|
||||
var someInts = [Int]()
|
||||
@ -42,7 +44,7 @@ print("someInts is of type [Int] with \(someInts.count) items.")
|
||||
|
||||
注意,通过构造函数的类型,`someInts` 的值类型被推断为 `[Int]`。
|
||||
|
||||
或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:`[]`(一对空方括号):
|
||||
或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,你可以使用空数组语句创建一个空数组,它的写法很简单:`[]`(一对空方括号):
|
||||
|
||||
```swift
|
||||
someInts.append(3)
|
||||
@ -53,7 +55,7 @@ someInts = []
|
||||
|
||||
### 创建一个带有默认值的数组 {#creating-an-array-with-a-default-value}
|
||||
|
||||
Swift 中的 `Array` 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(`count`)和适当类型的初始值(`repeating`)传入数组构造函数:
|
||||
Swift 中的 `Array` 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。可以把准备加入新数组的数据项数量(`count`)和适当类型的初始值(`repeating`)传入数组构造函数:
|
||||
|
||||
```swift
|
||||
var threeDoubles = Array(repeating: 0.0, count: 3)
|
||||
@ -62,7 +64,7 @@ var threeDoubles = Array(repeating: 0.0, count: 3)
|
||||
|
||||
### 通过两个数组相加创建一个数组 {#creating-an-array-by-adding-two-arrays-together}
|
||||
|
||||
我们可以使用加法操作符(`+`)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:
|
||||
你可以使用加法操作符(`+`)来组合两个已存在的相同类型数组。新数组的数据类型会从两个数组的数据类型中推断出来:
|
||||
|
||||
```swift
|
||||
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
|
||||
@ -74,7 +76,7 @@ var sixDoubles = threeDoubles + anotherThreeDoubles
|
||||
|
||||
### 用数组字面量构造数组 {#creating-an-array-with-an-array-literals}
|
||||
|
||||
我们可以使用*数组字面量*来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:
|
||||
你可以使用*数组字面量*来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:
|
||||
|
||||
`[value 1, value 2, value 3]`。
|
||||
|
||||
@ -85,15 +87,15 @@ var shoppingList: [String] = ["Eggs", "Milk"]
|
||||
// shoppingList 已经被构造并且拥有两个初始项。
|
||||
```
|
||||
|
||||
`shoppingList` 变量被声明为“字符串值类型的数组“,记作 `[String]`。 因为这个数组被规定只有 `String` 一种数据结构,所以只有 `String` 类型可以在其中被存取。 在这里,`shoppingList` 数组由两个 `String` 值(`"Eggs"` 和 `"Milk"`)构造,并且由数组字面量定义。
|
||||
`shoppingList` 变量被声明为“字符串值类型的数组“,记作 `[String]`。因为这个数组被规定只有 `String` 一种数据结构,所以只有 `String` 类型可以在其中被存取。在这里,`shoppingList` 数组由两个 `String` 值(`"Eggs"` 和 `"Milk"`)构造,并且由数组字面量定义。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `shoppingList` 数组被声明为变量(`var` 关键字创建)而不是常量(`let` 创建)是因为以后可能会有更多的数据项被插入其中。
|
||||
> `shoppingList` 数组被声明为变量(`var` 关键字创建)而不是常量(`let` 创建)是因为之后会有更多的数据项被插入其中。
|
||||
|
||||
在这个例子中,字面量仅仅包含两个 `String` 值。匹配了该数组的变量声明(只能包含 `String` 的数组),所以这个字面量的分配过程可以作为用两个初始项来构造 `shoppingList` 的一种方式。
|
||||
在这个例子中,字面量仅仅包含两个 `String` 值。匹配了该数组的声明(只能包含 `String` 的数组),所以可以将这个字面量的赋值过程看作用两个初始项来构造 `shoppingList` 的一种方式。
|
||||
|
||||
由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。`shoppingList` 的构造也可以这样写:
|
||||
由于 Swift 的类型推断机制,当你用字面量构造拥有相同类型值数组的时候,不必把数组的类型定义清楚。`shoppingList` 的构造也可以这样写:
|
||||
|
||||
```swift
|
||||
var shoppingList = ["Eggs", "Milk"]
|
||||
@ -103,7 +105,7 @@ var shoppingList = ["Eggs", "Milk"]
|
||||
|
||||
### 访问和修改数组 {#accessing-and-modifying-an-array}
|
||||
|
||||
我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。
|
||||
你可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。
|
||||
|
||||
可以使用数组的只读属性 `count` 来获取数组中的数据项数量:
|
||||
|
||||
@ -127,10 +129,10 @@ if shoppingList.isEmpty {
|
||||
|
||||
```swift
|
||||
shoppingList.append("Flour")
|
||||
// shoppingList 现在有3个数据项,有人在摊煎饼
|
||||
// shoppingList 现在有3个数据项,似乎有人在摊煎饼
|
||||
```
|
||||
|
||||
除此之外,使用加法赋值运算符(`+=`)也可以直接在数组后面添加一个或多个拥有相同类型的数据项:
|
||||
除此之外,也可以使用加法赋值运算符(`+=`)直接将另一个相同类型数组中的数据添加到该数组后面:
|
||||
|
||||
```swift
|
||||
shoppingList += ["Baking Powder"]
|
||||
@ -139,7 +141,7 @@ shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
|
||||
// shoppingList 现在有七项了
|
||||
```
|
||||
|
||||
可以直接使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值放在直接放在数组名称的方括号中:
|
||||
可以直接使用*下标语法*来获取数组中的数据项,把所需要数据项的索引值直接放在数组名称之后的方括号中:
|
||||
|
||||
```swift
|
||||
var firstItem = shoppingList[0]
|
||||
@ -150,13 +152,15 @@ var firstItem = shoppingList[0]
|
||||
>
|
||||
> 第一项在数组中的索引值是 `0` 而不是 `1`。 Swift 中的数组索引总是从零开始。
|
||||
|
||||
我们也可以用下标来改变某个已有索引值对应的数据值:
|
||||
你也可以用下标来改变某个有效索引值对应的数据值:
|
||||
|
||||
```swift
|
||||
shoppingList[0] = "Six eggs"
|
||||
// 其中的第一项现在是“Six eggs”而不是“Eggs”
|
||||
```
|
||||
|
||||
当你使用下标语法,所使用的下标必须是有效的。例如,试图通过 `shoppingList[shoppingList.count] = "Salt"` 在数组的最后添加一项,将产生一个运行时错误。
|
||||
|
||||
还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 `"Chocolate Spread"`、`"Cheese"` 和 `"Butter"` 替换为 `"Bananas"` 和 `"Apples"`:
|
||||
|
||||
```swift
|
||||
@ -164,11 +168,7 @@ shoppingList[4...6] = ["Bananas", "Apples"]
|
||||
// shoppingList 现在有6项
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 不可以用下标访问的形式去在数组尾部添加新项。
|
||||
|
||||
调用数组的 `insert(_:at:)` 方法来在某个具体索引值之前添加数据项:
|
||||
通过调用数组的 `insert(_:at:)` 方法在某个指定索引值之前添加数据项:
|
||||
|
||||
```swift
|
||||
shoppingList.insert("Maple Syrup", at: 0)
|
||||
@ -178,18 +178,18 @@ shoppingList.insert("Maple Syrup", at: 0)
|
||||
|
||||
这次 `insert(_:at:)` 方法调用把值为 `"Maple Syrup"` 的新数据项插入列表的最开始位置,并且使用 `0` 作为索引值。
|
||||
|
||||
类似的我们可以使用 `remove(at:)` 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它):
|
||||
类似的可以使用 `remove(at:)` 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(不需要的时候就可以无视它):
|
||||
|
||||
```swift
|
||||
let mapleSyrup = shoppingList.remove(at: 0)
|
||||
// 索引值为0的数据项被移除
|
||||
// shoppingList 现在只有6项,而且不包括 Maple Syrup
|
||||
// mapleSyrup 常量的值等于被移除数据项“Maple Syrup”的值
|
||||
// mapleSyrup 常量的值等于被移除数据项“Maple Syrup”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的 `count` 属性进行比较来在使用某个索引之前先检验是否有效。除了当 `count` 等于 0 时(说明这是个空数组),最大索引值一直是 `count - 1`,因为数组都是零起索引。
|
||||
> 如果你试图通过越界索引来执行访问或者修改数据的操作,会引发一个运行时错误。此时可以使用索引值和数组的 `count` 属性进行比较来在使用该索引之前检验其是否有效。除了当 `count` 等于 0 时(说明这是个空数组),最大索引值一直是 `count - 1`,因为数组都是零起索引。
|
||||
|
||||
数据项被移除后数组中的空出项会被自动填补,所以现在索引值为 `0` 的数据项的值再次等于 `"Six eggs"`:
|
||||
|
||||
@ -198,18 +198,18 @@ firstItem = shoppingList[0]
|
||||
// firstItem 现在等于“Six eggs”
|
||||
```
|
||||
|
||||
如果我们只想把数组中的最后一项移除,可以使用 `removeLast()` 方法而不是 `remove(at:)` 方法来避免我们需要获取数组的 `count` 属性。就像后者一样,前者也会返回被移除的数据项:
|
||||
如果你只想把数组中的最后一项移除,可以使用 `removeLast()` 方法而不是 `remove(at:)` 方法来避免需要获取数组的 `count` 属性。就像后者一样,前者也会返回被移除的数据项:
|
||||
|
||||
```swift
|
||||
let apples = shoppingList.removeLast()
|
||||
// 数组的最后一项被移除了
|
||||
// shoppingList 现在只有5项,不包括 Apples
|
||||
// apples 常量的值现在等于“Apples”字符串
|
||||
// apples 常量的值现在等于字符串“Apples”
|
||||
```
|
||||
|
||||
### 数组的遍历 {#iterating-over-an-array}
|
||||
|
||||
我们可以使用 `for-in` 循环来遍历所有数组中的数据项:
|
||||
你可以使用 `for-in` 循环来遍历数组中所有的数据项:
|
||||
|
||||
```swift
|
||||
for item in shoppingList {
|
||||
@ -222,7 +222,7 @@ for item in shoppingList {
|
||||
// Bananas
|
||||
```
|
||||
|
||||
如果我们同时需要每个数据项的值和索引值,可以使用 `enumerated()` 方法来进行数组遍历。`enumerated()` 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:
|
||||
如果同时需要每个数据项的值和索引值,可以使用 `enumerated()` 方法来进行数组遍历。`enumerated()` 返回一个由索引值和数据值组成的元组数组。索引值从零开始,并且每次增加一;如果枚举一整个数组,索引值将会和数据值一一匹配。你可以把这个元组分解成临时常量或者变量来进行遍历:
|
||||
|
||||
```swift
|
||||
for (index, value) in shoppingList.enumerated() {
|
||||
@ -239,34 +239,34 @@ for (index, value) in shoppingList.enumerated() {
|
||||
|
||||
## 集合(Sets) {#sets}
|
||||
|
||||
*集合(Set)*用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。
|
||||
*集合*用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。
|
||||
|
||||
> 注意
|
||||
> Swift 的 `Set` 类型被桥接到 `Foundation` 中的 `NSSet` 类。
|
||||
> Swift 的 `Set` 类型被桥接到 Foundation 中的 `NSSet` 类。
|
||||
>
|
||||
> 关于使用 `Foundation` 和 `Cocoa` 中 `Set` 的知识,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中[使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6)部分。
|
||||
> 关于使用 Foundation 和 Cocoa 中 `Set` 的知识,参见 [Bridging Between Set and NSSet](https://developer.apple.com/documentation/swift/set#2845530)
|
||||
|
||||
### 集合类型的哈希值 {#hash-values-for-set-types}
|
||||
|
||||
一个类型为了存储在集合中,该类型必须是*可哈希化*的——也就是说,该类型必须提供一个方法来计算它的*哈希值*。一个哈希值是 `Int` 类型的,相等的对象哈希值必须相同,比如 `a==b`,因此必须 `a.hashValue == b.hashValue`。
|
||||
一个类型为了存储在集合中,该类型必须是*可哈希化*的——也就是说,该类型必须提供一个方法来计算它的*哈希值*。一个哈希值是 `Int` 类型的,相等的对象哈希值必须相同,比如 `a == b`,因此必须 `a.hashValue == b.hashValue`。
|
||||
|
||||
Swift 的所有基本类型(比如 `String`、`Int`、`Double` 和 `Bool`)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有关联值的枚举成员值(在 [枚举](./08_Enumerations.md) 有讲述)默认也是可哈希化的。
|
||||
Swift 的所有基本类型(比如 `String`、`Int`、`Double` 和 `Bool`)默认都是可哈希化的,可以作为集合值的类型或者字典键的类型。没有关联值的枚举成员值(在 [枚举](./08_Enumerations.md) 有讲述)默认也是可哈希化的。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型遵循 Swift 标准库中的 `Hashable` 协议。遵循 `Hashable` 协议的类型需要提供一个类型为 `Int` 的可读属性 `hashValue`。由类型的 `hashValue` 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。
|
||||
> 你可以使用自定义的类型作为集合值的类型或者是字典键的类型,但需要使自定义类型遵循 Swift 标准库中的 `Hashable` 协议。遵循 `Hashable` 协议的类型需要提供一个类型为 `Int` 的可读属性 `hashValue`。由类型的 `hashValue` 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。
|
||||
>
|
||||
> 因为 `Hashable` 协议遵循 `Equatable` 协议,所以遵循该协议的类型也必须提供一个“是否相等”运算符(`==`)的实现。这个 `Equatable` 协议要求任何遵循 `==` 实现的实例间都是一种相等的关系。也就是说,对于 `a,b,c` 三个值来说,`==` 的实现必须满足下面三种情况:
|
||||
|
||||
> * `a == a`(自反性)
|
||||
> * `a == b` 意味着 `b == a`(对称性)
|
||||
> * `a == b && b == c` 意味着 `a == c`(传递性)
|
||||
|
||||
关于遵循协议的更多信息,请看 [协议](./22_Protocols.md)。
|
||||
>
|
||||
> 关于遵循协议的更多信息,请看 [协议](./21_Protocols.md)。
|
||||
|
||||
### 集合类型语法 {#set-type-syntax}
|
||||
|
||||
Swift 中的 `Set` 类型被写为 `Set<Element>`,这里的 `Element` 表示 `Set` 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。
|
||||
Swift 中的集合类型被写为 `Set<Element>`,这里的 `Element` 表示集合中允许存储的类型。和数组不同的是,集合没有等价的简化形式。
|
||||
|
||||
### 创建和构造一个空的集合 {#creating-and-initalizing-an-empty-set}
|
||||
|
||||
@ -280,9 +280,9 @@ print("letters is of type Set<Character> with \(letters.count) items.")
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 通过构造器,这里的 `letters` 变量的类型被推断为 `Set<Character>`。
|
||||
> 通过构造器,这里 `letters` 变量的类型被推断为 `Set<Character>`。
|
||||
|
||||
此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,我们可以通过一个空的数组字面量创建一个空的 `Set`:
|
||||
此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,你可以通过一个空的数组字面量创建一个空的集合:
|
||||
|
||||
```swift
|
||||
letters.insert("a")
|
||||
@ -293,7 +293,7 @@ letters = []
|
||||
|
||||
### 用数组字面量创建集合 {#creating-a-set-with-an-array-literal}
|
||||
|
||||
你可以使用数组字面量来构造集合,并且可以使用简化形式写一个或者多个值作为集合元素。
|
||||
你可以使用数组字面量来构造集合,相当于一种简化的形式将一个或者多个值作为集合元素。
|
||||
|
||||
下面的例子创建一个称之为 `favoriteGenres` 的集合来存储 `String` 类型的值:
|
||||
|
||||
@ -302,13 +302,13 @@ var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
|
||||
// favoriteGenres 被构造成含有三个初始值的集合
|
||||
```
|
||||
|
||||
这个 `favoriteGenres` 变量被声明为“一个 `String` 值的集合”,写为 `Set<String>`。由于这个特定的集合含有指定 `String` 类型的值,所以它只允许存储 `String` 类型值。这里的 `favoriteGenres` 变量有三个 `String` 类型的初始值(`"Rock"`,`"Classical"` 和 `"Hip hop"`),并以数组字面量的方式出现。
|
||||
这个 `favoriteGenres` 变量被声明为“一个 `String` 值的集合”,写为 `Set<String>`。由于这个特定集合指定了值为 `String` 类型,所以它*只*允许存储 `String` 类型值。这里的 `favoriteGenres` 变量有三个 `String` 类型的初始值(`"Rock"`,`"Classical"` 和 `"Hip hop"`),以数组字面量的形式书写。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `favoriteGenres` 被声明为一个变量(拥有 `var` 标示符)而不是一个常量(拥有 `let` 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。
|
||||
> `favoriteGenres` 被声明为一个变量(拥有 `var` 标示符)而不是一个常量(拥有 `let` 标示符),因为它里面的元素将会在之后的例子中被增加或者移除。
|
||||
|
||||
一个 `Set` 类型不能从数组字面量中被单独推断出来,因此 `Set` 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 `Set` 并且该数组字面量中的所有元素类型相同,那么你无须写出 `Set` 的具体类型。`favoriteGenres` 的构造形式可以采用简化的方式代替:
|
||||
一个集合类型不能从数组字面量中被直接推断出来,因此 `Set` 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个集合并且与该数组字面量中的所有元素类型相同,那么无须写出集合的具体类型。`favoriteGenres` 的构造形式可以采用简化的方式代替:
|
||||
|
||||
```swift
|
||||
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
|
||||
@ -318,9 +318,9 @@ var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
|
||||
|
||||
### 访问和修改一个集合 {#accesing-and-modifying-a-set}
|
||||
|
||||
你可以通过 `Set` 的属性和方法来访问和修改一个 `Set`。
|
||||
你可以通过集合的属性和方法来对其进行访问和修改。
|
||||
|
||||
为了找出一个 `Set` 中元素的数量,可以使用其只读属性 `count`:
|
||||
为了获取一个集合中元素的数量,可以使用其只读属性 `count`:
|
||||
|
||||
```swift
|
||||
print("I have \(favoriteGenres.count) favorite music genres.")
|
||||
@ -338,14 +338,14 @@ if favoriteGenres.isEmpty {
|
||||
// 打印“I have particular music preferences.”
|
||||
```
|
||||
|
||||
你可以通过调用 `Set` 的 `insert(_:)` 方法来添加一个新元素:
|
||||
你可以通过调用集合的 `insert(_:)` 方法来添加一个新元素:
|
||||
|
||||
```swift
|
||||
favoriteGenres.insert("Jazz")
|
||||
// favoriteGenres 现在包含4个元素
|
||||
```
|
||||
|
||||
你可以通过调用 `Set` 的 `remove(_:)` 方法去删除一个元素,如果该值是该 `Set` 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 `Set` 不包含该值,则返回 `nil`。另外,`Set` 中的所有元素可以通过它的 `removeAll()` 方法删除。
|
||||
你可以通过调用集合的 `remove(_:)` 方法去删除一个元素,如果它是该集合的一个元素则删除它并且返回它的值,若该集合不包含它,则返回 `nil`。另外,集合可以通过 `removeAll()` 方法删除所有元素。
|
||||
|
||||
```swift
|
||||
if let removedGenre = favoriteGenres.remove("Rock") {
|
||||
@ -356,7 +356,7 @@ if let removedGenre = favoriteGenres.remove("Rock") {
|
||||
// 打印“Rock? I'm over it.”
|
||||
```
|
||||
|
||||
使用 `contains(_:)` 方法去检查 `Set` 中是否包含一个特定的值:
|
||||
使用 `contains(_:)` 方法去检查集合中是否包含一个特定的值:
|
||||
|
||||
```swift
|
||||
if favoriteGenres.contains("Funk") {
|
||||
@ -369,7 +369,7 @@ if favoriteGenres.contains("Funk") {
|
||||
|
||||
### 遍历一个集合 {#iterating-over-a-set}
|
||||
|
||||
你可以在一个 `for-in` 循环中遍历一个 `Set` 中的所有值。
|
||||
你可以在一个 `for-in` 循环中遍历一个集合中的所有值。
|
||||
|
||||
```swift
|
||||
for genre in favoriteGenres {
|
||||
@ -382,7 +382,7 @@ for genre in favoriteGenres {
|
||||
|
||||
更多关于 `for-in` 循环的信息,参见 [For 循环](./05_Control_Flow.md#for_loops)。
|
||||
|
||||
Swift 的 `Set` 类型没有确定的顺序,为了按照特定顺序来遍历一个 `Set` 中的值可以使用 `sorted()` 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符'<'对元素进行比较的结果来确定。
|
||||
Swift 的 `Set` 类型没有确定的顺序,为了按照特定顺序来遍历一个集合中的值可以使用 `sorted()` 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符 `<` 对元素进行比较的结果来确定。
|
||||
|
||||
```swift
|
||||
for genre in favoriteGenres.sorted() {
|
||||
@ -395,7 +395,7 @@ for genre in favoriteGenres.sorted() {
|
||||
|
||||
## 集合操作 {#performing-set-operations}
|
||||
|
||||
你可以高效地完成 `Set` 的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。
|
||||
你可以高效地完成集合的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。
|
||||
|
||||
### 基本集合操作 {#fundamental-set-operations}
|
||||
|
||||
@ -403,10 +403,10 @@ for genre in favoriteGenres.sorted() {
|
||||
|
||||

|
||||
|
||||
* 使用 `intersection(_:)` 方法根据两个集合中都包含的值创建的一个新的集合。
|
||||
* 使用 `symmetricDifference(_:)` 方法根据在一个集合中但不在两个集合中的值创建一个新的集合。
|
||||
* 使用 `union(_:)` 方法根据两个集合的值创建一个新的集合。
|
||||
* 使用 `subtracting(_:)` 方法根据不在该集合中的值创建一个新的集合。
|
||||
* 使用 `intersection(_:)` 方法根据两个集合的交集创建一个新的集合。
|
||||
* 使用 `symmetricDifference(_:)` 方法根据两个集合不相交的值创建一个新的集合。
|
||||
* 使用 `union(_:)` 方法根据两个集合的所有值创建一个新的集合。
|
||||
* 使用 `subtracting(_:)` 方法根据不在另一个集合中的值创建一个新的集合。
|
||||
|
||||
```swift
|
||||
let oddDigits: Set = [1, 3, 5, 7, 9]
|
||||
@ -425,13 +425,13 @@ oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
|
||||
|
||||
### 集合成员关系和相等 {#set-membership-and-equality}
|
||||
|
||||
下面的插图描述了三个集合 `a`、`b` 和 `c`,以及通过重叠区域表述集合间共享的元素。集合 `a` 是集合 `b` 的父集合,因为 `a` 包含了 `b` 中所有的元素,相反的,集合 `b` 是集合 `a` 的子集合,因为属于 `b` 的元素也被 `a` 包含。集合 `b` 和集合 `c` 彼此不关联,因为它们之间没有共同的元素。
|
||||
下面的插图描述了三个集合 `a`、`b` 和 `c`,以及通过重叠区域表述集合间共享的元素。集合 `a` 是集合 `b` 的*父集合*,因为 `a` 包含了 `b` 中所有的元素。相反的,集合 `b` 是集合 `a` 的*子集合*,因为属于 `b` 的元素也被 `a` 包含。集合 `b` 和集合 `c` 是*不相交*的,因为它们之间没有共同的元素。
|
||||
|
||||

|
||||
|
||||
* 使用“是否相等”运算符(`==`)来判断两个集合是否包含全部相同的值。
|
||||
* 使用 `isSubset(of:)` 方法来判断一个集合中的值是否也被包含在另外一个集合中。
|
||||
* 使用 `isSuperset(of:)` 方法来判断一个集合中包含另一个集合中所有的值。
|
||||
* 使用“是否相等”运算符(`==`)来判断两个集合包含的值是否全部相同。
|
||||
* 使用 `isSubset(of:)` 方法来判断一个集合中的所有值是否也被包含在另外一个集合中。
|
||||
* 使用 `isSuperset(of:)` 方法来判断一个集合是否包含另一个集合中所有的值。
|
||||
* 使用 `isStrictSubset(of:)` 或者 `isStrictSuperset(of:)` 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
|
||||
* 使用 `isDisjoint(with:)` 方法来判断两个集合是否不含有相同的值(是否没有交集)。
|
||||
|
||||
@ -450,27 +450,27 @@ farmAnimals.isDisjoint(with: cityAnimals)
|
||||
|
||||
## 字典 {#dictionaries}
|
||||
|
||||
*字典*是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。
|
||||
*字典*是一种无序的集合,它存储的是键值对之间的关系,其所有键的值需要是相同的类型,所有值的类型也需要相同。每个值(value)都关联唯一的*键*(key),键作为字典中这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。你在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和在现实世界中使用字典查字义的方法一样。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `Dictionary` 类型被桥接到 `Foundation` 的 `NSDictionary` 类。
|
||||
> Swift 的 `Dictionary` 类型被桥接到 Foundation 的 `NSDictionary` 类。
|
||||
>
|
||||
> 更多关于在 `Foundation` 和 `Cocoa` 中使用 `Dictionary` 类型的信息,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中 [使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6) 部分。
|
||||
> 更多关于在 Foundation 和 Cocoa 中使用 `Dictionary` 类型的信息,参见 [Bridging Between Dictionary and NSDictionary](https://developer.apple.com/documentation/swift/dictionary#2846239)。
|
||||
|
||||
### 字典类型简化语法 {#dictionary-type-shorthand-syntax}
|
||||
|
||||
Swift 的字典使用 `Dictionary<Key, Value>` 定义,其中 `Key` 是字典中键的数据类型,`Value` 是字典中对应于这些键所存储值的数据类型。
|
||||
Swift 的字典使用 `Dictionary<Key, Value>` 定义,其中 `Key` 是一种可以在字典中被用作键的类型,`Value` 是字典中对应于这些键所存储值的数据类型。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 一个字典的 `Key` 类型必须遵循 `Hashable` 协议,就像 `Set` 的值类型。
|
||||
|
||||
我们也可以用 `[Key: Value]` 这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。
|
||||
你也可以用 `[Key: Value]` 这样简化的形式去表示字典类型。虽然这两种形式功能上相同,但是后者是首选,并且本教程中涉及到字典类型时通篇采用后者。
|
||||
|
||||
### 创建一个空字典 {#creating-an-empty-dictionary}
|
||||
|
||||
我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典:
|
||||
你可以像数组一样使用构造语法创建一个拥有确定类型的空字典:
|
||||
|
||||
```swift
|
||||
var namesOfIntegers = [Int: String]()
|
||||
@ -479,7 +479,7 @@ var namesOfIntegers = [Int: String]()
|
||||
|
||||
这个例子创建了一个 `[Int: String]` 类型的空字典来储存整数的英语命名。它的键是 `Int` 型,值是 `String` 型。
|
||||
|
||||
如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作 `[:]`(中括号中放一个冒号):
|
||||
如果上下文已经提供了类型信息,你可以使用空字典字面量来创建一个空字典,记作 `[:]` (一对方括号中放一个冒号):
|
||||
|
||||
```swift
|
||||
namesOfIntegers[16] = "sixteen"
|
||||
@ -490,9 +490,9 @@ namesOfIntegers = [:]
|
||||
|
||||
### 用字典字面量创建字典 {#creating-a-dictionary-with-a-dictionary-literal}
|
||||
|
||||
我们可以使用*字典字面量*来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 `Dictionary` 集合的快捷途径。
|
||||
你可以使用*字典字面量*来构造字典,这和刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 `Dictionary` 集合的快捷途径。
|
||||
|
||||
一个键值对是一个 `key` 和一个 `value` 的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割:
|
||||
*一个键值对*是一个键和一个值的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由逗号分割、并整体被包裹在一对方括号中:
|
||||
|
||||
```swift
|
||||
[key 1: value 1, key 2: value 2, key 3: value 3]
|
||||
@ -508,26 +508,26 @@ var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `airports` 字典被声明为变量(用 `var` 关键字)而不是常量(`let` 关键字)因为后来更多的机场信息会被添加到这个示例字典中。
|
||||
> `airports` 字典被声明为变量(用 `var` 关键字)而不是常量(用 `let` 关键字)因为后面会有更多的机场信息被添加到这个字典中。
|
||||
|
||||
`airports` 字典使用字典字面量初始化,包含两个键值对。第一对的键是 `YYZ`,值是 `Toronto Pearson`。第二对的键是 `DUB`,值是 `Dublin`。
|
||||
|
||||
这个字典语句包含了两个 `String: String` 类型的键值对。它们对应 `airports` 变量声明的类型(一个只有 `String` 键和 `String` 值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的 `airport` 字典。
|
||||
这个字典语句包含了两个 `String: String` 类型的键值对。它们对应 `airports` 变量声明的类型(一个只有 `String` 键和 `String` 值的字典),所以这个字典字面量的赋值是一种方式用来构造拥有两个初始数据项的 `airport` 字典。
|
||||
|
||||
和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。
|
||||
和数组一样,你在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。
|
||||
`airports` 字典也可以用这种简短方式定义:
|
||||
|
||||
```swift
|
||||
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
|
||||
```
|
||||
|
||||
因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 `Dictionary<String, String>` 是 `airports` 字典的正确类型。
|
||||
因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 `[String: String]` 是 `airports` 字典的正确类型。
|
||||
|
||||
### 访问和修改字典 {#accessing-and-modifying-a-dictionary}
|
||||
|
||||
我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。
|
||||
你可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。
|
||||
|
||||
和数组一样,我们可以通过字典的只读属性 `count` 来获取某个字典的数据项数量:
|
||||
和数组一样,可以通过 `Dictionary` 的只读属性 `count` 来获取字典的数据项数量:
|
||||
|
||||
```swift
|
||||
print("The dictionary of airports contains \(airports.count) items.")
|
||||
@ -545,25 +545,23 @@ if airports.isEmpty {
|
||||
// 打印“The airports dictionary is not empty.”
|
||||
```
|
||||
|
||||
我们也可以在字典中使用下标语法来添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值:
|
||||
你可以通过下标语法来给字典添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值:
|
||||
|
||||
```swift
|
||||
airports["LHR"] = "London"
|
||||
// airports 字典现在有三个数据项
|
||||
```
|
||||
|
||||
我们也可以使用下标语法来改变特定键对应的值:
|
||||
也可以使用下标语法来改变特定键对应的值:
|
||||
|
||||
```swift
|
||||
airports["LHR"] = "London Heathrow"
|
||||
// “LHR”对应的值被改为“London Heathrow”
|
||||
```
|
||||
|
||||
作为另一种下标方法,字典的 `updateValue(_:forKey:)` 方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,`updateValue(_:forKey:)` 方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和上面的下标方法不同的,`updateValue(_:forKey:)` 这个方法返回更新值之前的原值。这样使得我们可以检查更新是否成功。
|
||||
作为一种替代下标语法的方式,字典的 `updateValue(_:forKey:)` 方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,`updateValue(_:forKey:)` 方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和下标的方式不同,`updateValue(_:forKey:)` 这个方法返回更新值之前的*原值*。这样使得你可以检查更新是否成功。
|
||||
|
||||
`updateValue(_:forKey:)` 方法会返回对应值的类型的可选值。举例来说:对于存储 `String` 值的字典,这个函数会返回一个 `String?` 或者“可选 `String`”类型的值。
|
||||
|
||||
如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 `nil`。
|
||||
`updateValue(_:forKey:)` 方法会返回对应值类型的可选类型。举例来说:对于存储 `String` 值的字典,这个函数会返回一个 `String?` 或者“可选 `String`”类型的值。如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 `nil` :
|
||||
|
||||
```swift
|
||||
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
|
||||
@ -572,7 +570,7 @@ if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
|
||||
// 输出“The old value for DUB was Dublin.”
|
||||
```
|
||||
|
||||
我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回 `nil`:
|
||||
你也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值类型的可选类型。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选类型,否则将返回 `nil`:
|
||||
|
||||
```swift
|
||||
if let airportName = airports["DUB"] {
|
||||
@ -583,7 +581,7 @@ if let airportName = airports["DUB"] {
|
||||
// 打印“The name of the airport is Dublin Airport.”
|
||||
```
|
||||
|
||||
我们还可以使用下标语法来通过给某个键的对应值赋值为 `nil` 来从字典里移除一个键值对:
|
||||
还可以使用下标语法通过将某个键的对应值赋值为 `nil` 来从字典里移除一个键值对:
|
||||
|
||||
```swift
|
||||
airports["APL"] = "Apple Internation"
|
||||
@ -592,7 +590,7 @@ airports["APL"] = nil
|
||||
// APL 现在被移除了
|
||||
```
|
||||
|
||||
此外,`removeValue(forKey:)` 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回 `nil`:
|
||||
此外,`removeValue(forKey:)` 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有对应值的情况下返回 `nil`:
|
||||
|
||||
```swift
|
||||
if let removedValue = airports.removeValue(forKey: "DUB") {
|
||||
@ -605,7 +603,7 @@ if let removedValue = airports.removeValue(forKey: "DUB") {
|
||||
|
||||
### 字典遍历 {#iterating-over-a-dictionary}
|
||||
|
||||
我们可以使用 `for-in` 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 `(key, value)` 元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组:
|
||||
你可以使用 `for-in` 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 `(key, value)` 元组形式返回,并且可以使用临时常量或者变量来分解这些元组:
|
||||
|
||||
```swift
|
||||
for (airportCode, airportName) in airports {
|
||||
@ -617,7 +615,7 @@ for (airportCode, airportName) in airports {
|
||||
|
||||
更多关于 `for-in` 循环的信息,参见 [For 循环](./05_Control_Flow.md#for_loops)。
|
||||
|
||||
通过访问 `keys` 或者 `values` 属性,我们也可以遍历字典的键或者值:
|
||||
通过访问 `keys` 或者 `values` 属性,你也可以遍历字典的键或者值:
|
||||
|
||||
```swift
|
||||
for airportCode in airports.keys {
|
||||
@ -633,7 +631,7 @@ for airportName in airports.values {
|
||||
// Airport name: London Heathrow
|
||||
```
|
||||
|
||||
如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受 `Array` 实例的 API 的参数,可以直接使用 `keys` 或者 `values` 属性构造一个新数组:
|
||||
如果你需要使用某个字典的键集合或者值集合来作为某个接受 `Array` 实例的 API 的参数,可以直接使用 `keys` 或者 `values` 属性构造一个新数组:
|
||||
|
||||
```swift
|
||||
let airportCodes = [String](airports.keys)
|
||||
@ -643,4 +641,4 @@ let airportNames = [String](airports.values)
|
||||
// airportNames 是 ["Toronto Pearson", "London Heathrow"]
|
||||
```
|
||||
|
||||
Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 `keys` 或 `values` 属性使用 `sorted()` 方法。
|
||||
Swift 的 `Dictionary` 是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 `keys` 或 `values` 属性使用 `sorted()` 方法。
|
||||
|
||||
@ -202,7 +202,8 @@ if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
|
||||
// 打印“min is -6 and max is 109”
|
||||
```
|
||||
|
||||
### 隐式返回的函数
|
||||
|
||||
### 隐式返回的函数 {#functions-with-an-implicit-return}
|
||||
如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:
|
||||
|
||||
```
|
||||
@ -219,9 +220,11 @@ print(anotherGreeting(for: "Dave"))
|
||||
// 打印 "Hello, Dave!"
|
||||
```
|
||||
|
||||
greeting(for:) 函数的整个定义是它返回的一条打招呼的信息,这就意味着这个函数可以使用这个更短的隐式返回形式。anothergreeting(for:) 函数使用了 return 关键词返回同样的信息,这看上去像一个更长的函数。任何一个可以被写成一行 return 语句的函数都可以忽略 return。
|
||||
|
||||
正如你将会在 [简略的 Getter 声明](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID608) 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。
|
||||
`greeting(for:)` 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。`anothergreeting(for:)` 函数返回同样的内容,却因为 `return` 关键字显得函数更长。任何一个可以被写成一行 `return` 语句的函数都可以忽略 `return`。
|
||||
|
||||
正如你将会在 [简略的 Getter 声明](./10_Properties.md) 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。
|
||||
|
||||
|
||||
## 函数参数标签和参数名称 {#Function-Argument-Labels-and-Parameter-Names}
|
||||
|
||||
|
||||
@ -174,6 +174,28 @@ struct AlternativeRect {
|
||||
}
|
||||
```
|
||||
|
||||
### 简化 Getter 声明 {#shorthand-getter-declaration}
|
||||
如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 `Rect` 结构体,用到了简化的 getter 和 setter 声明:
|
||||
|
||||
```swift
|
||||
struct CompactRect {
|
||||
var origin = Point()
|
||||
var size = Size()
|
||||
var center: Point {
|
||||
get {
|
||||
Point(x: origin.x + (size.width / 2),
|
||||
y: origin.y + (size.height / 2))
|
||||
}
|
||||
set {
|
||||
origin.x = newValue.x - (size.width / 2)
|
||||
origin.y = newValue.y - (size.height / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 getter 中忽略 `return` 与在函数中忽略 `return` 的规则相同,请参考 [隐式返回的函数](./06_Functions.md/#functions-with-an-implicit-return)。
|
||||
|
||||
### 只读计算属性 {#readonly-computed-properties}
|
||||
|
||||
只有 getter 没有 setter 的计算属性叫*只读计算属性*。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
|
||||
|
||||
@ -72,7 +72,7 @@ numberOfLegs["bird"] = 2
|
||||
|
||||
## 下标选项 {#subscript-options}
|
||||
|
||||
下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用变量参数和可变参数,但不能使用输入输出参数,也不能给参数设置默认值。
|
||||
下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用可变参数,并且可以提供默认参数数值,但是不能使用输入输出参数。
|
||||
|
||||
一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标,这就是*下标的重载*。
|
||||
|
||||
@ -139,4 +139,18 @@ func indexIsValid(row: Int, column: Int) -> Bool {
|
||||
```swift
|
||||
let someValue = matrix[2, 2]
|
||||
// 断言将会触发,因为 [2, 2] 已经超过了 matrix 的范围
|
||||
```
|
||||
```
|
||||
|
||||
## 类型下标{#type-subscripts}
|
||||
正如上节所述,实例下标是在特定类型的一个实例上调用的下标。你也可以定义一种在这个类型本身上调用的下标。这种下标的类型被称作类型下标。你可以通过在 `subscript` 关键字之前写下 `static` 关键字的方式来表示一个类型下标。类可以使用 `class` 关键字来允许子类重写父类中对那个下标的实现。下面的例子展示了如何定义和调用一个类型下标:
|
||||
```
|
||||
enum Planet: Int {
|
||||
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
|
||||
static subscript(n: Int) -> Planet {
|
||||
return Planet(rawValue: n)!
|
||||
}
|
||||
}
|
||||
let mars = Planet[4]
|
||||
print(mars)
|
||||
```
|
||||
|
||||
|
||||
@ -240,7 +240,9 @@ struct Size {
|
||||
let twoByTwo = Size(width: 2.0, height: 2.0)
|
||||
```
|
||||
|
||||
当你调用一个逐一成员构造器(memberwise initializer)时,你可以忽略任何一个有默认值的属性。在以上提到的例子中,这个 Size 结构体的 height 和 width 属性各有一个默认值。你可以忽略两者或两者之一,并且这个结构体的构造器会使用任何你忽略的属性的默认值来完成构造。举个例子:
|
||||
|
||||
当你调用一个逐一成员构造器(memberwise initializer)时,可以省略任何一个有默认值的属性。在上面这个例子中,`Size` 结构体的 `height` 和 `width` 属性各有一个默认值。你可以省略两者或两者之一,对于被省略的属性,构造器会使用默认值。举个例子:
|
||||
|
||||
```
|
||||
let zeroByTwo = Size(height: 2.0)
|
||||
print(zeroByTwo.width, zeroByTwo.height)
|
||||
|
||||
@ -242,7 +242,7 @@ class SomeSubClass: SomeSuperClass, SomeProtocol {
|
||||
|
||||
## 协议作为类型 {#protocols-as-types}
|
||||
|
||||
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。
|
||||
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。
|
||||
|
||||
协议可以像其他普通类型一样使用,使用场景如下:
|
||||
|
||||
@ -272,7 +272,7 @@ class Dice {
|
||||
|
||||
例子中定义了一个 `Dice` 类,用来代表桌游中拥有 N 个面的骰子。`Dice` 的实例含有 `sides` 和 `generator` 两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器,从而生成随机点数。
|
||||
|
||||
`generator` 属性的类型为 `RandomNumberGenerator`,因此任何遵循了 `RandomNumberGenerator` 协议的类型的实例都可以赋值给 `generator`,除此之外并无其他要求。
|
||||
`generator` 属性的类型为 `RandomNumberGenerator`,因此任何遵循了 `RandomNumberGenerator` 协议的类型的实例都可以赋值给 `generator`,除此之外并无其他要求。并且由于其类型是 `RandomNumberGenerator`,在 `Dice` 类中与 `generator` 交互的代码,必须适用于所有 `generator` 实例都遵循的方法。这句话的意思是不能使用由 `generator` 底层类型提供的任何方法或属性。但是你可以通过向下转型,从协议类型转换成底层实现类型,比如从父类向下转型为子类。请参考 [向下转型](./18_Type_Casting#downcasting)。
|
||||
|
||||
`Dice` 类还有一个构造器,用来设置初始状态。构造器有一个名为 `generator`,类型为 `RandomNumberGenerator` 的形参。在调用构造方法创建 `Dice` 的实例时,可以传入任何遵循 `RandomNumberGenerator` 协议的实例给 `generator`。
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 泛型
|
||||
|
||||
*泛型代码*让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可避免编写重复的代码,用一种清晰抽象的方式来表达代码的意图。
|
||||
*泛型代码*让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可避免编写重复的代码,而是用一种清晰抽象的方式来表达代码的意图。
|
||||
|
||||
泛型是 Swift 最强大的特性之一,很多 Swift 标准库是基于泛型代码构建的。实际上,即使你没有意识到,你也一直在*语言指南*中使用泛型。例如,Swift 的 `Array` 和 `Dictionary` 都是泛型集合。你可以创建一个 `Int` 类型数组,也可创建一个 `String` 类型数组,甚至可以是任意其他 Swift 类型的数组。同样,你也可以创建一个存储任意指定类型的字典,并对该类型没有限制。
|
||||
|
||||
@ -226,6 +226,8 @@ if let topItem = stackOfStrings.topItem {
|
||||
// 打印“The top item on the stack is tres.”
|
||||
```
|
||||
|
||||
泛型类型的扩展,还可以包括类型扩展需要额外满足的条件,从而对类型添加新功能,这一部分将在[具有泛型 Where 子句的扩展](#extensions-with-a-generic-where-clause)中进行讨论。
|
||||
|
||||
## 类型约束 {#type-constraints}
|
||||
|
||||
`swapTwoValues(_:_:)` 函数和 `Stack` 适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的*类型约束*,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
|
||||
@ -408,7 +410,7 @@ struct Stack<Element>: Container {
|
||||
|
||||
### 扩展现有类型来指定关联类型 {#extending-an-existing-type-to-specify-an-associated-type}
|
||||
|
||||
[在扩展添加协议一致性](./21_Protocols.md#adding_protocol_conformance_with_an_extension) 中描述了如何利用扩展让一个已存在的类型符合一个协议,这包括使用了关联类型协议。
|
||||
[在扩展添加协议一致性](./21_Protocols.md#adding_protocol_conformance_with_an_extension) 中描述了如何利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。
|
||||
|
||||
Swift 的 `Array` 类型已经提供 `append(_:)` 方法,`count` 属性,以及带有 `Int` 索引的下标来检索其元素。这三个功能都符合 `Container` 协议的要求,也就意味着你只需声明 `Array` 遵循`Container` 协议,就可以扩展 Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:
|
||||
|
||||
@ -444,9 +446,9 @@ protocol SuffixableContainer: Container {
|
||||
}
|
||||
```
|
||||
|
||||
在这个协议里,`Suffix` 是一个关联类型,就像上边例子中 `Container` 的 `Item` 类型一样。`Suffix` 拥有两个约束:它必须遵循 `SuffixableContainer` 协议(就是当前定义的协议),以及它的 `Item` 类型必须是和容器里的 `Item` 类型相同。`Item` 的约束是一个 `where` 分句,它在下面带有泛型 `Where` 分句的扩展中有讨论。
|
||||
在这个协议里,`Suffix` 是一个关联类型,就像上边例子中 `Container` 的 `Item` 类型一样。`Suffix` 拥有两个约束:它必须遵循 `SuffixableContainer` 协议(就是当前定义的协议),以及它的 `Item` 类型必须是和容器里的 `Item` 类型相同。`Item` 的约束是一个 `where` 分句,它在下面[具有泛型 Where 子句的扩展](#extensions-with-a-generic-where-clause)中有讨论。
|
||||
|
||||
这是上面 [强引用循环闭包](./23_Automatic_Reference_Counting.md#strong_reference_cycles_for_closures) 中 `Stack` 类型的扩展,它遵循了 SuffixableContainer 协议:
|
||||
这是上面 [泛型类型](#generic-types) 中 `Stack` 类型的扩展,它遵循了 SuffixableContainer 协议:
|
||||
|
||||
```swift
|
||||
extension Stack: SuffixableContainer {
|
||||
|
||||
249
source/chapter2/27_Opaque_Types.md
Normal file
249
source/chapter2/27_Opaque_Types.md
Normal file
@ -0,0 +1,249 @@
|
||||
# 不透明类型
|
||||
|
||||
具有不透明返回类型的函数或方法会隐藏返回值的类型信息。函数不再提供具体的类型作为返回类型,而是根据它支持的协议来描述返回值。在处理模块和调用代码之间的关系时,隐藏类型信息非常有用,因为返回的底层数据类型仍然可以保持私有。而且不同于返回协议类型,不透明类型能保证类型一致性 —— 编译器能获取到类型信息,同时模块使用者却不能获取到。
|
||||
|
||||
## 不透明类型解决的问题 {#the-problem-that-opaque-types-solve}
|
||||
|
||||
举个例子,假设你正在写一个模块,用来绘制 ASCII 符号构成的几何图形。它的基本特征是有一个 `draw()` 方法,会返回一个代表最终几何图形的字符串,你可以用包含这个方法的 `Shape` 协议来描述:
|
||||
|
||||
```swift
|
||||
protocol Shape {
|
||||
func draw() -> String
|
||||
}
|
||||
|
||||
struct Triangle: Shape {
|
||||
var size: Int
|
||||
func draw() -> String {
|
||||
var result = [String]()
|
||||
for length in 1...size {
|
||||
result.append(String(repeating: "*", count: length))
|
||||
}
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
let smallTriangle = Triangle(size: 3)
|
||||
print(smallTriangle.draw())
|
||||
// *
|
||||
// **
|
||||
// ***
|
||||
```
|
||||
|
||||
你可以利用泛型来实现垂直翻转之类的操作,就像下面这样。然而,这种方式有一个很大的局限:翻转操作的结果会暴露我们用于构造结果的泛型类型:
|
||||
|
||||
```swift
|
||||
struct FlippedShape<T: Shape>: Shape {
|
||||
var shape: T
|
||||
func draw() -> String {
|
||||
let lines = shape.draw().split(separator: "\n")
|
||||
return lines.reversed().joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
let flippedTriangle = FlippedShape(shape: smallTriangle)
|
||||
print(flippedTriangle.draw())
|
||||
// ***
|
||||
// **
|
||||
// *
|
||||
```
|
||||
|
||||
如下方代码所示,用同样的方式定义了一个 `JoinedShape<T: Shape, U: Shape>` 结构体,能将几何图形垂直拼接起来。如果拼接一个翻转三角形和一个普通三角形,它就会得到类似于 `JoinedShape<FlippedShape<Triangle>, Triangle>` 这样的类型。
|
||||
|
||||
```swift
|
||||
struct JoinedShape<T: Shape, U: Shape>: Shape {
|
||||
var top: T
|
||||
var bottom: U
|
||||
func draw() -> String {
|
||||
return top.draw() + "\n" + bottom.draw()
|
||||
}
|
||||
}
|
||||
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
|
||||
print(joinedTriangles.draw())
|
||||
// *
|
||||
// **
|
||||
// ***
|
||||
// ***
|
||||
// **
|
||||
// *
|
||||
```
|
||||
|
||||
暴露构造所用的具体类型会造成类型信息的泄露,因为 ASCII 几何图形模块的部分公开接口必须声明完整的返回类型,而实际上这些类型信息并不应该被公开声明。输出同一种几何图形,模块内部可能有多种实现方式,而外部使用时,应该与内部各种变换顺序的实现逻辑无关。诸如 `JoinedShape` 和 `FlippedShape` 这样包装后的类型,模块使用者并不关心,它们也不应该可见。模块的公开接口应该由拼接、翻转等基础操作组成,这些操作也应该返回独立的 `Shape` 类型的值。
|
||||
|
||||
## 返回不透明类型 {#returning-an-opaque-type}
|
||||
|
||||
你可以认为不透明类型和泛型相反。泛型允许调用一个方法时,为这个方法的形参和返回值指定一个与实现无关的类型。举个例子,下面这个函数的返回值类型就由它的调用者决定:
|
||||
|
||||
```swift
|
||||
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
|
||||
```
|
||||
|
||||
`x` 和 `y` 的值由调用 `max(_:_:)` 的代码决定,而它们的类型决定了 `T` 的具体类型。调用代码可以使用任何遵循了 `Comparable` 协议的类型,函数内部也要以一种通用的方式来写代码,才能应对调用者传入的各种类型。`max(_:_:)` 的实现就只使用了所有遵循 `Comparable` 协议的类型共有的特性。
|
||||
|
||||
而在返回不透明类型的函数中,上述角色发生了互换。不透明类型允许函数实现时,选择一个与调用代码无关的返回类型。比如,下面的例子返回了一个梯形,却没直接输出梯形的底层类型:
|
||||
|
||||
```swift
|
||||
struct Square: Shape {
|
||||
var size: Int
|
||||
func draw() -> String {
|
||||
let line = String(repeating: "*", count: size)
|
||||
let result = Array<String>(repeating: line, count: size)
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func makeTrapezoid() -> some Shape {
|
||||
let top = Triangle(size: 2)
|
||||
let middle = Square(size: 2)
|
||||
let bottom = FlippedShape(shape: top)
|
||||
let trapezoid = JoinedShape(
|
||||
top: top,
|
||||
bottom: JoinedShape(top: middle, bottom: bottom)
|
||||
)
|
||||
return trapezoid
|
||||
}
|
||||
let trapezoid = makeTrapezoid()
|
||||
print(trapezoid.draw())
|
||||
// *
|
||||
// **
|
||||
// **
|
||||
// **
|
||||
// **
|
||||
// *
|
||||
```
|
||||
|
||||
这个例子中,`makeTrapezoid()` 函数将返回值类型定义为 `some Shape`;因此,该函数返回遵循 `Shape` 协议的给定类型,而不需指定任何具体类型。这样写 `makeTrapezoid()` 函数可以表明它公共接口的基本性质 —— 返回的是一个几何图形 —— 而不是部分的公共接口生成的特殊类型。上述实现过程中使用了两个三角形和一个正方形,还可以用其他多种方式重写画梯形的函数,都不必改变返回类型。
|
||||
|
||||
这个例子凸显了不透明返回类型和泛型的相反之处。`makeTrapezoid()` 中代码可以返回任意它需要的类型,只要这个类型是遵循 `Shape` 协议的,就像调用泛型函数时可以使用任何需要的类型一样。这个函数的调用代码需要采用通用的方式,就像泛型函数的实现代码一样,这样才能让 `makeTrapezoid()` 返回的任何 `Shape` 类型的值都能被正常使用。
|
||||
|
||||
你也可以将不透明返回类型和泛型结合起来,下面的两个泛型函数也都返回了遵循 `Shape` 协议的不透明类型。
|
||||
|
||||
```swift
|
||||
func flip<T: Shape>(_ shape: T) -> some Shape {
|
||||
return FlippedShape(shape: shape)
|
||||
}
|
||||
func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
|
||||
JoinedShape(top: top, bottom: bottom)
|
||||
}
|
||||
|
||||
let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
|
||||
print(opaqueJoinedTriangles.draw())
|
||||
// *
|
||||
// **
|
||||
// ***
|
||||
// ***
|
||||
// **
|
||||
// *
|
||||
```
|
||||
|
||||
这个例子中 `opaqueJoinedTriangles` 的值和前文 [不透明类型解决的问题](#the-problem-that-opaque-types-solve) 中关于泛型的那个例子中的 `joinedTriangles` 完全一样。不过和前文不一样的是,`flip(_:)` 和 `join(_:_:)` 将对泛型参数的操作后的返回结果包装成了不透明类型,这样保证了在结果中泛型参数类型不可见。两个函数都是泛型函数,因为他们都依赖于泛型参数,而泛型参数又将 `FlippedShape` 和 `JoinedShape` 所需要的类型信息传递给它们。
|
||||
|
||||
如果函数中有多个地方返回了不透明类型,那么所有可能的返回值都必须是同一类型。即使对于泛型函数,不透明返回类型可以使用泛型参数,但仍需保证返回类型唯一。比如,下面就是一个*非法*示例 —— 包含针对 `Square` 类型进行特殊处理的翻转函数。
|
||||
|
||||
```swift
|
||||
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
|
||||
if shape is Square {
|
||||
return shape // 错误:返回类型不一致
|
||||
}
|
||||
return FlippedShape(shape: shape) // 错误:返回类型不一致
|
||||
}
|
||||
```
|
||||
|
||||
如果你调用这个函数时传入一个 `Square` 类型,那么它会返回 `Square` 类型;否则,它会返回一个 `FlippedShape` 类型。这违反了返回值类型唯一的要求,所以 `invalidFlip(_:)` 不正确。修正 `invalidFlip(_:)` 的方法之一就是将针对 `Square` 的特殊处理移入到 `FlippedShape` 的实现中去,这样就能保证这个函数始终返回 `FlippedShape`:
|
||||
|
||||
```swift
|
||||
struct FlippedShape<T: Shape>: Shape {
|
||||
var shape: T
|
||||
func draw() -> String {
|
||||
if shape is Square {
|
||||
return shape.draw()
|
||||
}
|
||||
let lines = shape.draw().split(separator: "\n")
|
||||
return lines.reversed().joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
返回类型始终唯一的要求,并不会影响在返回的不透明类型中使用泛型。比如下面的函数,就是在返回的底层类型中使用了泛型参数:
|
||||
|
||||
```swift
|
||||
func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
|
||||
return Array<T>(repeating: shape, count: count)
|
||||
}
|
||||
```
|
||||
|
||||
这种情况下,返回的底层类型会根据 `T` 的不同而发生变化:但无论什么形状被传入,`repeat(shape:count:)` 都会创建并返回一个元素为相应形状的数组。尽管如此,返回值始终还是同样的底层类型 `[T]`, 所以这符合不透明返回类型始终唯一的要求。
|
||||
|
||||
## 不透明类型和协议类型的区别 {#differences-between-opaque-types-and-protocol-types}
|
||||
|
||||
虽然使用不透明类型作为函数返回值,看起来和返回协议类型非常相似,但这两者有一个主要区别,就在于是否需要保证类型一致性。一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多样的值,而不透明类型对这些底层类型有更强的限定。
|
||||
|
||||
比如,这是 `flip(_:)` 方法不采用不透明类型,而采用返回协议类型的版本:
|
||||
|
||||
```swift
|
||||
func protoFlip<T: Shape>(_ shape: T) -> Shape {
|
||||
return FlippedShape(shape: shape)
|
||||
}
|
||||
```
|
||||
|
||||
这个版本的 `protoFlip(_:)` 和 `flip(_:)` 有相同的函数体,并且它也始终返回唯一类型。但不同于 `flip(_:)`,`protoFlip(_:)` 返回值其实不需要始终返回唯一类型 —— 返回类型只需要遵循 `Shape` 协议即可。换句话说,`protoFlip(_:)` 比起 `flip(_:)` 对 API 调用者的约束更加松散。它保留了返回多种不同类型的灵活性:
|
||||
|
||||
```swift
|
||||
func protoFlip<T: Shape>(_ shape: T) -> Shape {
|
||||
if shape is Square {
|
||||
return shape
|
||||
}
|
||||
|
||||
return FlippedShape(shape: shape)
|
||||
}
|
||||
```
|
||||
|
||||
修改后的代码根据代表形状的参数的不同,可能返回 `Square` 实例或者 `FlippedShape` 实例,所以同样的函数可能返回完全不同的两个类型。当翻转相同形状的多个实例时,此函数的其他有效版本也可能返回完全不同类型的结果。`protoFlip(_:)` 返回类型的不确定性,意味着很多依赖返回类型信息的操作也无法执行了。举个例子,这个函数的返回结果就不能用 == 运算符进行比较了。
|
||||
|
||||
```swift
|
||||
let protoFlippedTriangle = protoFlip(smallTriangle)
|
||||
let sameThing = protoFlip(smallTriangle)
|
||||
protoFlippedTriangle == sameThing // 错误
|
||||
```
|
||||
|
||||
上面的例子中,最后一行的错误来源于多个原因。最直接的问题在于,`Shape` 协议中并没有包含对 == 运算符的声明。如果你尝试加上这个声明,那么你会遇到新的问题,就是 == 运算符需要知道左右两侧参数的类型。这类运算符通常会使用 `Self` 类型作为参数,用来匹配符合协议的具体类型,但是由于将协议当成类型使用时会发生类型擦除,所以并不能给协议加上对 `Self` 的实现要求。
|
||||
|
||||
将协议类型作为函数的返回类型能更加灵活,函数只要返回遵循协议的类型即可。然而,更具灵活性导致牺牲了对返回值执行某些操作的能力。上面的例子就说明了为什么不能使用 == 运算符 —— 它依赖于具体的类型信息,而这正是使用协议类型所无法提供的。
|
||||
|
||||
这种方法的另一个问题在于,变换形状的操作不能嵌套。翻转三角形的结果是一个 `Shape` 类型的值,而 `protoFlip(_:)` 方法的则将遵循 `Shape` 协议的类型作为形参,然而协议类型的值并不遵循这个协议;`protoFlip(_:)` 的返回值也并不遵循 `Shape` 协议。这就是说 `protoFlip(protoFlip(smallTriange))` 这样的多重变换操作是非法的,因为经过翻转操作后的结果类型并不能作为 `protoFlip(_:)` 的形参。
|
||||
|
||||
相比之下,不透明类型则保留了底层类型的唯一性。Swift 能够推断出关联类型,这个特点使得作为函数返回值,不透明类型比协议类型有更大的使用场景。比如,下面这个例子是 [泛型](./22_Generics.md) 中讲到的 `Container` 协议:
|
||||
|
||||
```swift
|
||||
protocol Container {
|
||||
associatedtype Item
|
||||
var count: Int { get }
|
||||
subscript(i: Int) -> Item { get }
|
||||
}
|
||||
extension Array: Container { }
|
||||
```
|
||||
|
||||
你不能将 `Container` 作为方法的返回类型,因为此协议有一个关联类型。你也不能将它用于对泛型返回类型的约束,因为函数体之外并没有暴露足够多的信息来推断泛型类型。
|
||||
|
||||
```swift
|
||||
// 错误:有关联类型的协议不能作为返回类型。
|
||||
func makeProtocolContainer<T>(item: T) -> Container {
|
||||
return [item]
|
||||
}
|
||||
|
||||
// 错误:没有足够多的信息来推断 C 的类型。
|
||||
func makeProtocolContainer<T, C: Container>(item: T) -> C {
|
||||
return [item]
|
||||
}
|
||||
```
|
||||
|
||||
而使用不透明类型 `some Container` 作为返回类型,就能够明确地表达所需要的 API 契约 —— 函数会返回一个集合类型,但并不指明它的具体类型:
|
||||
|
||||
```swift
|
||||
func makeOpaqueContainer<T>(item: T) -> some Container {
|
||||
return [item]
|
||||
}
|
||||
let opaqueContainer = makeOpaqueContainer(item: 12)
|
||||
let twelve = opaqueContainer[0]
|
||||
print(type(of: twelve))
|
||||
// 输出 "Int"
|
||||
```
|
||||
|
||||
`twelve` 的类型可以被推断出为 `Int`, 这说明了类型推断适用于不透明类型。在 `makeOpaqueContainer(item:)` 的实现中,底层类型是不透明集合 `[T]`。在上述这种情况下,`T` 就是 `Int` 类型,所以返回值就是整数数组,而关联类型 `Item` 也被推断出为 `Int`。`Container` 协议中的 `subscipt` 方法会返回 `Item`,这也意味着 `twelve` 的类型也被能推断出为 `Int`。
|
||||
Reference in New Issue
Block a user