Add files via upload
104
IOS/Task00:Swift基础语法学习/0.Swift介绍与环境搭建.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Swift介绍与环境搭建
|
||||
|
||||
Swift 是一种支持多编程范式和编译式的开源编程语言,苹果于2014年WWDC(苹果开发者大会)发布,用于开发 iOS,OS X 和 watchOS 应用程序。Swift 结合了 C 和 Objective-C 的优点并且不受 C 兼容性的限制。Swift 在 Mac OS 和 iOS 平台可以和 Object-C 使用相同的运行环境。2015年6月8日,苹果于WWDC 2015上宣布,Swift将开放源代码,包括编译器和标准库。
|
||||
|
||||
## 关于Swift
|
||||
|
||||
Swift 是一种非常好的编写软件的方式,无论是手机,台式机,服务器,还是其他运行代码的设备。它是一种安全,快速和互动的编程语言,将现代编程语言的精华和苹果工程师文化的智慧,以及来自开源社区的多样化贡献结合了起来。编译器对性能进行了优化,编程语言对开发进行了优化,两者互不干扰,鱼与熊掌兼得。
|
||||
|
||||
Swift 对于初学者来说也很友好。它是第一个既满足工业标准又像脚本语言一样充满表现力和趣味的系统编程语言。它支持代码预览(playgrounds),这个革命性的特性可以允许程序员在不编译和运行应用程序的前提下运行 Swift 代码并实时查看结果。
|
||||
|
||||
Swift 通过采用现代编程模式来避免大量常见编程错误:
|
||||
|
||||
- 变量始终在使用前初始化。
|
||||
- 检查数组索引超出范围的错误。
|
||||
- 检查整数是否溢出。
|
||||
- 可选值确保明确处理 `nil` 值。
|
||||
- 内存被自动管理。
|
||||
- 错误处理允许从意外故障控制恢复。
|
||||
|
||||
Swift 代码被编译和优化,以充分利用现代硬件。语法和标准库是基于指导原则设计的,编写代码的明显方式也应该是最好的。安全性和速度的结合使得 Swift 成为从 “Hello,world!” 到整个操作系统的绝佳选择。
|
||||
|
||||
Swift 将强大的类型推理和模式匹配与现代轻巧的语法相结合,使复杂的想法能够以清晰简洁的方式表达。因此,代码不仅更容易编写,而且易于阅读和维护。
|
||||
|
||||
## 环境搭建
|
||||
|
||||
Swift是一门开源的编程语言,该语言用于开发OS X和iOS应用程序。
|
||||
|
||||
在正式开发应用程序前,我们需要搭建Swift开发环境,以便更好友好的使用各种开发工具和语言进行快速应用开发。由于Swift开发环境需要在OS X系统中运行,因此其环境的搭建将不同于Windows环境,下面就一起来学习一下Swift开发环境的搭建方法。
|
||||
|
||||
成功搭建Swift开发环境的前提:
|
||||
|
||||
- 一台Mac/iMac 。因为集成开发环境XCode只能运行在OS X系统上。
|
||||
- 电脑系统必须在OS 10.9.3及以上。
|
||||
- 电脑必须安装Xcode集成开发环境。
|
||||
|
||||
此外,如果您的手上没有Mac/iMac,您仍然可以尝试在iPad的Playgrounds中进行Swift小项目的探索。如果您手上既没有Mac/iMac,也没有iPad相关产品,您可以选择在虚拟机上搭建相关项目,这可能会造成系统性能的下降。如果您对这些都不感兴趣,学习一门面向协议的新型函数式编程语言也不失为一种收获。
|
||||
|
||||
## 使用Xcode创建第一个程序
|
||||
|
||||
在App Store下载Xcode完毕后,双击运行:
|
||||
|
||||
- 点击`Create a new Xcode project`
|
||||
|
||||
<img src="./images/1.png" alt="avatar" style="zoom:40%;" />
|
||||
|
||||
|
||||
|
||||
- 选择`iOS`-`App`并`Next`
|
||||
|
||||
<img src="images/2.png" alt="2" style="zoom:33%;" />
|
||||
|
||||
- 输入自己的`Product Name`与`Organization Identifier`:
|
||||
|
||||
<img src="images/3.png" alt="3" style="zoom:33%;" />
|
||||
|
||||
- 初始页面如下,在左侧找到`ViewController.swift`,添加如下代码:
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
var label: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view.
|
||||
|
||||
label = UILabel.init(frame: CGRect(x:50,y:50, width:100,height:30))
|
||||
//创建标签,并设置位置,宽度、高度
|
||||
label.text = "Hello!"
|
||||
//设置标签文字
|
||||
label.textColor = UIColor.red
|
||||
//设置标签字体颜色
|
||||
self.view.addSubview(label)
|
||||
//将标签添加到View中
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
- 点击运行,结果如下:
|
||||
|
||||
<img src="/Users/mac/Desktop/iOSdev/Task00:Swift基础语法学习/images/5.png" alt="5" style="zoom:33%;" />
|
||||
|
||||
至此,我们完成了第一个iOS程序的搭建
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Xcode初探
|
||||
|
||||
对于初学者而言,Xcode中的storyboard提供了简单易懂的UI界面,我们可以在storyboard上搭建自己想要的界面。
|
||||
|
||||

|
||||
|
||||
双击编辑,输入Hello World!点击运行即可看到预览结果
|
||||
|
||||

|
||||
|
||||
除了Storyboard,Xcode还存在另一种启动方式:代码启动。在实际工程文件与开发过程中,Storyboard上堆砌大量空间会导致运行卡顿,因此本部分仅作为演示使用。
|
||||
772
IOS/Task00:Swift基础语法学习/1.基础部分.md
Normal file
@@ -0,0 +1,772 @@
|
||||
# Swift语法基础部分
|
||||
|
||||
## 基础部分
|
||||
|
||||
Swift 是一门开发 iOS, macOS, watchOS 和 tvOS 应用的新语言。然而,如果你有 C 或者 Objective-C 开发经验的话,你会发现 Swift 的很多内容都是你熟悉的。
|
||||
|
||||
Swift 包含了 C 和 Objective-C 上所有基础数据类型,`Int` 表示整型值; `Double` 和 `Float` 表示浮点型值; `Bool` 是布尔型值;`String` 是文本型数据。 Swift 还提供了三个基本的集合类型,`Array`、`Set` 和 `Dictionary` ,详见`集合类型`部分。
|
||||
|
||||
就像 C 语言一样,Swift 使用变量来进行存储并通过变量名来关联值。在 Swift 中,广泛的使用着值不可变的变量,它们就是常量,而且比 C 语言的常量更强大。在 Swift 中,如果你要处理的值不需要改变,那使用常量可以让你的代码更加安全并且更清晰地表达你的意图。
|
||||
|
||||
除了我们熟悉的类型,Swift 还增加了 Objective-C 中没有的高阶数据类型比如元组(Tuple)。元组可以让你创建或者传递一组数据,比如作为函数的返回值时,你可以用一个元组可以返回多个值。
|
||||
|
||||
Swift 还增加了可选(Optional)类型,用于处理值缺失的情况。可选表示 “那儿有一个值,并且它等于 *x* ” 或者 “那儿没有值” 。可选有点像在 Objective-C 中使用 `nil` ,但是它可以用在任何类型上,不仅仅是类。可选类型比 Objective-C 中的 `nil` 指针更加安全也更具表现力,它是 Swift 许多强大特性的重要组成部分。
|
||||
|
||||
Swift 是一门*类型安全*的语言,这意味着 Swift 可以让你清楚地知道值的类型。如果你的代码需要一个 `String` ,类型安全会阻止你不小心传入一个 `Int` 。同样的,如果你的代码需要一个 `String`,类型安全会阻止你意外传入一个可选的 `String` 。类型安全可以帮助你在开发阶段尽早发现并修正错误。
|
||||
|
||||
## 常量和变量
|
||||
|
||||
常量和变量把一个名字(比如 `maximumNumberOfLoginAttempts` 或者 `welcomeMessage` )和一个指定类型的值(比如数字 `10` 或者字符串 `"Hello"` )关联起来。*常量*的值一旦设定就不能改变,而*变量*的值可以随意更改。
|
||||
|
||||
### 声明常量和变量
|
||||
|
||||
常量和变量必须在使用前声明,用 `let` 来声明常量,用 `var` 来声明变量。下面的例子展示了如何用常量和变量来记录用户尝试登录的次数:
|
||||
|
||||
```swift
|
||||
let maximumNumberOfLoginAttempts = 10
|
||||
var currentLoginAttempt = 0
|
||||
```
|
||||
|
||||
这两行代码可以被理解为:
|
||||
|
||||
“声明一个名字是 `maximumNumberOfLoginAttempts` 的新常量,并给它一个值 `10` 。然后,声明一个名字是 `currentLoginAttempt` 的变量并将它的值初始化为 `0` 。”
|
||||
|
||||
在这个例子中,允许的最大尝试登录次数被声明为一个常量,因为这个值不会改变。当前尝试登录次数被声明为一个变量,因为每次尝试登录失败的时候都需要增加这个值。
|
||||
|
||||
你可以在一行中声明多个常量或者多个变量,用逗号隔开:
|
||||
|
||||
```swift
|
||||
var x = 0.0, y = 0.0, z = 0.0
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你的代码中有不需要改变的值,请使用 `let` 关键字将它声明为常量。只将需要改变的值声明为变量。
|
||||
|
||||
### 类型注解
|
||||
|
||||
当你声明常量或者变量的时候可以加上*类型注解(type annotation)*,说明常量或者变量中要存储的值的类型。如果要添加类型注解,需要在常量或者变量名后面加上一个冒号和空格,然后加上类型名称。
|
||||
|
||||
这个例子给 `welcomeMessage` 变量添加了类型注解,表示这个变量可以存储 `String` 类型的值:
|
||||
|
||||
```swift
|
||||
var welcomeMessage: String
|
||||
```
|
||||
|
||||
声明中的冒号代表着*“是...类型”*,所以这行代码可以被理解为:
|
||||
|
||||
“声明一个类型为 `String` ,名字为 `welcomeMessage` 的变量。”
|
||||
|
||||
“类型为 `String` ”的意思是“可以存储任意 `String` 类型的值。”
|
||||
|
||||
`welcomeMessage` 变量现在可以被设置成任意字符串:
|
||||
|
||||
```swift
|
||||
welcomeMessage = "Hello"
|
||||
```
|
||||
|
||||
你可以在一行中定义多个同样类型的变量,用逗号分割,并在最后一个变量名之后添加类型注解:
|
||||
|
||||
```swift
|
||||
var red, green, blue: Double
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 一般来说你很少需要写类型注解。如果你在声明常量或者变量的时候赋了一个初始值,Swift 可以推断出这个常量或者变量的类型,请参考`类型安全和类型推断`。在上面的例子中,没有给 `welcomeMessage` 赋初始值,所以变量 `welcomeMessage` 的类型是通过一个类型注解指定的,而不是通过初始值推断的。
|
||||
|
||||
### 常量和变量的命名
|
||||
|
||||
常量和变量名可以包含任何字符,包括 Unicode 字符:
|
||||
|
||||
```swift
|
||||
let π = 3.14159
|
||||
let 你好 = "你好世界"
|
||||
let 🐶🐮 = "dogcow"
|
||||
```
|
||||
|
||||
常量与变量名不能包含数学符号,箭头,保留的(或者非法的)Unicode 码位,连线与制表符。也不能以数字开头,但是可以在常量与变量名的其他地方包含数字。
|
||||
|
||||
一旦你将常量或者变量声明为确定的类型,你就不能使用相同的名字再次进行声明,或者改变其存储的值的类型。同时,你也不能将常量与变量进行互转。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你需要使用与 Swift 保留关键字相同的名称作为常量或者变量名,你可以使用反引号(`)将关键字包围的方式将其作为名字使用。无论如何,你应当避免使用关键字作为常量或变量名,除非你别无选择。
|
||||
|
||||
你可以更改现有的变量值为其他同类型的值,在下面的例子中,`friendlyWelcome` 的值从 `"Hello!"`改为了 `"Bonjour!"`:
|
||||
|
||||
```swift
|
||||
var friendlyWelcome = "Hello!"
|
||||
friendlyWelcome = "Bonjour!"
|
||||
// friendlyWelcome 现在是 "Bonjour!"
|
||||
```
|
||||
|
||||
与变量不同,常量的值一旦被确定就不能更改了。尝试这样做会导致编译时报错:
|
||||
|
||||
```swift
|
||||
let languageName = "Swift"
|
||||
languageName = "Swift++"
|
||||
// 这会报编译时错误 - languageName 不可改变
|
||||
```
|
||||
|
||||
### 输出常量和变量
|
||||
|
||||
你可以用 `print(_:separator:terminator:)` 函数来输出当前常量或变量的值:
|
||||
|
||||
```swift
|
||||
print(friendlyWelcome)
|
||||
// 输出“Bonjour!”
|
||||
```
|
||||
|
||||
`print(_:separator:terminator:)` 是一个用来输出一个或多个值到适当输出区的全局函数。如果你用 Xcode,`print(_:separator:terminator:)` 将会输出内容到“console”面板上。`separator` 和 `terminator` 参数具有默认值,因此你调用这个函数的时候可以忽略它们。默认情况下,该函数通过添加换行符来结束当前行。如果不想换行,可以传递一个空字符串给 `terminator` 参数--例如,`print(someValue, terminator:"")` 。关于参数默认值的更多信息,请参考 [默认参数值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#default_parameter_values)。
|
||||
|
||||
Swift 用*字符串插值(string interpolation)*的方式把常量名或者变量名当做占位符加入到长字符串中,Swift 会用当前常量或变量的值替换这些占位符。将常量或变量名放入圆括号中,并在开括号前使用反斜杠将其转义:
|
||||
|
||||
```swift
|
||||
print("The current value of friendlyWelcome is \(friendlyWelcome)")
|
||||
// 输出“The current value of friendlyWelcome is Bonjour!”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 字符串插值所有可用的选项,请参考 [字符串插值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#string_interpolation)。
|
||||
|
||||
## 注释
|
||||
|
||||
请将你的代码中的非执行文本注释成提示或者笔记以方便你将来阅读。Swift 的编译器将会在编译代码时自动忽略掉注释部分。
|
||||
|
||||
Swift 中的注释与 C 语言的注释非常相似。单行注释以双正斜杠(`//`)作为起始标记:
|
||||
|
||||
```swift
|
||||
// 这是一个注释
|
||||
```
|
||||
|
||||
你也可以进行多行注释,其起始标记为单个正斜杠后跟随一个星号(`/*`),终止标记为一个星号后跟随单个正斜杠(`*/`):
|
||||
|
||||
```swift
|
||||
/* 这也是一个注释,
|
||||
但是是多行的 */
|
||||
```
|
||||
|
||||
与 C 语言多行注释不同,Swift 的多行注释可以嵌套在其它的多行注释之中。你可以先生成一个多行注释块,然后在这个注释块之中再嵌套成第二个多行注释。终止注释时先插入第二个注释块的终止标记,然后再插入第一个注释块的终止标记:
|
||||
|
||||
```swift
|
||||
/* 这是第一个多行注释的开头
|
||||
/* 这是第二个被嵌套的多行注释 */
|
||||
这是第一个多行注释的结尾 */
|
||||
```
|
||||
|
||||
通过运用嵌套多行注释,你可以快速方便的注释掉一大段代码,即使这段代码之中已经含有了多行注释块。
|
||||
|
||||
## 分号
|
||||
|
||||
与其他大部分编程语言不同,Swift 并不强制要求你在每条语句的结尾处使用分号(`;`),当然,你也可以按照你自己的习惯添加分号。有一种情况下必须要用分号,即你打算在同一行内写多条独立的语句:
|
||||
|
||||
```swift
|
||||
let cat = "🐱"; print(cat)
|
||||
// 输出“🐱”
|
||||
```
|
||||
|
||||
## 整数
|
||||
|
||||
整数就是没有小数部分的数字,比如 `42` 和 `-23` 。整数可以是 `有符号`(正、负、零)或者 `无符号`(正、零)。
|
||||
|
||||
Swift 提供了8、16、32和64位的有符号和无符号整数类型。这些整数类型和 C 语言的命名方式很像,比如8位无符号整数类型是 `UInt8`,32位有符号整数类型是 `Int32` 。就像 Swift 的其他类型一样,整数类型采用大写命名法。
|
||||
|
||||
### 整数范围
|
||||
|
||||
你可以访问不同整数类型的 `min` 和 `max` 属性来获取对应类型的最小值和最大值:
|
||||
|
||||
```swift
|
||||
let minValue = UInt8.min // minValue 为 0,是 UInt8 类型
|
||||
let maxValue = UInt8.max // maxValue 为 255,是 UInt8 类型
|
||||
```
|
||||
|
||||
`min` 和 `max` 所传回值的类型,正是其所对的整数类型(如上例 UInt8, 所传回的类型是 UInt8),可用在表达式中相同类型值旁。
|
||||
|
||||
### Int
|
||||
|
||||
一般来说,你不需要专门指定整数的长度。Swift 提供了一个特殊的整数类型 `Int`,长度与当前平台的原生字长相同:
|
||||
|
||||
- 在32位平台上,`Int` 和 `Int32` 长度相同。
|
||||
- 在64位平台上,`Int` 和 `Int64` 长度相同。
|
||||
|
||||
除非你需要特定长度的整数,一般来说使用 `Int` 就够了。这可以提高代码一致性和可复用性。即使是在32位平台上,`Int` 可以存储的整数范围也可以达到 `-2,147,483,648` ~ `2,147,483,647`,大多数时候这已经足够大了。
|
||||
|
||||
### UInt
|
||||
|
||||
Swift 也提供了一个特殊的无符号类型 `UInt`,长度与当前平台的原生字长相同:
|
||||
|
||||
- 在32位平台上,`UInt` 和 `UInt32` 长度相同。
|
||||
- 在64位平台上,`UInt` 和 `UInt64` 长度相同。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 尽量不要使用 `UInt`,除非你真的需要存储一个和当前平台原生字长相同的无符号整数。除了这种情况,最好使用 `Int`,即使你要存储的值已知是非负的。统一使用 `Int` 可以提高代码的可复用性,避免不同类型数字之间的转换,并且匹配数字的类型推断,请参考 [类型安全和类型推断](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/01_The_Basics.html#type_safety_and_type_inference)。
|
||||
|
||||
## 浮点数
|
||||
|
||||
浮点数是有小数部分的数字,比如 `3.14159`、`0.1` 和 `-273.15`。
|
||||
|
||||
浮点类型比整数类型表示的范围更大,可以存储比 `Int` 类型更大或者更小的数字。Swift 提供了两种有符号浮点数类型:
|
||||
|
||||
- `Double` 表示64位浮点数。当你需要存储很大或者很高精度的浮点数时请使用此类型。
|
||||
- `Float` 表示32位浮点数。精度要求不高的话可以使用此类型。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `Double` 精确度很高,至少有15位数字,而 `Float` 只有6位数字。选择哪个类型取决于你的代码需要处理的值的范围,在两种类型都匹配的情况下,将优先选择 `Double`。
|
||||
|
||||
## 类型安全和类型推断
|
||||
|
||||
Swift 是一个*类型安全(type safe)*的语言。类型安全的语言可以让你清楚地知道代码要处理的值的类型。如果你的代码需要一个 `String`,你绝对不可能不小心传进去一个 `Int`。
|
||||
|
||||
由于 Swift 是类型安全的,所以它会在编译你的代码时进行*类型检查(type checks)*,并把不匹配的类型标记为错误。这可以让你在开发的时候尽早发现并修复错误。
|
||||
|
||||
当你要处理不同类型的值时,类型检查可以帮你避免错误。然而,这并不是说你每次声明常量和变量的时候都需要显式指定类型。如果你没有显式指定类型,Swift 会使用*类型推断(type inference)*来选择合适的类型。有了类型推断,编译器可以在编译代码的时候自动推断出表达式的类型。原理很简单,只要检查你赋的值即可。
|
||||
|
||||
因为有类型推断,和 C 或者 Objective-C 比起来 Swift 很少需要声明类型。常量和变量虽然需要明确类型,但是大部分工作并不需要你自己来完成。
|
||||
|
||||
当你声明常量或者变量并赋初值的时候类型推断非常有用。当你在声明常量或者变量的时候赋给它们一个字面量(literal value 或 literal)即可触发类型推断。(字面量就是会直接出现在你代码中的值,比如 `42`和 `3.14159` 。)
|
||||
|
||||
例如,如果你给一个新常量赋值 `42` 并且没有标明类型,Swift 可以推断出常量类型是 `Int` ,因为你给它赋的初始值看起来像一个整数:
|
||||
|
||||
```swift
|
||||
let meaningOfLife = 42
|
||||
// meaningOfLife 会被推测为 Int 类型
|
||||
```
|
||||
|
||||
同理,如果你没有给浮点字面量标明类型,Swift 会推断你想要的是 `Double`:
|
||||
|
||||
```swift
|
||||
let pi = 3.14159
|
||||
// pi 会被推测为 Double 类型
|
||||
```
|
||||
|
||||
当推断浮点数的类型时,Swift 总是会选择 `Double` 而不是 `Float`。
|
||||
|
||||
如果表达式中同时出现了整数和浮点数,会被推断为 `Double` 类型:
|
||||
|
||||
```swift
|
||||
let anotherPi = 3 + 0.14159
|
||||
// anotherPi 会被推测为 Double 类型
|
||||
```
|
||||
|
||||
原始值 `3` 没有显式声明类型,而表达式中出现了一个浮点字面量,所以表达式会被推断为 `Double` 类型。
|
||||
|
||||
## 数值型字面量
|
||||
|
||||
整数字面量可以被写作:
|
||||
|
||||
- 一个*十进制*数,没有前缀
|
||||
- 一个*二进制*数,前缀是 `0b`
|
||||
- 一个*八进制*数,前缀是 `0o`
|
||||
- 一个*十六进制*数,前缀是 `0x`
|
||||
|
||||
下面的所有整数字面量的十进制值都是 `17`:
|
||||
|
||||
```swift
|
||||
let decimalInteger = 17
|
||||
let binaryInteger = 0b10001 // 二进制的17
|
||||
let octalInteger = 0o21 // 八进制的17
|
||||
let hexadecimalInteger = 0x11 // 十六进制的17
|
||||
```
|
||||
|
||||
浮点字面量可以是十进制(没有前缀)或者是十六进制(前缀是 `0x` )。小数点两边必须有至少一个十进制数字(或者是十六进制的数字)。十进制浮点数也可以有一个可选的指数(exponent),通过大写或者小写的 `e` 来指定;十六进制浮点数必须有一个指数,通过大写或者小写的 `p` 来指定。
|
||||
|
||||
如果一个十进制数的指数为 `exp`,那这个数相当于基数和10^exp 的乘积:
|
||||
|
||||
- `1.25e2` 表示 1.25 × 10^2,等于 `125.0`。
|
||||
- `1.25e-2` 表示 1.25 × 10^-2,等于 `0.0125`。
|
||||
|
||||
如果一个十六进制数的指数为 `exp`,那这个数相当于基数和2^exp 的乘积:
|
||||
|
||||
- `0xFp2` 表示 15 × 2^2,等于 `60.0`。
|
||||
- `0xFp-2` 表示 15 × 2^-2,等于 `3.75`。
|
||||
|
||||
下面的这些浮点字面量都等于十进制的 `12.1875`:
|
||||
|
||||
```swift
|
||||
let decimalDouble = 12.1875
|
||||
let exponentDouble = 1.21875e1
|
||||
let hexadecimalDouble = 0xC.3p0
|
||||
```
|
||||
|
||||
数值类字面量可以包括额外的格式来增强可读性。整数和浮点数都可以添加额外的零并且包含下划线,并不会影响字面量:
|
||||
|
||||
```swift
|
||||
let paddedDouble = 000123.456
|
||||
let oneMillion = 1_000_000
|
||||
let justOverOneMillion = 1_000_000.000_000_1
|
||||
```
|
||||
|
||||
## 数值型类型转换
|
||||
|
||||
通常来讲,即使代码中的整数常量和变量已知非负,也请使用 `Int` 类型。总是使用默认的整数类型可以保证你的整数常量和变量可以直接被复用并且可以匹配整数类字面量的类型推断。
|
||||
|
||||
只有在必要的时候才使用其他整数类型,比如要处理外部的长度明确的数据或者为了优化性能、内存占用等等。使用显式指定长度的类型可以及时发现值溢出并且可以暗示正在处理特殊数据。
|
||||
|
||||
### 整数转换
|
||||
|
||||
不同整数类型的变量和常量可以存储不同范围的数字。`Int8` 类型的常量或者变量可以存储的数字范围是 `-128`~`127`,而 `UInt8` 类型的常量或者变量能存储的数字范围是 `0`~`255`。如果数字超出了常量或者变量可存储的范围,编译的时候会报错:
|
||||
|
||||
```swift
|
||||
let cannotBeNegative: UInt8 = -1
|
||||
// UInt8 类型不能存储负数,所以会报错
|
||||
let tooBig: Int8 = Int8.max + 1
|
||||
// Int8 类型不能存储超过最大值的数,所以会报错
|
||||
```
|
||||
|
||||
由于每种整数类型都可以存储不同范围的值,所以你必须根据不同情况选择性使用数值型类型转换。这种选择性使用的方式,可以预防隐式转换的错误并让你的代码中的类型转换意图变得清晰。
|
||||
|
||||
要将一种数字类型转换成另一种,你要用当前值来初始化一个期望类型的新数字,这个数字的类型就是你的目标类型。在下面的例子中,常量 `twoThousand` 是 `UInt16` 类型,然而常量 `one` 是 `UInt8` 类型。它们不能直接相加,因为它们类型不同。所以要调用 `UInt16(one)` 来创建一个新的 `UInt16` 数字并用 `one` 的值来初始化,然后使用这个新数字来计算:
|
||||
|
||||
```swift
|
||||
let twoThousand: UInt16 = 2_000
|
||||
let one: UInt8 = 1
|
||||
let twoThousandAndOne = twoThousand + UInt16(one)
|
||||
```
|
||||
|
||||
现在两个数字的类型都是 `UInt16`,可以进行相加。目标常量 `twoThousandAndOne` 的类型被推断为 `UInt16`,因为它是两个 `UInt16` 值的和。
|
||||
|
||||
`SomeType(ofInitialValue)` 是调用 Swift 构造器并传入一个初始值的默认方法。在语言内部,`UInt16` 有一个构造器,可以接受一个 `UInt8` 类型的值,所以这个构造器可以用现有的 `UInt8`来创建一个新的 `UInt16`。注意,你并不能传入任意类型的值,只能传入 `UInt16` 内部有对应构造器的值。不过你可以扩展现有的类型来让它可以接收其他类型的值(包括自定义类型),请参考 [扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html)。
|
||||
|
||||
### 整数和浮点数转换
|
||||
|
||||
整数和浮点数的转换必须显式指定类型:
|
||||
|
||||
```swift
|
||||
let three = 3
|
||||
let pointOneFourOneFiveNine = 0.14159
|
||||
let pi = Double(three) + pointOneFourOneFiveNine
|
||||
// pi 等于 3.14159,所以被推测为 Double 类型
|
||||
```
|
||||
|
||||
这个例子中,常量 `three` 的值被用来创建一个 `Double` 类型的值,所以加号两边的数类型须相同。如果不进行转换,两者无法相加。
|
||||
|
||||
浮点数到整数的反向转换同样行,整数类型可以用 `Double` 或者 `Float` 类型来初始化:
|
||||
|
||||
```swift
|
||||
let integerPi = Int(pi)
|
||||
// integerPi 等于 3,所以被推测为 Int 类型
|
||||
```
|
||||
|
||||
当用这种方式来初始化一个新的整数值时,浮点值会被截断。也就是说 `4.75` 会变成 `4`,`-3.9` 会变成 `-3`。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 结合数字类常量和变量不同于结合数字类字面量。字面量 `3` 可以直接和字面量 `0.14159` 相加,因为数字字面量本身没有明确的类型。它们的类型只在编译器需要求值的时候被推测。
|
||||
|
||||
## 类型别名
|
||||
|
||||
*类型别名(type aliases)*就是给现有类型定义另一个名字。你可以使用 `typealias` 关键字来定义类型别名。
|
||||
|
||||
当你想要给现有类型起一个更有意义的名字时,类型别名非常有用。假设你正在处理特定长度的外部资源的数据:
|
||||
|
||||
```swift
|
||||
typealias AudioSample = UInt16
|
||||
```
|
||||
|
||||
定义了一个类型别名之后,你可以在任何使用原始名的地方使用别名:
|
||||
|
||||
```swift
|
||||
var maxAmplitudeFound = AudioSample.min
|
||||
// maxAmplitudeFound 现在是 0
|
||||
```
|
||||
|
||||
本例中,`AudioSample` 被定义为 `UInt16` 的一个别名。因为它是别名,`AudioSample.min` 实际上是 `UInt16.min`,所以会给 `maxAmplitudeFound` 赋一个初值 `0`。
|
||||
|
||||
## 布尔值
|
||||
|
||||
Swift 有一个基本的*布尔(Boolean)类型*,叫做 `Bool`。布尔值指*逻辑*上的值,因为它们只能是真或者假。Swift 有两个布尔常量,`true` 和 `false`:
|
||||
|
||||
```swift
|
||||
let orangesAreOrange = true
|
||||
let turnipsAreDelicious = false
|
||||
```
|
||||
|
||||
`orangesAreOrange` 和 `turnipsAreDelicious` 的类型会被推断为 `Bool`,因为它们的初值是布尔字面量。就像之前提到的 `Int` 和 `Double` 一样,如果你创建变量的时候给它们赋值 `true` 或者 `false`,那你不需要将常量或者变量声明为 `Bool` 类型。初始化常量或者变量的时候如果所赋的值类型已知,就可以触发类型推断,这让 Swift 代码更加简洁并且可读性更高。
|
||||
|
||||
当你编写条件语句比如 `if` 语句的时候,布尔值非常有用:
|
||||
|
||||
```swift
|
||||
if turnipsAreDelicious {
|
||||
print("Mmm, tasty turnips!")
|
||||
} else {
|
||||
print("Eww, turnips are horrible.")
|
||||
}
|
||||
// 输出“Eww, turnips are horrible.”
|
||||
```
|
||||
|
||||
条件语句,例如 `if`,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
如果你在需要使用 `Bool` 类型的地方使用了非布尔值,Swift 的类型安全机制会报错。下面的例子会报告一个编译时错误:
|
||||
|
||||
```swift
|
||||
let i = 1
|
||||
if i {
|
||||
// 这个例子不会通过编译,会报错
|
||||
}
|
||||
```
|
||||
|
||||
然而,下面的例子是合法的:
|
||||
|
||||
```swift
|
||||
let i = 1
|
||||
if i == 1 {
|
||||
// 这个例子会编译成功
|
||||
}
|
||||
```
|
||||
|
||||
`i == 1` 的比较结果是 `Bool` 类型,所以第二个例子可以通过类型检查。类似 `i == 1` 这样的比较,请参考 [基本操作符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
和 Swift 中的其他类型安全的例子一样,这个方法可以避免错误并保证这块代码的意图总是清晰的。
|
||||
|
||||
## 元组
|
||||
|
||||
*元组(tuples)*把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。
|
||||
|
||||
下面这个例子中,`(404, "Not Found")` 是一个描述 *HTTP 状态码(HTTP status code)*的元组。HTTP 状态码是当你请求网页的时候 web 服务器返回的一个特殊值。如果你请求的网页不存在就会返回一个 `404 Not Found` 状态码。
|
||||
|
||||
```swift
|
||||
let http404Error = (404, "Not Found")
|
||||
// http404Error 的类型是 (Int, String),值是 (404, "Not Found")
|
||||
```
|
||||
|
||||
`(404, "Not Found")` 元组把一个 `Int` 值和一个 `String` 值组合起来表示 HTTP 状态码的两个部分:一个数字和一个人类可读的描述。这个元组可以被描述为“一个类型为 `(Int, String)` 的元组”。
|
||||
|
||||
你可以把任意顺序的类型组合成一个元组,这个元组可以包含所有类型。只要你想,你可以创建一个类型为 `(Int, Int, Int)` 或者 `(String, Bool)` 或者其他任何你想要的组合的元组。
|
||||
|
||||
你可以将一个元组的内容分解(decompose)成单独的常量和变量,然后你就可以正常使用它们了:
|
||||
|
||||
```swift
|
||||
let (statusCode, statusMessage) = http404Error
|
||||
print("The status code is \(statusCode)")
|
||||
// 输出“The status code is 404”
|
||||
print("The status message is \(statusMessage)")
|
||||
// 输出“The status message is Not Found”
|
||||
```
|
||||
|
||||
如果你只需要一部分元组值,分解的时候可以把要忽略的部分用下划线(`_`)标记:
|
||||
|
||||
```swift
|
||||
let (justTheStatusCode, _) = http404Error
|
||||
print("The status code is \(justTheStatusCode)")
|
||||
// 输出“The status code is 404”
|
||||
```
|
||||
|
||||
此外,你还可以通过下标来访问元组中的单个元素,下标从零开始:
|
||||
|
||||
```swift
|
||||
print("The status code is \(http404Error.0)")
|
||||
// 输出“The status code is 404”
|
||||
print("The status message is \(http404Error.1)")
|
||||
// 输出“The status message is Not Found”
|
||||
```
|
||||
|
||||
你可以在定义元组的时候给单个元素命名:
|
||||
|
||||
```swift
|
||||
let http200Status = (statusCode: 200, description: "OK")
|
||||
```
|
||||
|
||||
给元组中的元素命名后,你可以通过名字来获取这些元素的值:
|
||||
|
||||
```swift
|
||||
print("The status code is \(http200Status.statusCode)")
|
||||
// 输出“The status code is 200”
|
||||
print("The status message is \(http200Status.description)")
|
||||
// 输出“The status message is OK”
|
||||
```
|
||||
|
||||
作为函数返回值时,元组非常有用。一个用来获取网页的函数可能会返回一个 `(Int, String)` 元组来描述是否获取成功。和只能返回一个类型的值比较起来,一个包含两个不同类型值的元组可以让函数的返回信息更有用。请参考 [函数参数与返回值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#Function_Parameters_and_Return_Values)。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 当遇到一些相关值的简单分组时,元组是很有用的。元组不适合用来创建复杂的数据结构。如果你的数据结构比较复杂,不要使用元组,用类或结构体去建模。欲获得更多信息,请参考 [结构体和类](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md)。
|
||||
|
||||
## 可选类型
|
||||
|
||||
使用*可选类型(optionals)*来处理值可能缺失的情况。可选类型表示两种可能: 或者有值, 你可以解析可选类型访问这个值, 或者根本没有值。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> C 和 Objective-C 中并没有可选类型这个概念。最接近的是 Objective-C 中的一个特性,一个方法要不返回一个对象要不返回 `nil`,`nil` 表示“缺少一个合法的对象”。然而,这只对对象起作用——对于结构体,基本的 C 类型或者枚举类型不起作用。对于这些类型,Objective-C 方法一般会返回一个特殊值(比如 `NSNotFound`)来暗示值缺失。这种方法假设方法的调用者知道并记得对特殊值进行判断。然而,Swift 的可选类型可以让你暗示*任意类型*的值缺失,并不需要一个特殊值。
|
||||
|
||||
来看一个例子。Swift 的 `Int` 类型有一种构造器,作用是将一个 `String` 值转换成一个 `Int` 值。然而,并不是所有的字符串都可以转换成一个整数。字符串 `"123"` 可以被转换成数字 `123` ,但是字符串 `"hello, world"` 不行。
|
||||
|
||||
下面的例子使用这种构造器来尝试将一个 `String` 转换成 `Int`:
|
||||
|
||||
```swift
|
||||
let possibleNumber = "123"
|
||||
let convertedNumber = Int(possibleNumber)
|
||||
// convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int"
|
||||
```
|
||||
|
||||
因为该构造器可能会失败,所以它返回一个*可选类型*(optional)`Int`,而不是一个 `Int`。一个可选的 `Int` 被写作 `Int?` 而不是 `Int`。问号暗示包含的值是可选类型,也就是说可能包含 `Int` 值也可能*不包含值*。(不能包含其他任何值比如 `Bool` 值或者 `String` 值。只能是 `Int` 或者什么都没有。)
|
||||
|
||||
### nil
|
||||
|
||||
你可以给可选变量赋值为 `nil` 来表示它没有值:
|
||||
|
||||
```swift
|
||||
var serverResponseCode: Int? = 404
|
||||
// serverResponseCode 包含一个可选的 Int 值 404
|
||||
serverResponseCode = nil
|
||||
// serverResponseCode 现在不包含值
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `nil` 不能用于非可选的常量和变量。如果你的代码中有常量或者变量需要处理值缺失的情况,请把它们声明成对应的可选类型。
|
||||
|
||||
如果你声明一个可选常量或者变量但是没有赋值,它们会自动被设置为 `nil`:
|
||||
|
||||
```swift
|
||||
var surveyAnswer: String?
|
||||
// surveyAnswer 被自动设置为 nil
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `nil` 和 Objective-C 中的 `nil` 并不一样。在 Objective-C 中,`nil` 是一个指向不存在对象的指针。在 Swift 中,`nil` 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 `nil`,不只是对象类型。
|
||||
|
||||
### if 语句以及强制解析
|
||||
|
||||
你可以使用 `if` 语句和 `nil` 比较来判断一个可选值是否包含值。你可以使用“相等”(`==`)或“不等”(`!=`)来执行比较。
|
||||
|
||||
如果可选类型有值,它将不等于 `nil`:
|
||||
|
||||
```swift
|
||||
if convertedNumber != nil {
|
||||
print("convertedNumber contains some integer value.")
|
||||
}
|
||||
// 输出“convertedNumber contains some integer value.”
|
||||
```
|
||||
|
||||
当你确定可选类型确实包含值之后,你可以在可选的名字后面加一个感叹号(`!`)来获取值。这个惊叹号表示“我知道这个可选有值,请使用它。”这被称为可选值的*强制解析(forced unwrapping)*:
|
||||
|
||||
```swift
|
||||
if convertedNumber != nil {
|
||||
print("convertedNumber has an integer value of \(convertedNumber!).")
|
||||
}
|
||||
// 输出“convertedNumber has an integer value of 123.”
|
||||
```
|
||||
|
||||
更多关于 `if` 语句的内容,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 使用 `!` 来获取一个不存在的可选值会导致运行时错误。使用 `!` 来强制解析值之前,一定要确定可选包含一个非 `nil` 的值。
|
||||
|
||||
### 可选绑定
|
||||
|
||||
使用*可选绑定(optional binding)*来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。可选绑定可以用在 `if` 和 `while` 语句中,这条语句不仅可以用来判断可选类型中是否有值,同时可以将可选类型中的值赋给一个常量或者变量。`if` 和 `while` 语句,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
像下面这样在 `if` 语句中写一个可选绑定:
|
||||
|
||||
```swift
|
||||
if let constantName = someOptional {
|
||||
statements
|
||||
}
|
||||
```
|
||||
|
||||
你可以像上面这样使用可选绑定来重写 在 [可选类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/01_The_Basics.html#optionals) 举出的 `possibleNumber` 例子:
|
||||
|
||||
```swift
|
||||
if let actualNumber = Int(possibleNumber) {
|
||||
print("\'\(possibleNumber)\' has an integer value of \(actualNumber)")
|
||||
} else {
|
||||
print("\'\(possibleNumber)\' could not be converted to an integer")
|
||||
}
|
||||
// 输出“'123' has an integer value of 123”
|
||||
```
|
||||
|
||||
这段代码可以被理解为:
|
||||
|
||||
“如果 `Int(possibleNumber)` 返回的可选 `Int` 包含一个值,创建一个叫做 `actualNumber` 的新常量并将可选包含的值赋给它。”
|
||||
|
||||
如果转换成功,`actualNumber` 常量可以在 `if` 语句的第一个分支中使用。它已经被可选类型 *包含的*值初始化过,所以不需要再使用 `!` 后缀来获取它的值。在这个例子中,`actualNumber` 只被用来输出转换结果。
|
||||
|
||||
你可以在可选绑定中使用常量和变量。如果你想在 `if` 语句的第一个分支中操作 `actualNumber` 的值,你可以改成 `if var actualNumber`,这样可选类型包含的值就会被赋给一个变量而非常量。
|
||||
|
||||
你可以包含多个可选绑定或多个布尔条件在一个 `if` 语句中,只要使用逗号分开就行。只要有任意一个可选绑定的值为 `nil`,或者任意一个布尔条件为 `false`,则整个 `if` 条件判断为 `false`,这时你就需要使用嵌套 `if` 条件语句来处理,如下所示:
|
||||
|
||||
```swift
|
||||
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
|
||||
print("\(firstNumber) < \(secondNumber) < 100")
|
||||
}
|
||||
// 输出“4 < 42 < 100”
|
||||
|
||||
if let firstNumber = Int("4") {
|
||||
if let secondNumber = Int("42") {
|
||||
if firstNumber < secondNumber && secondNumber < 100 {
|
||||
print("\(firstNumber) < \(secondNumber) < 100")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 输出“4 < 42 < 100”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 在 `if` 条件语句中使用常量和变量来创建一个可选绑定,仅在 `if` 语句的句中(`body`)中才能获取到值。相反,在 `guard` 语句中使用常量和变量来创建一个可选绑定,仅在 `guard` 语句外且在语句后才能获取到值,请参考 [提前退出](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#early_exit)。
|
||||
|
||||
### 隐式解析可选类型
|
||||
|
||||
如上所述,可选类型暗示了常量或者变量可以“没有值”。可选可以通过 `if` 语句来判断是否有值,如果有值的话可以通过可选绑定来解析值。
|
||||
|
||||
有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型*总会*有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。
|
||||
|
||||
这种类型的可选状态被定义为隐式解析可选类型(implicitly unwrapped optionals)。把想要用作可选的类型的后面的问号(`String?`)改成感叹号(`String!`)来声明一个隐式解析可选类型。
|
||||
|
||||
当可选类型被第一次赋值之后就可以确定之后一直有值的时候,隐式解析可选类型非常有用。隐式解析可选类型主要被用在 Swift 中类的构造过程中,请参考 [无主引用以及隐式解析可选属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html#unowned_references_and_implicitly_unwrapped_optional_properties)。
|
||||
|
||||
一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次都使用解析来获取可选值。下面的例子展示了可选类型 `String` 和隐式解析可选类型 `String` 之间的区别:
|
||||
|
||||
```swift
|
||||
let possibleString: String? = "An optional string."
|
||||
let forcedString: String = possibleString! // 需要感叹号来获取值
|
||||
|
||||
let assumedString: String! = "An implicitly unwrapped optional string."
|
||||
let implicitString: String = assumedString // 不需要感叹号
|
||||
```
|
||||
|
||||
你可以把隐式解析可选类型当做一个可以自动解析的可选类型。你要做的只是声明的时候把感叹号放到类型的结尾,而不是每次取值的可选名字的结尾。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。和你在没有值的普通可选类型后面加一个惊叹号一样。
|
||||
|
||||
你仍然可以把隐式解析可选类型当做普通可选类型来判断它是否包含值:
|
||||
|
||||
```swift
|
||||
if assumedString != nil {
|
||||
print(assumedString!)
|
||||
}
|
||||
// 输出“An implicitly unwrapped optional string.”
|
||||
```
|
||||
|
||||
你也可以在可选绑定中使用隐式解析可选类型来检查并解析它的值:
|
||||
|
||||
```swift
|
||||
if let definiteString = assumedString {
|
||||
print(definiteString)
|
||||
}
|
||||
// 输出“An implicitly unwrapped optional string.”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果一个变量之后可能变成 `nil` 的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是 `nil` 的话,请使用普通可选类型。
|
||||
|
||||
## 错误处理
|
||||
|
||||
你可以使用 *错误处理(error handling)* 来应对程序执行中可能会遇到的错误条件。
|
||||
|
||||
相对于可选中运用值的存在与缺失来表达函数的成功与失败,错误处理可以推断失败的原因,并传播至程序的其他部分。
|
||||
|
||||
当一个函数遇到错误条件,它能报错。调用函数的地方能抛出错误消息并合理处理。
|
||||
|
||||
```swift
|
||||
func canThrowAnError() throws {
|
||||
// 这个函数有可能抛出错误
|
||||
}
|
||||
```
|
||||
|
||||
一个函数可以通过在声明中添加 `throws` 关键词来抛出错误消息。当你的函数能抛出错误消息时,你应该在表达式中前置 `try` 关键词。
|
||||
|
||||
```swift
|
||||
do {
|
||||
try canThrowAnError()
|
||||
// 没有错误消息抛出
|
||||
} catch {
|
||||
// 有一个错误消息抛出
|
||||
}
|
||||
```
|
||||
|
||||
一个 `do` 语句创建了一个新的包含作用域,使得错误能被传播到一个或多个 `catch` 从句。
|
||||
|
||||
这里有一个错误处理如何用来应对不同错误条件的例子。
|
||||
|
||||
```swift
|
||||
func makeASandwich() throws {
|
||||
// ...
|
||||
}
|
||||
|
||||
do {
|
||||
try makeASandwich()
|
||||
eatASandwich()
|
||||
} catch SandwichError.outOfCleanDishes {
|
||||
washDishes()
|
||||
} catch SandwichError.missingIngredients(let ingredients) {
|
||||
buyGroceries(ingredients)
|
||||
}
|
||||
```
|
||||
|
||||
在此例中,`makeASandwich()`(做一个三明治)函数会抛出一个错误消息如果没有干净的盘子或者某个原料缺失。因为 `makeASandwich()` 抛出错误,函数调用被包裹在 `try` 表达式中。将函数包裹在一个 `do` 语句中,任何被抛出的错误会被传播到提供的 `catch` 从句中。
|
||||
|
||||
如果没有错误被抛出,`eatASandwich()` 函数会被调用。如果一个匹配 `SandwichError.outOfCleanDishes` 的错误被抛出,`washDishes()` 函数会被调用。如果一个匹配 `SandwichError.missingIngredients` 的错误被抛出,`buyGroceries(_:)` 函数会被调用,并且使用 `catch` 所捕捉到的关联值 `[String]` 作为参数。
|
||||
|
||||
抛出,捕捉,以及传播错误会在 [错误处理](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/17_Error_Handling.html) 章节详细说明。
|
||||
|
||||
## 断言和先决条件
|
||||
|
||||
断言和先决条件是在运行时所做的检查。你可以用他们来检查在执行后续代码之前是否一个必要的条件已经被满足了。如果断言或者先决条件中的布尔条件评估的结果为 true(真),则代码像往常一样继续执行。如果布尔条件评估结果为 false(假),程序的当前状态是无效的,则代码执行结束,应用程序中止。
|
||||
|
||||
你使用断言和先决条件来表达你所做的假设和你在编码时候的期望。你可以将这些包含在你的代码中。断言帮助你在开发阶段找到错误和不正确的假设,先决条件帮助你在生产环境中探测到存在的问题。
|
||||
|
||||
除了在运行时验证你的期望值,断言和先决条件也变成了一个在你的代码中的有用的文档形式。和在上面讨论过的 [错误处理](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/17_Error_Handling.html) 不同,断言和先决条件并不是用来处理可以恢复的或者可预期的错误。因为一个断言失败表明了程序正处于一个无效的状态,没有办法去捕获一个失败的断言。
|
||||
|
||||
使用断言和先决条件不是一个能够避免出现程序出现无效状态的编码方法。然而,如果一个无效状态程序产生了,断言和先决条件可以强制检查你的数据和程序状态,使得你的程序可预测的中止(译者:不是系统强制的,被动的中止),并帮助使这个问题更容易调试。一旦探测到无效的状态,执行则被中止,防止无效的状态导致的进一步对于系统的伤害。
|
||||
|
||||
断言和先决条件的不同点是,他们什么时候进行状态检测:断言仅在调试环境运行,而先决条件则在调试环境和生产环境中运行。在生产环境中,断言的条件将不会进行评估。这个意味着你可以使用很多断言在你的开发阶段,但是这些断言在生产环境中不会产生任何影响。
|
||||
|
||||
### 使用断言进行调试
|
||||
|
||||
你可以调用 Swift 标准库的 `assert(_:_:file:line:)` 函数来写一个断言。向这个函数传入一个结果为 `true` 或者 `false` 的表达式以及一条信息,当表达式的结果为 `false` 的时候这条信息会被显示:
|
||||
|
||||
```swift
|
||||
let age = -3
|
||||
assert(age >= 0, "A person's age cannot be less than zero")
|
||||
// 因为 age < 0,所以断言会触发
|
||||
```
|
||||
|
||||
在这个例子中,只有 `age >= 0` 为 `true` 时,即 `age` 的值非负的时候,代码才会继续执行。如果 `age` 的值是负数,就像代码中那样,`age >= 0` 为 `false`,断言被触发,终止应用。
|
||||
|
||||
如果不需要断言信息,可以就像这样忽略掉:
|
||||
|
||||
```swift
|
||||
assert(age >= 0)
|
||||
```
|
||||
|
||||
如果代码已经检查了条件,你可以使用 `assertionFailure(_:file:line:)` 函数来表明断言失败了,例如:
|
||||
|
||||
```swift
|
||||
if age > 10 {
|
||||
print("You can ride the roller-coaster or the ferris wheel.")
|
||||
} else if age > 0 {
|
||||
print("You can ride the ferris wheel.")
|
||||
} else {
|
||||
assertionFailure("A person's age can't be less than zero.")
|
||||
}
|
||||
```
|
||||
|
||||
### 强制执行先决条件
|
||||
|
||||
当一个条件可能为假,但是继续执行代码要求条件必须为真的时候,需要使用先决条件。例如使用先决条件来检查是否下标越界,或者来检查是否将一个正确的参数传给函数。
|
||||
|
||||
你可以使用全局 `precondition(_:_:file:line:)` 函数来写一个先决条件。向这个函数传入一个结果为 `true` 或者 `false` 的表达式以及一条信息,当表达式的结果为 `false` 的时候这条信息会被显示:
|
||||
|
||||
```swift
|
||||
// 在一个下标的实现里...
|
||||
precondition(index > 0, "Index must be greater than zero.")
|
||||
```
|
||||
|
||||
你可以调用 `preconditionFailure(_:file:line:)` 方法来表明出现了一个错误,例如,switch 进入了 default 分支,但是所有的有效值应该被任意一个其他分支(非 default 分支)处理。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你使用 unchecked 模式(-Ounchecked)编译代码,先决条件将不会进行检查。编译器假设所有的先决条件总是为 true(真),他将优化你的代码。然而,`fatalError(_:file:line:)` 函数总是中断执行,无论你怎么进行优化设定。
|
||||
>
|
||||
> 你能使用 `fatalError(_:file:line:)` 函数在设计原型和早期开发阶段,这个阶段只有方法的声明,但是没有具体实现,你可以在方法体中写上 fatalError("Unimplemented")作为具体实现。因为 fatalError 不会像断言和先决条件那样被优化掉,所以你可以确保当代码执行到一个没有被实现的方法时,程序会被中断。
|
||||
483
IOS/Task00:Swift基础语法学习/2.基本运算符.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# 基本运算符
|
||||
|
||||
*运算符*是检查、改变、合并值的特殊符号或短语。例如,加号(`+`)将两个数相加(如 `let i = 1 + 2`)。更复杂的运算例子包括逻辑与运算符 `&&`(如 `if enteredDoorCode && passedRetinaScan`)。
|
||||
|
||||
Swift 支持大部分标准 C 语言的运算符,且为了减少常见编码错误做了部分改进。如:赋值符(`=`)不再有返回值,这样就消除了手误将判等运算符(`==`)写成赋值符导致代码错误的缺陷。算术运算符(`+`,`-`,`*`,`/`,`%` 等)的结果会被检测并禁止值溢出,以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果。当然允许你使用 Swift 的溢出运算符来实现溢出。详情参见 [溢出运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#overflow_operators)。
|
||||
|
||||
Swift 还提供了 C 语言没有的区间运算符,例如 `a..<b` 或 `a...b`,这方便我们表达一个区间内的数值。
|
||||
|
||||
本章节只描述了 Swift 中的基本运算符,[高级运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html) 这章会包含 Swift 中的高级运算符,及如何自定义运算符,及如何进行自定义类型的运算符重载。
|
||||
|
||||
## 术语
|
||||
|
||||
运算符分为一元、二元和三元运算符:
|
||||
|
||||
- *一元*运算符对单一操作对象操作(如 `-a`)。一元运算符分前置运算符和后置运算符,*前置运算符*需紧跟在操作对象之前(如 `!b`),*后置运算符*需紧跟在操作对象之后(如 `c!`)。
|
||||
- *二元*运算符操作两个操作对象(如 `2 + 3`),是*中置*的,因为它们出现在两个操作对象之间。
|
||||
- *三元*运算符操作三个操作对象,和 C 语言一样,Swift 只有一个三元运算符,就是三目运算符(`a ? b : c`)。
|
||||
|
||||
受运算符影响的值叫*操作数*,在表达式 `1 + 2` 中,加号 `+` 是二元运算符,它的两个操作数是值 `1` 和 `2`。
|
||||
|
||||
## 赋值运算符
|
||||
|
||||
*赋值运算符*(`a = b`),表示用 `b` 的值来初始化或更新 `a` 的值:
|
||||
|
||||
```swift
|
||||
let b = 10
|
||||
var a = 5
|
||||
a = b
|
||||
// a 现在等于 10
|
||||
```
|
||||
|
||||
如果赋值的右边是一个多元组,它的元素可以马上被分解成多个常量或变量:
|
||||
|
||||
```swift
|
||||
let (x, y) = (1, 2)
|
||||
// 现在 x 等于 1,y 等于 2
|
||||
```
|
||||
|
||||
与 C 语言和 Objective-C 不同,Swift 的赋值操作并不返回任何值。所以下面语句是无效的:
|
||||
|
||||
```swift
|
||||
if x = y {
|
||||
// 此句错误,因为 x = y 并不返回任何值
|
||||
}
|
||||
```
|
||||
|
||||
通过将 `if x = y` 标记为无效语句,Swift 能帮你避免把 (`==`)错写成(`=`)这类错误的出现。
|
||||
|
||||
## 算术运算符
|
||||
|
||||
Swift 中所有数值类型都支持了基本的四则*算术运算符*:
|
||||
|
||||
- 加法(`+`)
|
||||
- 减法(`-`)
|
||||
- 乘法(`*`)
|
||||
- 除法(`/`)
|
||||
|
||||
```swift
|
||||
1 + 2 // 等于 3
|
||||
5 - 3 // 等于 2
|
||||
2 * 3 // 等于 6
|
||||
10.0 / 2.5 // 等于 4.0
|
||||
```
|
||||
|
||||
与 C 语言和 Objective-C 不同的是,Swift 默认情况下不允许在数值运算中出现溢出情况。但是你可以使用 Swift 的溢出运算符来实现溢出运算(如 `a &+ b`)。详情参见 [溢出运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#overflow_operators)。
|
||||
|
||||
加法运算符也可用于 `String` 的拼接:
|
||||
|
||||
```swift
|
||||
"hello, " + "world" // 等于 "hello, world"
|
||||
```
|
||||
|
||||
### 求余运算符
|
||||
|
||||
*求余运算符*(`a % b`)是计算 `b` 的多少倍刚刚好可以容入 `a`,返回多出来的那部分(余数)。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 求余运算符(`%`)在其他语言也叫*取模运算符*。但是严格说来,我们看该运算符对负数的操作结果,「求余」比「取模」更合适些。
|
||||
|
||||
我们来谈谈取余是怎么回事,计算 `9 % 4`,你先计算出 `4` 的多少倍会刚好可以容入 `9` 中:
|
||||
|
||||

|
||||
|
||||
你可以在 `9` 中放入两个 `4`,那余数是 1(用橙色标出)。
|
||||
|
||||
在 Swift 中可以表达为:
|
||||
|
||||
```swift
|
||||
9 % 4 // 等于 1
|
||||
```
|
||||
|
||||
为了得到 `a % b` 的结果,`%` 计算了以下等式,并输出 `余数`作为结果:
|
||||
|
||||
```
|
||||
a = (b × 倍数) + 余数
|
||||
```
|
||||
|
||||
当 `倍数`取最大值的时候,就会刚好可以容入 `a` 中。
|
||||
|
||||
把 `9` 和 `4` 代入等式中,我们得 `1`:
|
||||
|
||||
```
|
||||
9 = (4 × 2) + 1
|
||||
```
|
||||
|
||||
同样的方法,我们来计算 `-9 % 4`:
|
||||
|
||||
```swift
|
||||
-9 % 4 // 等于 -1
|
||||
```
|
||||
|
||||
把 `-9` 和 `4` 代入等式,`-2` 是取到的最大整数:
|
||||
|
||||
```
|
||||
-9 = (4 × -2) + -1
|
||||
```
|
||||
|
||||
余数是 `-1`。
|
||||
|
||||
在对负数 `b` 求余时,`b` 的符号会被忽略。这意味着 `a % b` 和 `a % -b` 的结果是相同的。
|
||||
|
||||
### 一元负号运算符
|
||||
|
||||
数值的正负号可以使用前缀 `-`(即*一元负号符*)来切换:
|
||||
|
||||
```swift
|
||||
let three = 3
|
||||
let minusThree = -three // minusThree 等于 -3
|
||||
let plusThree = -minusThree // plusThree 等于 3, 或 "负负3"
|
||||
```
|
||||
|
||||
一元负号符(`-`)写在操作数之前,中间没有空格。
|
||||
|
||||
### 一元正号运算符
|
||||
|
||||
*一元正号符*(`+`)不做任何改变地返回操作数的值:
|
||||
|
||||
```swift
|
||||
let minusSix = -6
|
||||
let alsoMinusSix = +minusSix // alsoMinusSix 等于 -6
|
||||
```
|
||||
|
||||
虽然一元正号符什么都不会改变,但当你在使用一元负号来表达负数时,你可以使用一元正号来表达正数,如此你的代码会具有对称美。
|
||||
|
||||
## 组合赋值运算符
|
||||
|
||||
如同 C 语言,Swift 也提供把其他运算符和赋值运算(`=`)组合的*组合赋值运算符*,组合加运算(`+=`)是其中一个例子:
|
||||
|
||||
```swift
|
||||
var a = 1
|
||||
a += 2
|
||||
// a 现在是 3
|
||||
```
|
||||
|
||||
表达式 `a += 2` 是 `a = a + 2` 的简写,一个组合加运算就是把加法运算和赋值运算组合成进一个运算符里,同时完成两个运算任务。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 复合赋值运算没有返回值,`let b = a += 2` 这类代码是错误。这不同于上面提到的自增和自减运算符。
|
||||
|
||||
更多 Swift 标准库运算符的信息,请看 [运算符声明](https://developer.apple.com/documentation/swift/operator_declarations)。
|
||||
|
||||
## 比较运算符(Comparison Operators)
|
||||
|
||||
所有标准 C 语言中的*比较运算符*都可以在 Swift 中使用:
|
||||
|
||||
- 等于(`a == b`)
|
||||
- 不等于(`a != b`)
|
||||
- 大于(`a > b`)
|
||||
- 小于(`a < b`)
|
||||
- 大于等于(`a >= b`)
|
||||
- 小于等于(`a <= b`)
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 也提供恒等(`===`)和不恒等(`!==`)这两个比较符来判断两个对象是否引用同一个对象实例。更多细节在 [类与结构](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md) 章节的 **Identity Operators** 部分。
|
||||
|
||||
每个比较运算都返回了一个标识表达式是否成立的布尔值:
|
||||
|
||||
```swift
|
||||
1 == 1 // true, 因为 1 等于 1
|
||||
2 != 1 // true, 因为 2 不等于 1
|
||||
2 > 1 // true, 因为 2 大于 1
|
||||
1 < 2 // true, 因为 1 小于2
|
||||
1 >= 1 // true, 因为 1 大于等于 1
|
||||
2 <= 1 // false, 因为 2 并不小于等于 1
|
||||
```
|
||||
|
||||
比较运算多用于条件语句,如 `if` 条件:
|
||||
|
||||
```swift
|
||||
let name = "world"
|
||||
if name == "world" {
|
||||
print("hello, world")
|
||||
} else {
|
||||
print("I'm sorry \(name), but I don't recognize you")
|
||||
}
|
||||
// 输出“hello, world", 因为 `name` 就是等于 "world”
|
||||
```
|
||||
|
||||
关于 `if` 语句,请看 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
如果两个元组的元素相同,且长度相同的话,元组就可以被比较。比较元组大小会按照从左到右、逐值比较的方式,直到发现有两个值不等时停止。如果所有的值都相等,那么这一对元组我们就称它们是相等的。例如:
|
||||
|
||||
```swift
|
||||
(1, "zebra") < (2, "apple") // true,因为 1 小于 2
|
||||
(3, "apple") < (3, "bird") // true,因为 3 等于 3,但是 apple 小于 bird
|
||||
(4, "dog") == (4, "dog") // true,因为 4 等于 4,dog 等于 dog
|
||||
```
|
||||
|
||||
在上面的例子中,你可以看到,在第一行中从左到右的比较行为。因为 `1` 小于 `2`,所以 `(1, "zebra")` 小于 `(2, "apple")`,不管元组剩下的值如何。所以 `"zebra"` 大于 `"apple"` 对结果没有任何影响,因为元组的比较结果已经被第一个元素决定了。不过,当元组的第一个元素相同时候,第二个元素将会用作比较-第二行和第三行代码就发生了这样的比较。
|
||||
|
||||
当元组中的元素都可以被比较时,你也可以使用这些运算符来比较它们的大小。例如,像下面展示的代码,你可以比较两个类型为 `(String, Int)` 的元组,因为 `Int` 和 `String` 类型的值可以比较。相反,`Bool` 不能被比较,也意味着存有布尔类型的元组不能被比较。
|
||||
|
||||
```swift
|
||||
("blue", -1) < ("purple", 1) // 正常,比较的结果为 true
|
||||
("blue", false) < ("purple", true) // 错误,因为 < 不能比较布尔类型
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 标准库只能比较七个以内元素的元组比较函数。如果你的元组元素超过七个时,你需要自己实现比较运算符。
|
||||
|
||||
## 三元运算符(Ternary Conditional Operator)
|
||||
|
||||
*三元运算符*的特殊在于它是有三个操作数的运算符,它的形式是 `问题 ? 答案 1 : 答案 2`。它简洁地表达根据 `问题`成立与否作出二选一的操作。如果 `问题` 成立,返回 `答案 1` 的结果;反之返回 `答案 2`的结果。
|
||||
|
||||
三元运算符是以下代码的缩写形式:
|
||||
|
||||
```swift
|
||||
if question {
|
||||
answer1
|
||||
} else {
|
||||
answer2
|
||||
}
|
||||
```
|
||||
|
||||
这里有个计算表格行高的例子。如果有表头,那行高应比内容高度要高出 50 点;如果没有表头,只需高出 20 点:
|
||||
|
||||
```swift
|
||||
let contentHeight = 40
|
||||
let hasHeader = true
|
||||
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
|
||||
// rowHeight 现在是 90
|
||||
```
|
||||
|
||||
上面的写法比下面的代码更简洁:
|
||||
|
||||
```swift
|
||||
let contentHeight = 40
|
||||
let hasHeader = true
|
||||
var rowHeight = contentHeight
|
||||
if hasHeader {
|
||||
rowHeight = rowHeight + 50
|
||||
} else {
|
||||
rowHeight = rowHeight + 20
|
||||
}
|
||||
// rowHeight 现在是 90
|
||||
```
|
||||
|
||||
第一段代码例子使用了三元运算,所以一行代码就能让我们得到正确答案。这比第二段代码简洁得多,无需将 `rowHeight` 定义成变量,因为它的值无需在 `if` 语句中改变。
|
||||
|
||||
三元运算为二选一场景提供了一个非常便捷的表达形式。不过需要注意的是,滥用三元运算符会降低代码可读性。所以我们应避免在一个复合语句中使用多个三元运算符。
|
||||
|
||||
## 空合运算符(Nil Coalescing Operator)
|
||||
|
||||
*空合运算符*(`a ?? b`)将对可选类型 `a` 进行空判断,如果 `a` 包含一个值就进行解包,否则就返回一个默认值 `b`。表达式 `a` 必须是 Optional 类型。默认值 `b` 的类型必须要和 `a` 存储值的类型保持一致。
|
||||
|
||||
空合运算符是对以下代码的简短表达方法:
|
||||
|
||||
```swift
|
||||
a != nil ? a! : b
|
||||
```
|
||||
|
||||
上述代码使用了三元运算符。当可选类型 `a` 的值不为空时,进行强制解封(`a!`),访问 `a` 中的值;反之返回默认值 `b`。无疑空合运算符(`??`)提供了一种更为优雅的方式去封装条件判断和解封两种行为,显得简洁以及更具可读性。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果 `a` 为非空值(`non-nil`),那么值 `b` 将不会被计算。这也就是所谓的*短路求值*。
|
||||
|
||||
下文例子采用空合运算符,实现了在默认颜色名和可选自定义颜色名之间抉择:
|
||||
|
||||
```swift
|
||||
let defaultColorName = "red"
|
||||
var userDefinedColorName: String? //默认值为 nil
|
||||
|
||||
var colorNameToUse = userDefinedColorName ?? defaultColorName
|
||||
// userDefinedColorName 的值为空,所以 colorNameToUse 的值为 "red"
|
||||
```
|
||||
|
||||
`userDefinedColorName` 变量被定义为一个可选的 `String` 类型,默认值为 `nil`。由于 `userDefinedColorName` 是一个可选类型,我们可以使用空合运算符去判断其值。在上一个例子中,通过空合运算符为一个名为 `colorNameToUse` 的变量赋予一个字符串类型初始值。 由于 `userDefinedColorName` 值为空,因此表达式 `userDefinedColorName ?? defaultColorName` 返回 `defaultColorName` 的值,即 `red`。
|
||||
|
||||
如果你分配一个非空值(`non-nil`)给 `userDefinedColorName`,再次执行空合运算,运算结果为封包在 `userDefaultColorName` 中的值,而非默认值。
|
||||
|
||||
```swift
|
||||
userDefinedColorName = "green"
|
||||
colorNameToUse = userDefinedColorName ?? defaultColorName
|
||||
// userDefinedColorName 非空,因此 colorNameToUse 的值为 "green"
|
||||
```
|
||||
|
||||
## 区间运算符(Range Operators)
|
||||
|
||||
Swift 提供了几种方便表达一个区间的值的*区间运算符*。
|
||||
|
||||
### 闭区间运算符
|
||||
|
||||
*闭区间运算符*(`a...b`)定义一个包含从 `a` 到 `b`(包括 `a` 和 `b`)的所有值的区间。`a` 的值不能超过 `b`。
|
||||
|
||||
闭区间运算符在迭代一个区间的所有值时是非常有用的,如在 `for-in` 循环中:
|
||||
|
||||
```swift
|
||||
for index in 1...5 {
|
||||
print("\(index) * 5 = \(index * 5)")
|
||||
}
|
||||
// 1 * 5 = 5
|
||||
// 2 * 5 = 10
|
||||
// 3 * 5 = 15
|
||||
// 4 * 5 = 20
|
||||
// 5 * 5 = 25
|
||||
```
|
||||
|
||||
关于 `for-in` 循环,请看 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。
|
||||
|
||||
### 半开区间运算符
|
||||
|
||||
*半开区间运算符*(`a..<b`)定义一个从 `a` 到 `b` 但不包括 `b` 的区间。 之所以称为*半开区间*,是因为该区间包含第一个值而不包括最后的值。
|
||||
|
||||
半开区间的实用性在于当你使用一个从 0 开始的列表(如数组)时,非常方便地从0数到列表的长度。
|
||||
|
||||
```swift
|
||||
let names = ["Anna", "Alex", "Brian", "Jack"]
|
||||
let count = names.count
|
||||
for i in 0..<count {
|
||||
print("第 \(i + 1) 个人叫 \(names[i])")
|
||||
}
|
||||
// 第 1 个人叫 Anna
|
||||
// 第 2 个人叫 Alex
|
||||
// 第 3 个人叫 Brian
|
||||
// 第 4 个人叫 Jack
|
||||
```
|
||||
|
||||
数组有 4 个元素,但 `0..<count` 只数到3(最后一个元素的下标),因为它是半开区间。关于数组,请查阅 [数组](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/04_Collection_Types.html#arrays)。
|
||||
|
||||
### 单侧区间
|
||||
|
||||
闭区间操作符有另一个表达形式,可以表达往一侧无限延伸的区间 —— 例如,一个包含了数组从索引 2 到结尾的所有值的区间。在这些情况下,你可以省略掉区间操作符一侧的值。这种区间叫做单侧区间,因为操作符只有一侧有值。例如:
|
||||
|
||||
```swift
|
||||
for name in names[2...] {
|
||||
print(name)
|
||||
}
|
||||
// Brian
|
||||
// Jack
|
||||
|
||||
for name in names[...2] {
|
||||
print(name)
|
||||
}
|
||||
// Anna
|
||||
// Alex
|
||||
// Brian
|
||||
```
|
||||
|
||||
半开区间操作符也有单侧表达形式,附带上它的最终值。就像你使用区间去包含一个值,最终值并不会落在区间内。例如:
|
||||
|
||||
```swift
|
||||
for name in names[..<2] {
|
||||
print(name)
|
||||
}
|
||||
// Anna
|
||||
// Alex
|
||||
```
|
||||
|
||||
单侧区间不止可以在下标里使用,也可以在别的情境下使用。你不能遍历省略了初始值的单侧区间,因为遍历的开端并不明显。你可以遍历一个省略最终值的单侧区间;然而,由于这种区间无限延伸的特性,请保证你在循环里有一个结束循环的分支。你也可以查看一个单侧区间是否包含某个特定的值,就像下面展示的那样。
|
||||
|
||||
```swift
|
||||
let range = ...5
|
||||
range.contains(7) // false
|
||||
range.contains(4) // true
|
||||
range.contains(-1) // true
|
||||
```
|
||||
|
||||
## 逻辑运算符(Logical Operators)
|
||||
|
||||
*逻辑运算符*的操作对象是逻辑布尔值。Swift 支持基于 C 语言的三个标准逻辑运算。
|
||||
|
||||
- 逻辑非(`!a`)
|
||||
- 逻辑与(`a && b`)
|
||||
- 逻辑或(`a || b`)
|
||||
|
||||
### 逻辑非运算符
|
||||
|
||||
*逻辑非运算符*(`!a`)对一个布尔值取反,使得 `true` 变 `false`,`false` 变 `true`。
|
||||
|
||||
它是一个前置运算符,需紧跟在操作数之前,且不加空格。读作 `非 a` ,例子如下:
|
||||
|
||||
```swift
|
||||
let allowedEntry = false
|
||||
if !allowedEntry {
|
||||
print("ACCESS DENIED")
|
||||
}
|
||||
// 输出“ACCESS DENIED”
|
||||
```
|
||||
|
||||
`if !allowedEntry` 语句可以读作「如果非 allowedEntry」,接下一行代码只有在「非 allowedEntry」为 `true`,即 `allowEntry` 为 `false` 时被执行。
|
||||
|
||||
在示例代码中,小心地选择布尔常量或变量有助于代码的可读性,并且避免使用双重逻辑非运算,或混乱的逻辑语句。
|
||||
|
||||
### 逻辑与运算符 #{logical_and_operator}
|
||||
|
||||
*逻辑与运算符*(`a && b`)表达了只有 `a` 和 `b` 的值都为 `true` 时,整个表达式的值才会是 `true`。
|
||||
|
||||
只要任意一个值为 `false`,整个表达式的值就为 `false`。事实上,如果第一个值为 `false`,那么是不去计算第二个值的,因为它已经不可能影响整个表达式的结果了。这被称做*短路计算(short-circuit evaluation)*。
|
||||
|
||||
以下例子,只有两个 `Bool` 值都为 `true` 的时候才允许进入 if:
|
||||
|
||||
```swift
|
||||
let enteredDoorCode = true
|
||||
let passedRetinaScan = false
|
||||
if enteredDoorCode && passedRetinaScan {
|
||||
print("Welcome!")
|
||||
} else {
|
||||
print("ACCESS DENIED")
|
||||
}
|
||||
// 输出“ACCESS DENIED”
|
||||
```
|
||||
|
||||
### 逻辑或运算符 #{logical_or_operator}
|
||||
|
||||
逻辑或运算符(`a || b`)是一个由两个连续的 `|` 组成的中置运算符。它表示了两个逻辑表达式的其中一个为 `true`,整个表达式就为 `true`。
|
||||
|
||||
同逻辑与运算符类似,逻辑或也是「短路计算」的,当左端的表达式为 `true` 时,将不计算右边的表达式了,因为它不可能改变整个表达式的值了。
|
||||
|
||||
以下示例代码中,第一个布尔值(`hasDoorKey`)为 `false`,但第二个值(`knowsOverridePassword`)为 `true`,所以整个表达是 `true`,于是允许进入:
|
||||
|
||||
```swift
|
||||
let hasDoorKey = false
|
||||
let knowsOverridePassword = true
|
||||
if hasDoorKey || knowsOverridePassword {
|
||||
print("Welcome!")
|
||||
} else {
|
||||
print("ACCESS DENIED")
|
||||
}
|
||||
// 输出“Welcome!”
|
||||
```
|
||||
|
||||
### 逻辑运算符组合计算
|
||||
|
||||
我们可以组合多个逻辑运算符来表达一个复合逻辑:
|
||||
|
||||
```swift
|
||||
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
|
||||
print("Welcome!")
|
||||
} else {
|
||||
print("ACCESS DENIED")
|
||||
}
|
||||
// 输出“Welcome!”
|
||||
```
|
||||
|
||||
这个例子使用了含多个 `&&` 和 `||` 的复合逻辑。但无论怎样,`&&` 和 `||` 始终只能操作两个值。所以这实际是三个简单逻辑连续操作的结果。我们来解读一下:
|
||||
|
||||
如果我们输入了正确的密码并通过了视网膜扫描,或者我们有一把有效的钥匙,又或者我们知道紧急情况下重置的密码,我们就能把门打开进入。
|
||||
|
||||
前两种情况,我们都不满足,所以前两个简单逻辑的结果是 `false`,但是我们是知道紧急情况下重置的密码的,所以整个复杂表达式的值还是 `true`。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 逻辑操作符 `&&` 和 `||` 是左结合的,这意味着拥有多元逻辑操作符的复合表达式优先计算最左边的子表达式。
|
||||
|
||||
### 使用括号来明确优先级
|
||||
|
||||
为了一个复杂表达式更容易读懂,在合适的地方使用括号来明确优先级是很有效的,虽然它并非必要的。在上个关于门的权限的例子中,我们给第一个部分加个括号,使它看起来逻辑更明确:
|
||||
|
||||
```swift
|
||||
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
|
||||
print("Welcome!")
|
||||
} else {
|
||||
print("ACCESS DENIED")
|
||||
}
|
||||
// 输出“Welcome!”
|
||||
```
|
||||
|
||||
这括号使得前两个值被看成整个逻辑表达中独立的一个部分。虽然有括号和没括号的输出结果是一样的,但对于读代码的人来说有括号的代码更清晰。可读性比简洁性更重要,请在可以让你代码变清晰的地方加个括号吧!
|
||||
655
IOS/Task00:Swift基础语法学习/3.字符串和字符.md
Normal file
@@ -0,0 +1,655 @@
|
||||
# 字符串和字符
|
||||
|
||||
*字符串*是是一系列字符的集合,例如 `"hello, world"`,`"albatross"`。Swift 的字符串通过 `String`类型来表示。而 `String` 内容的访问方式有多种,例如以 `Character` 值的集合。
|
||||
|
||||
Swift 的 `String` 和 `Character` 类型提供了一种快速且兼容 Unicode 的方式来处理代码中的文本内容。创建和操作字符串的语法与 C 语言中字符串操作相似,轻量并且易读。通过 `+` 符号就可以非常简单的实现两个字符串的拼接操作。与 Swift 中其他值一样,能否更改字符串的值,取决于其被定义为常量还是变量。你可以在已有字符串中插入常量、变量、字面量和表达式从而形成更长的字符串,这一过程也被成为字符串插值。尤其是在为显示、存储和打印创建自定义字符串值时,字符串插值操作尤其有用。
|
||||
|
||||
尽管语法简易,但 Swift 中的 `String` 类型的实现却很快速和现代化。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `String` 类型与 Foundation `NSString` 类进行了无缝桥接。Foundation 还对 `String` 进行扩展使其可以访问 `NSString` 类型中定义的方法。这意味着调用那些 `NSString` 的方法,你无需进行任何类型转换。
|
||||
>
|
||||
> 更多关于在 Foundation 和 Cocoa 中使用 `String` 的信息请查看 *[Bridging Between String and NSString](https://developer.apple.com/documentation/swift/string#2919514)*。
|
||||
|
||||
## 字符串字面量
|
||||
|
||||
你可以在代码里使用一段预定义的字符串值作为字符串字面量。字符串字面量是由一对双引号包裹着的具有固定顺序的字符集。
|
||||
|
||||
字符串字面量可以用于为常量和变量提供初始值:
|
||||
|
||||
```swift
|
||||
let someString = "Some string literal value"
|
||||
```
|
||||
|
||||
注意,Swift 之所以推断 `someString` 常量为字符串类型,是因为它使用了字面量方式进行初始化。
|
||||
|
||||
### 多行字符串字面量
|
||||
|
||||
如果你需要一个字符串是跨越多行的,那就使用多行字符串字面量 — 由一对三个双引号包裹着的具有固定顺序的文本字符集:
|
||||
|
||||
```swift
|
||||
let quotation = """
|
||||
The White Rabbit put on his spectacles. "Where shall I begin,
|
||||
please your Majesty?" he asked.
|
||||
|
||||
"Begin at the beginning," the King said gravely, "and go on
|
||||
till you come to the end; then stop."
|
||||
"""
|
||||
```
|
||||
|
||||
一个多行字符串字面量包含了所有的在开启和关闭引号(`"""`)中的行。这个字符从开启引号(`"""`)之后的第一行开始,到关闭引号(`"""`)之前为止。这就意味着字符串开启引号之后(`"""`)或者结束引号(`"""`)之前都没有换行符号。(译者:下面两个字符串其实是一样的,虽然第二个使用了多行字符串的形式)
|
||||
|
||||
```swift
|
||||
let singleLineString = "These are the same."
|
||||
let multilineString = """
|
||||
These are the same.
|
||||
"""
|
||||
```
|
||||
|
||||
如果你的代码中,多行字符串字面量包含换行符的话,则多行字符串字面量中也会包含换行符。如果你想换行,以便加强代码的可读性,但是你又不想在你的多行字符串字面量中出现换行符的话,你可以用在行尾写一个反斜杠(`\`)作为续行符。
|
||||
|
||||
```swift
|
||||
let softWrappedQuotation = """
|
||||
The White Rabbit put on his spectacles. "Where shall I begin, \
|
||||
please your Majesty?" he asked.
|
||||
|
||||
"Begin at the beginning," the King said gravely, "and go on \
|
||||
till you come to the end; then stop."
|
||||
"""
|
||||
```
|
||||
|
||||
为了让一个多行字符串字面量开始和结束于换行符,请将换行写在第一行和最后一行,例如:
|
||||
|
||||
```swift
|
||||
let lineBreaks = """
|
||||
|
||||
This string starts with a line break.
|
||||
It also ends with a line break.
|
||||
|
||||
"""
|
||||
```
|
||||
|
||||
一个多行字符串字面量能够缩进来匹配周围的代码。关闭引号(`"""`)之前的空白字符串告诉 Swift 编译器其他各行多少空白字符串需要忽略。然而,如果你在某行的前面写的空白字符串超出了关闭引号(`"""`)之前的空白字符串,则超出部分将被包含在多行字符串字面量中。
|
||||
|
||||

|
||||
|
||||
在上面的例子中,尽管整个多行字符串字面量都是缩进的(源代码缩进),第一行和最后一行没有以空白字符串开始(实际的变量值)。中间一行的缩进用空白字符串(源代码缩进)比关闭引号(`"""`)之前的空白字符串多,所以,它的行首将有4个空格。
|
||||
|
||||
### 字符串字面量的特殊字符
|
||||
|
||||
字符串字面量可以包含以下特殊字符:
|
||||
|
||||
- 转义字符 `\0`(空字符)、`\\`(反斜线)、`\t`(水平制表符)、`\n`(换行符)、`\r`(回车符)、`\"`(双引号)、`\'`(单引号)。
|
||||
- Unicode 标量,写成 `\u{n}`(u 为小写),其中 `n` 为任意一到八位十六进制数且可用的 Unicode 位码。
|
||||
|
||||
下面的代码为各种特殊字符的使用示例。 `wiseWords` 常量包含了两个双引号。`dollarSign`、`blackHeart` 和 `sparklingHeart` 常量演示了三种不同格式的 Unicode 标量:
|
||||
|
||||
```swift
|
||||
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
|
||||
// "Imageination is more important than knowledge" - Enistein
|
||||
let dollarSign = "\u{24}" // $,Unicode 标量 U+0024
|
||||
let blackHeart = "\u{2665}" // ♥,Unicode 标量 U+2665
|
||||
let sparklingHeart = "\u{1F496}" // 💖,Unicode 标量 U+1F496
|
||||
```
|
||||
|
||||
由于多行字符串字面量使用了三个双引号,而不是一个,所以你可以在多行字符串字面量里直接使用双引号(`"`)而不必加上转义符(`\`)。要在多行字符串字面量中使用 `"""` 的话,就需要使用至少一个转义符(`\`):
|
||||
|
||||
```swift
|
||||
let threeDoubleQuotes = """
|
||||
Escaping the first quote \"""
|
||||
Escaping all three quotes \"\"\"
|
||||
"""
|
||||
```
|
||||
|
||||
### 扩展字符串分隔符
|
||||
|
||||
您可以将字符串文字放在扩展分隔符中,这样字符串中的特殊字符将会被直接包含而非转义后的效果。将字符串放在引号(**"**)中并用数字符号(**#**)括起来。例如,打印字符串文字 **#"Line 1 \ nLine 2"#** 打印换行符转义序列(**\n**)而不是进行换行打印。
|
||||
|
||||
如果需要字符串文字中字符的特殊效果,请匹配转义字符(**\**)后面添加与起始位置个数相匹配的 **#** 符。 例如,如果您的字符串是 **#"Line 1 \ nLine 2"#** 并且您想要换行,则可以使用 **#“Line 1 \ #nLine 2”#**来代替。 同样,**###"Line1 \ ### nLine2"###** 也可以实现换行效果。
|
||||
|
||||
扩展分隔符创建的字符串文字也可以是多行字符串文字。 您可以使用扩展分隔符在多行字符串中包含文本 **"""**,覆盖原有的结束文字的默认行为。例如:
|
||||
|
||||
```swift
|
||||
let threeMoreDoubleQuotationMarks = #"""
|
||||
Here are three more double quotes: """
|
||||
"""#
|
||||
```
|
||||
|
||||
## 初始化空字符串
|
||||
|
||||
要创建一个空字符串作为初始值,可以将空的字符串字面量赋值给变量,也可以初始化一个新的 `String`实例:
|
||||
|
||||
```swift
|
||||
var emptyString = "" // 空字符串字面量
|
||||
var anotherEmptyString = String() // 初始化方法
|
||||
// 两个字符串均为空并等价。
|
||||
```
|
||||
|
||||
你可以通过检查 `Bool` 类型的 `isEmpty` 属性来判断该字符串是否为空:
|
||||
|
||||
```swift
|
||||
if emptyString.isEmpty {
|
||||
print("Nothing to see here")
|
||||
}
|
||||
// 打印输出:“Nothing to see here”
|
||||
```
|
||||
|
||||
## 字符串可变性
|
||||
|
||||
你可以通过将一个特定字符串分配给一个变量来对其进行修改,或者分配给一个常量来保证其不会被修改:
|
||||
|
||||
```swift
|
||||
var variableString = "Horse"
|
||||
variableString += " and carriage"
|
||||
// variableString 现在为 "Horse and carriage"
|
||||
|
||||
let constantString = "Highlander"
|
||||
constantString += " and another Highlander"
|
||||
// 这会报告一个编译错误(compile-time error) - 常量字符串不可以被修改。
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 在 Objective-C 和 Cocoa 中,需要通过选择两个不同的类(`NSString` 和 `NSMutableString`)来指定字符串是否可以被修改。
|
||||
|
||||
## 字符串是值类型
|
||||
|
||||
在 Swift 中 `String` 类型是*值类型*。如果你创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。在前述任一情况下,都会对已有字符串值创建新副本,并对该新副本而非原始字符串进行传递或赋值操作。值类型在 [结构体和枚举是值类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md#structures_and_enumerations_are_value_types) 中进行了详细描述。
|
||||
|
||||
Swift 默认拷贝字符串的行为保证了在函数/方法向你传递的字符串所属权属于你,无论该值来自于哪里。你可以确信传递的字符串不会被修改,除非你自己去修改它。
|
||||
|
||||
在实际编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着你将字符串作为值类型的同时可以获得极高的性能。
|
||||
|
||||
## 使用字符
|
||||
|
||||
你可通过 `for-in` 循环来遍历字符串,获取字符串中每一个字符的值:
|
||||
|
||||
```swift
|
||||
for character in "Dog!🐶" {
|
||||
print(character)
|
||||
}
|
||||
// D
|
||||
// o
|
||||
// g
|
||||
// !
|
||||
// 🐶
|
||||
```
|
||||
|
||||
`for-in` 循环在 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops) 中进行了详细描述。
|
||||
|
||||
另外,通过标明一个 `Character` 类型并用字符字面量进行赋值,可以建立一个独立的字符常量或变量:
|
||||
|
||||
```swift
|
||||
let exclamationMark: Character = "!"
|
||||
```
|
||||
|
||||
字符串可以通过传递一个值类型为 `Character` 的数组作为自变量来初始化:
|
||||
|
||||
```swift
|
||||
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
|
||||
let catString = String(catCharacters)
|
||||
print(catString)
|
||||
// 打印输出:“Cat!🐱”
|
||||
```
|
||||
|
||||
## 连接字符串和字符
|
||||
|
||||
字符串可以通过加法运算符(`+`)相加在一起(或称“连接”)创建一个新的字符串:
|
||||
|
||||
```swift
|
||||
let string1 = "hello"
|
||||
let string2 = " there"
|
||||
var welcome = string1 + string2
|
||||
// welcome 现在等于 "hello there"
|
||||
```
|
||||
|
||||
你也可以通过加法赋值运算符(`+=`)将一个字符串添加到一个已经存在字符串变量上:
|
||||
|
||||
```swift
|
||||
var instruction = "look over"
|
||||
instruction += string2
|
||||
// instruction 现在等于 "look over there"
|
||||
```
|
||||
|
||||
你可以用 `append()` 方法将一个字符附加到一个字符串变量的尾部:
|
||||
|
||||
```swift
|
||||
let exclamationMark: Character = "!"
|
||||
welcome.append(exclamationMark)
|
||||
// welcome 现在等于 "hello there!"
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 你不能将一个字符串或者字符添加到一个已经存在的字符变量上,因为字符变量只能包含一个字符。
|
||||
|
||||
如果你需要使用多行字符串字面量来拼接字符串,并且你需要字符串每一行都以换行符结尾,包括最后一行:
|
||||
|
||||
```swift
|
||||
let badStart = """
|
||||
one
|
||||
two
|
||||
"""
|
||||
let end = """
|
||||
three
|
||||
"""
|
||||
print(badStart + end)
|
||||
// 打印两行:
|
||||
// one
|
||||
// twothree
|
||||
|
||||
let goodStart = """
|
||||
one
|
||||
two
|
||||
|
||||
"""
|
||||
print(goodStart + end)
|
||||
// 打印三行:
|
||||
// one
|
||||
// two
|
||||
// three
|
||||
```
|
||||
|
||||
上面的代码,把 `badStart` 和 `end` 拼接起来的字符串非我们想要的结果。因为 `badStart` 最后一行没有换行符,它与 `end` 的第一行结合到了一起。相反的,`goodStart` 的每一行都以换行符结尾,所以它与 `end` 拼接的字符串总共有三行,正如我们期望的那样。
|
||||
|
||||
## 字符串插值
|
||||
|
||||
*字符串插值*是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。**字符串字面量**和**多行字符串字面量**都可以使用字符串插值。你插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:
|
||||
|
||||
```swift
|
||||
let multiplier = 3
|
||||
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
|
||||
// message 是 "3 times 2.5 is 7.5"
|
||||
```
|
||||
|
||||
在上面的例子中,`multiplier` 作为 `\(multiplier)` 被插入到一个字符串常量量中。当创建字符串执行插值计算时此占位符会被替换为 `multiplier` 实际的值。
|
||||
|
||||
`multiplier` 的值也作为字符串中后面表达式的一部分。该表达式计算 `Double(multiplier) * 2.5`的值并将结果(`7.5`)插入到字符串中。在这个例子中,表达式写为 `\(Double(multiplier) * 2.5)`并包含在字符串字面量中。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 插值字符串中写在括号中的表达式不能包含非转义反斜杠(`\`),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。
|
||||
|
||||
## Unicode
|
||||
|
||||
*Unicode*是一个用于在不同书写系统中对文本进行编码、表示和处理的国际标准。它使你可以用标准格式表示来自任意语言几乎所有的字符,并能够对文本文件或网页这样的外部资源中的字符进行读写操作。Swift 的 `String` 和 `Character` 类型是完全兼容 Unicode 标准的。
|
||||
|
||||
### Unicode 标量
|
||||
|
||||
Swift 的 `String` 类型是基于 *Unicode 标量* 建立的。Unicode 标量是对应字符或者修饰符的唯一的 21 位数字,例如 `U+0061` 表示小写的拉丁字母(`LATIN SMALL LETTER A`)("`a`"),`U+1F425` 表示小鸡表情(`FRONT-FACING BABY CHICK`)("`🐥`")。
|
||||
|
||||
请注意,并非所有 21 位 Unicode 标量值都分配给字符,某些标量被保留用于将来分配或用于 UTF-16 编码。已分配的标量值通常也有一个名称,例如上面示例中的 LATIN SMALL LETTER A 和 FRONT-FACING BABY CHICK。
|
||||
|
||||
### 可扩展的字形群集
|
||||
|
||||
每一个 Swift 的 `Character` 类型代表一个*可扩展的字形群*。而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。
|
||||
|
||||
举个例子,字母 `é` 可以用单一的 Unicode 标量 `é`(`LATIN SMALL LETTER E WITH ACUTE`, 或者 `U+00E9`)来表示。然而一个标准的字母 `e`(`LATIN SMALL LETTER E` 或者 `U+0065`) 加上一个急促重音(`COMBINING ACTUE ACCENT`)的标量(`U+0301`),这样一对标量就表示了同样的字母 `é`。 这个急促重音的标量形象的将 `e` 转换成了 `é`。
|
||||
|
||||
在这两种情况中,字母 `é` 代表了一个单一的 Swift 的 `Character` 值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:
|
||||
|
||||
```swift
|
||||
let eAcute: Character = "\u{E9}" // é
|
||||
let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́
|
||||
// eAcute 是 é, combinedEAcute 是 é
|
||||
```
|
||||
|
||||
可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。例如,来自朝鲜语字母表的韩语音节能表示为组合或分解的有序排列。在 Swift 都会表示为同一个单一的 `Character` 值:
|
||||
|
||||
```swift
|
||||
let precomposed: Character = "\u{D55C}" // 한
|
||||
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ
|
||||
// precomposed 是 한, decomposed 是 한
|
||||
```
|
||||
|
||||
可拓展的字符群集可以使包围记号(例如 `COMBINING ENCLOSING CIRCLE` 或者 `U+20DD`)的标量包围其他 Unicode 标量,作为一个单一的 `Character` 值:
|
||||
|
||||
```swift
|
||||
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
|
||||
// enclosedEAcute 是 é⃝
|
||||
```
|
||||
|
||||
地域性指示符号的 Unicode 标量可以组合成一个单一的 `Character` 值,例如 `REGIONAL INDICATOR SYMBOL LETTER U`(`U+1F1FA`)和 `REGIONAL INDICATOR SYMBOL LETTER S`(`U+1F1F8`):
|
||||
|
||||
```swift
|
||||
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
|
||||
// regionalIndicatorForUS 是 🇺🇸
|
||||
```
|
||||
|
||||
## 计算字符数量
|
||||
|
||||
如果想要获得一个字符串中 `Character` 值的数量,可以使用 `count` 属性:
|
||||
|
||||
```swift
|
||||
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
|
||||
print("unusualMenagerie has \(unusualMenagerie.count) characters")
|
||||
// 打印输出“unusualMenagerie has 40 characters”
|
||||
```
|
||||
|
||||
注意在 Swift 中,使用可拓展的字符群集作为 `Character` 值来连接或改变字符串时,并不一定会更改字符串的字符数量。
|
||||
|
||||
例如,如果你用四个字符的单词 `cafe` 初始化一个新的字符串,然后添加一个 `COMBINING ACTUE ACCENT`(`U+0301`)作为字符串的结尾。最终这个字符串的字符数量仍然是 `4`,因为第四个字符是 `é`,而不是 `e`:
|
||||
|
||||
```swift
|
||||
var word = "cafe"
|
||||
print("the number of characters in \(word) is \(word.count)")
|
||||
// 打印输出“the number of characters in cafe is 4”
|
||||
|
||||
word += "\u{301}" // 拼接一个重音,U+0301
|
||||
|
||||
print("the number of characters in \(word) is \(word.count)")
|
||||
// 打印输出“the number of characters in café is 4”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 可扩展的字形群可以由多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取字符串的可扩展的字符群的范围时候,就不能计算出字符串的字符数量。如果你正在处理一个长字符串,需要注意 `count` 属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。
|
||||
>
|
||||
> 另外需要注意的是通过 `count` 属性返回的字符数量并不总是与包含相同字符的 `NSString` 的 `length` 属性相同。`NSString` 的 `length` 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。
|
||||
|
||||
## 访问和修改字符串
|
||||
|
||||
你可以通过字符串的属性和方法来访问和修改它,当然也可以用下标语法完成。
|
||||
|
||||
### 字符串索引
|
||||
|
||||
每一个 `String` 值都有一个关联的索引(*index*)类型,`String.Index`,它对应着字符串中的每一个 `Character` 的位置。
|
||||
|
||||
前面提到,不同的字符可能会占用不同数量的内存空间,所以要知道 `Character` 的确定位置,就必须从 `String` 开头遍历每一个 Unicode 标量直到结尾。因此,Swift 的字符串不能用整数(integer)做索引。
|
||||
|
||||
使用 `startIndex` 属性可以获取一个 `String` 的第一个 `Character` 的索引。使用 `endIndex` 属性可以获取最后一个 `Character` 的后一个位置的索引。因此,`endIndex` 属性不能作为一个字符串的有效下标。如果 `String` 是空串,`startIndex` 和 `endIndex` 是相等的。
|
||||
|
||||
通过调用 `String` 的 `index(before:)` 或 `index(after:)` 方法,可以立即得到前面或后面的一个索引。你还可以通过调用 `index(_:offsetBy:)` 方法来获取对应偏移量的索引,这种方式可以避免多次调用 `index(before:)` 或 `index(after:)` 方法。
|
||||
|
||||
你可以使用下标语法来访问 `String` 特定索引的 `Character`。
|
||||
|
||||
```swift
|
||||
let greeting = "Guten Tag!"
|
||||
greeting[greeting.startIndex]
|
||||
// G
|
||||
greeting[greeting.index(before: greeting.endIndex)]
|
||||
// !
|
||||
greeting[greeting.index(after: greeting.startIndex)]
|
||||
// u
|
||||
let index = greeting.index(greeting.startIndex, offsetBy: 7)
|
||||
greeting[index]
|
||||
// a
|
||||
```
|
||||
|
||||
试图获取越界索引对应的 `Character`,将引发一个运行时错误。
|
||||
|
||||
```swift
|
||||
greeting[greeting.endIndex] // error
|
||||
greeting.index(after: endIndex) // error
|
||||
```
|
||||
|
||||
使用 `indices` 属性会创建一个包含全部索引的范围(`Range`),用来在一个字符串中访问单个字符。
|
||||
|
||||
```swift
|
||||
for index in greeting.indices {
|
||||
print("\(greeting[index]) ", terminator: "")
|
||||
}
|
||||
// 打印输出“G u t e n T a g ! ”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 你可以使用 `startIndex` 和 `endIndex` 属性或者 `index(before:)` 、`index(after:)` 和 `index(_:offsetBy:)` 方法在任意一个确认的并遵循 `Collection` 协议的类型里面,如上文所示是使用在 `String` 中,你也可以使用在 `Array`、`Dictionary` 和 `Set` 中。
|
||||
|
||||
### 插入和删除
|
||||
|
||||
调用 `insert(_:at:)` 方法可以在一个字符串的指定索引插入一个字符,调用 `insert(contentsOf:at:)` 方法可以在一个字符串的指定索引插入一个段字符串。
|
||||
|
||||
```swift
|
||||
var welcome = "hello"
|
||||
welcome.insert("!", at: welcome.endIndex)
|
||||
// welcome 变量现在等于 "hello!"
|
||||
|
||||
welcome.insert(contentsOf:" there", at: welcome.index(before: welcome.endIndex))
|
||||
// welcome 变量现在等于 "hello there!"
|
||||
```
|
||||
|
||||
调用 `remove(at:)` 方法可以在一个字符串的指定索引删除一个字符,调用 `removeSubrange(_:)` 方法可以在一个字符串的指定索引删除一个子字符串。
|
||||
|
||||
```swift
|
||||
welcome.remove(at: welcome.index(before: welcome.endIndex))
|
||||
// welcome 现在等于 "hello there"
|
||||
|
||||
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
|
||||
welcome.removeSubrange(range)
|
||||
// welcome 现在等于 "hello"
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 你可以使用 `insert(_:at:)`、`insert(contentsOf:at:)`、`remove(at:)` 和 `removeSubrange(_:)` 方法在任意一个确认的并遵循 `RangeReplaceableCollection` 协议的类型里面,如上文所示是使用在 `String` 中,你也可以使用在 `Array`、`Dictionary` 和 `Set` 中。
|
||||
|
||||
## 子字符串
|
||||
|
||||
当你从字符串中获取一个子字符串 —— 例如,使用下标或者 `prefix(_:)` 之类的方法 —— 就可以得到一个 `SubString` 的实例,而非另外一个 `String`。Swift 里的 `SubString` 绝大部分函数都跟 `String` 一样,意味着你可以使用同样的方式去操作 `SubString` 和 `String`。然而,跟 `String` 不同的是,你只有在短时间内需要操作字符串时,才会使用 `SubString`。当你需要长时间保存结果时,就把 `SubString` 转化为 `String` 的实例:
|
||||
|
||||
```swift
|
||||
let greeting = "Hello, world!"
|
||||
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
|
||||
let beginning = greeting[..<index]
|
||||
// beginning 的值为 "Hello"
|
||||
|
||||
// 把结果转化为 String 以便长期存储。
|
||||
let newString = String(beginning)
|
||||
```
|
||||
|
||||
就像 `String`,每一个 `SubString` 都会在内存里保存字符集。而 `String` 和 `SubString` 的区别在于性能优化上,`SubString` 可以重用原 `String` 的内存空间,或者另一个 `SubString` 的内存空间(`String` 也有同样的优化,但如果两个 `String` 共享内存的话,它们就会相等)。这一优化意味着你在修改 `String` 和 `SubString` 之前都不需要消耗性能去复制内存。就像前面说的那样,`SubString`不适合长期存储 —— 因为它重用了原 `String` 的内存空间,原 `String` 的内存空间必须保留直到它的 `SubString` 不再被使用为止。
|
||||
|
||||
上面的例子,`greeting` 是一个 `String`,意味着它在内存里有一片空间保存字符集。而由于 `beginning` 是 `greeting` 的 `SubString`,它重用了 `greeting` 的内存空间。相反,`newString` 是一个 `String` —— 它是使用 `SubString` 创建的,拥有一片自己的内存空间。下面的图展示了他们之间的关系:
|
||||
|
||||

|
||||
|
||||
> 注意
|
||||
>
|
||||
> `String` 和 `SubString` 都遵循 `StringProtocol<//apple_ref/swift/intf/s:s14StringProtocolP>` 协议,这意味着操作字符串的函数使用 `StringProtocol` 会更加方便。你可以传入 `String` 或 `SubString` 去调用函数。
|
||||
|
||||
## 比较字符串
|
||||
|
||||
Swift 提供了三种方式来比较文本值:字符串字符相等、前缀相等和后缀相等。
|
||||
|
||||
### 字符串/字符相等
|
||||
|
||||
字符串/字符可以用等于操作符(`==`)和不等于操作符(`!=`),详细描述在 [比较运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/02_Basic_Operators.html#comparison_operators):
|
||||
|
||||
```swift
|
||||
let quotation = "We're a lot alike, you and I."
|
||||
let sameQuotation = "We're a lot alike, you and I."
|
||||
if quotation == sameQuotation {
|
||||
print("These two strings are considered equal")
|
||||
}
|
||||
// 打印输出“These two strings are considered equal”
|
||||
```
|
||||
|
||||
如果两个字符串(或者两个字符)的可扩展的字形群集是标准相等,那就认为它们是相等的。只要可扩展的字形群集有同样的语言意义和外观则认为它们标准相等,即使它们是由不同的 Unicode 标量构成。
|
||||
|
||||
例如,`LATIN SMALL LETTER E WITH ACUTE`(`U+00E9`)就是标准相等于 `LATIN SMALL LETTER E`(`U+0065`)后面加上 `COMBINING ACUTE ACCENT`(`U+0301`)。这两个字符群集都是表示字符 `é` 的有效方式,所以它们被认为是标准相等的:
|
||||
|
||||
```swift
|
||||
// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E WITH ACUTE
|
||||
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
|
||||
|
||||
// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E and COMBINING ACUTE ACCENT
|
||||
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"
|
||||
|
||||
if eAcuteQuestion == combinedEAcuteQuestion {
|
||||
print("These two strings are considered equal")
|
||||
}
|
||||
// 打印输出“These two strings are considered equal”
|
||||
```
|
||||
|
||||
相反,英语中的 `LATIN CAPITAL LETTER A`(`U+0041`,或者 `A`)不等于俄语中的 `CYRILLIC CAPITAL LETTER A`(`U+0410`,或者 `A`)。两个字符看着是一样的,但却有不同的语言意义:
|
||||
|
||||
```swift
|
||||
let latinCapitalLetterA: Character = "\u{41}"
|
||||
|
||||
let cyrillicCapitalLetterA: Character = "\u{0410}"
|
||||
|
||||
if latinCapitalLetterA != cyrillicCapitalLetterA {
|
||||
print("These two characters are not equivalent")
|
||||
}
|
||||
// 打印“These two characters are not equivalent”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 在 Swift 中,字符串和字符并不区分地域(not locale-sensitive)。
|
||||
|
||||
### 前缀/后缀相等
|
||||
|
||||
通过调用字符串的 `hasPrefix(_:)`/`hasSuffix(_:)` 方法来检查字符串是否拥有特定前缀/后缀,两个方法均接收一个 `String` 类型的参数,并返回一个布尔值。
|
||||
|
||||
下面的例子以一个字符串数组表示莎士比亚话剧《罗密欧与朱丽叶》中前两场的场景位置:
|
||||
|
||||
```swift
|
||||
let romeoAndJuliet = [
|
||||
"Act 1 Scene 1: Verona, A public place",
|
||||
"Act 1 Scene 2: Capulet's mansion",
|
||||
"Act 1 Scene 3: A room in Capulet's mansion",
|
||||
"Act 1 Scene 4: A street outside Capulet's mansion",
|
||||
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
|
||||
"Act 2 Scene 1: Outside Capulet's mansion",
|
||||
"Act 2 Scene 2: Capulet's orchard",
|
||||
"Act 2 Scene 3: Outside Friar Lawrence's cell",
|
||||
"Act 2 Scene 4: A street in Verona",
|
||||
"Act 2 Scene 5: Capulet's mansion",
|
||||
"Act 2 Scene 6: Friar Lawrence's cell"
|
||||
]
|
||||
```
|
||||
|
||||
你可以调用 `hasPrefix(_:)` 方法来计算话剧中第一幕的场景数:
|
||||
|
||||
```swift
|
||||
var act1SceneCount = 0
|
||||
for scene in romeoAndJuliet {
|
||||
if scene.hasPrefix("Act 1 ") {
|
||||
act1SceneCount += 1
|
||||
}
|
||||
}
|
||||
print("There are \(act1SceneCount) scenes in Act 1")
|
||||
// 打印输出“There are 5 scenes in Act 1”
|
||||
```
|
||||
|
||||
相似地,你可以用 `hasSuffix(_:)` 方法来计算发生在不同地方的场景数:
|
||||
|
||||
```swift
|
||||
var mansionCount = 0
|
||||
var cellCount = 0
|
||||
for scene in romeoAndJuliet {
|
||||
if scene.hasSuffix("Capulet's mansion") {
|
||||
mansionCount += 1
|
||||
} else if scene.hasSuffix("Friar Lawrence's cell") {
|
||||
cellCount += 1
|
||||
}
|
||||
}
|
||||
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
|
||||
// 打印输出“6 mansion scenes; 2 cell scenes”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `hasPrefix(_:)` 和 `hasSuffix(_:)` 方法都是在每个字符串中逐字符比较其可扩展的字符群集是否标准相等,详细描述在 [字符串/字符相等](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#string_and_character_equality)。
|
||||
|
||||
## 字符串的 Unicode 表示形式
|
||||
|
||||
当一个 Unicode 字符串被写进文本文件或者其他储存时,字符串中的 Unicode 标量会用 Unicode 定义的几种 `编码格式`(encoding forms)编码。每一个字符串中的小块编码都被称 `代码单元`(code units)。这些包括 UTF-8 编码格式(编码字符串为 8 位的代码单元), UTF-16 编码格式(编码字符串位 16 位的代码单元),以及 UTF-32 编码格式(编码字符串32位的代码单元)。
|
||||
|
||||
Swift 提供了几种不同的方式来访问字符串的 Unicode 表示形式。你可以利用 `for-in` 来对字符串进行遍历,从而以 Unicode 可扩展的字符群集的方式访问每一个 `Character` 值。该过程在 [使用字符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#working_with_characters) 中进行了描述。
|
||||
|
||||
另外,能够以其他三种 Unicode 兼容的方式访问字符串的值:
|
||||
|
||||
- UTF-8 代码单元集合(利用字符串的 `utf8` 属性进行访问)
|
||||
- UTF-16 代码单元集合(利用字符串的 `utf16` 属性进行访问)
|
||||
- 21 位的 Unicode 标量值集合,也就是字符串的 UTF-32 编码格式(利用字符串的 `unicodeScalars`属性进行访问)
|
||||
|
||||
下面由 `D`,`o`,`g`,`‼`(`DOUBLE EXCLAMATION MARK`, Unicode 标量 `U+203C`)和 `🐶`(`DOG FACE`,Unicode 标量为 `U+1F436`)组成的字符串中的每一个字符代表着一种不同的表示:
|
||||
|
||||
```swift
|
||||
let dogString = "Dog‼🐶"
|
||||
```
|
||||
|
||||
### UTF-8 表示
|
||||
|
||||
你可以通过遍历 `String` 的 `utf8` 属性来访问它的 `UTF-8` 表示。其为 `String.UTF8View` 类型的属性,`UTF8View` 是无符号 8 位(`UInt8`)值的集合,每一个 `UInt8` 值都是一个字符的 UTF-8 表示:
|
||||
|
||||
| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 | | | | | |
|
||||
| --------------- | -------- | -------- | -------- | -------- | --------- | ---- | ---- | ---- | ---- | ---- |
|
||||
| UTF-8 Code Unit | 68 | 111 | 103 | 226 | 128 | 188 | 240 | 159 | 144 | 182 |
|
||||
| Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|
||||
|
||||
```swift
|
||||
for codeUnit in dogString.utf8 {
|
||||
print("\(codeUnit) ", terminator: "")
|
||||
}
|
||||
print("")
|
||||
// 68 111 103 226 128 188 240 159 144 182
|
||||
```
|
||||
|
||||
上面的例子中,前三个 10 进制 `codeUnit` 值(`68`、`111`、`103`)代表了字符 `D`、`o` 和 `g`,它们的 UTF-8 表示与 ASCII 表示相同。接下来的三个 10 进制 `codeUnit` 值(`226`、`128`、`188`)是 `DOUBLE EXCLAMATION MARK` 的3字节 UTF-8 表示。最后的四个 `codeUnit` 值(`240`、`159`、`144`、`182`)是 `DOG FACE` 的4字节 UTF-8 表示。
|
||||
|
||||
### UTF-16 表示
|
||||
|
||||
你可以通过遍历 `String` 的 `utf16` 属性来访问它的 `UTF-16` 表示。其为 `String.UTF16View` 类型的属性,`UTF16View` 是无符号16位(`UInt16`)值的集合,每一个 `UInt16` 都是一个字符的 UTF-16 表示:
|
||||
|
||||
| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 | |
|
||||
| ---------------- | -------- | -------- | -------- | -------- | --------- | ----- |
|
||||
| UTF-16 Code Unit | 68 | 111 | 103 | 8252 | 55357 | 56374 |
|
||||
| Position | 0 | 1 | 2 | 3 | 4 | 5 |
|
||||
|
||||
```swift
|
||||
for codeUnit in dogString.utf16 {
|
||||
print("\(codeUnit) ", terminator: "")
|
||||
}
|
||||
print("")
|
||||
// 68 111 103 8252 55357 56374
|
||||
```
|
||||
|
||||
同样,前三个 `codeUnit` 值(`68`、`111`、`103`)代表了字符 `D`、`o` 和 `g`,它们的 UTF-16 代码单元和 UTF-8 完全相同(因为这些 Unicode 标量表示 ASCII 字符)。
|
||||
|
||||
第四个 `codeUnit` 值(`8252`)是一个等于十六进制 `203C` 的的十进制值。这个代表了 `DOUBLE EXCLAMATION MARK` 字符的 Unicode 标量值 `U+203C`。这个字符在 UTF-16 中可以用一个代码单元表示。
|
||||
|
||||
第五和第六个 `codeUnit` 值(`55357` 和 `56374`)是 `DOG FACE` 字符的 UTF-16 表示。第一个值为 `U+D83D`(十进制值为 `55357`),第二个值为 `U+DC36`(十进制值为 `56374`)。
|
||||
|
||||
### Unicode 标量表示
|
||||
|
||||
你可以通过遍历 `String` 值的 `unicodeScalars` 属性来访问它的 Unicode 标量表示。其为 `UnicodeScalarView` 类型的属性,`UnicodeScalarView` 是 `UnicodeScalar` 类型的值的集合。
|
||||
|
||||
每一个 `UnicodeScalar` 拥有一个 `value` 属性,可以返回对应的 21 位数值,用 `UInt32` 来表示:
|
||||
|
||||
| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 |
|
||||
| ------------------------ | -------- | -------- | -------- | -------- | --------- |
|
||||
| Unicode Scalar Code Unit | 68 | 111 | 103 | 8252 | 128054 |
|
||||
| Position | 0 | 1 | 2 | 3 | 4 |
|
||||
|
||||
```swift
|
||||
for scalar in dogString.unicodeScalars {
|
||||
print("\(scalar.value) ", terminator: "")
|
||||
}
|
||||
print("")
|
||||
// 68 111 103 8252 128054
|
||||
```
|
||||
|
||||
前三个 `UnicodeScalar` 值(`68`、`111`、`103`)的 `value` 属性仍然代表字符 `D`、`o` 和 `g`。
|
||||
|
||||
第四个 `codeUnit` 值(`8252`)仍然是一个等于十六进制 `203C` 的十进制值。这个代表了 `DOUBLE EXCLAMATION MARK` 字符的 Unicode 标量 `U+203C`。
|
||||
|
||||
第五个 `UnicodeScalar` 值的 `value` 属性,`128054`,是一个十六进制 `1F436` 的十进制表示。其等同于 `DOG FACE` 的 Unicode 标量 `U+1F436`。
|
||||
|
||||
作为查询它们的 `value` 属性的一种替代方法,每个 `UnicodeScalar` 值也可以用来构建一个新的 `String` 值,比如在字符串插值中使用:
|
||||
|
||||
```swift
|
||||
for scalar in dogString.unicodeScalars {
|
||||
print("\(scalar) ")
|
||||
}
|
||||
// D
|
||||
// o
|
||||
// g
|
||||
// ‼
|
||||
// 🐶
|
||||
```
|
||||
|
||||
644
IOS/Task00:Swift基础语法学习/4.集合类型.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# 集合类型
|
||||
|
||||
Swift 语言提供 `Arrays`、`Sets` 和 `Dictionaries` 三种基本的*集合类型*用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。
|
||||
|
||||

|
||||
|
||||
Swift 语言中的 `Arrays`、`Sets` 和 `Dictionaries` 中存储的数据值类型必须明确。这意味着我们不能把错误的数据类型插入其中。同时这也说明你完全可以对取回值的类型非常放心。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 的 `Arrays`、`Sets` 和 `Dictionaries` 类型被实现为*泛型集合*。更多关于泛型类型和集合,参见 [泛型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Generics.md) 章节。
|
||||
|
||||
## 集合的可变性
|
||||
|
||||
如果创建一个 `Arrays`、`Sets` 或 `Dictionaries` 并且把它分配成一个变量,这个集合将会是*可变的*。这意味着你可以在创建之后添加更多或移除已存在的数据项,或者改变集合中的数据项。如果我们把 `Arrays`、`Sets` 或 `Dictionaries` 分配成常量,那么它就是*不可变的*,它的大小和内容都不能被改变。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 在我们不需要改变集合的时候创建不可变集合是很好的实践。如此 Swift 编译器可以优化我们创建的集合。
|
||||
|
||||
## 数组(Arrays)
|
||||
|
||||
*数组*使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 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<Element>` 这样的形式,其中 `Element` 是这个数组中唯一允许存在的数据类型。我们也可以使用像 `[Element]` 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。
|
||||
|
||||
### 创建一个空数组
|
||||
|
||||
我们可以使用构造语法来创建一个由特定数据类型构成的空数组:
|
||||
|
||||
```swift
|
||||
var someInts = [Int]()
|
||||
print("someInts is of type [Int] with \(someInts.count) items.")
|
||||
// 打印“someInts is of type [Int] with 0 items.”
|
||||
```
|
||||
|
||||
注意,通过构造函数的类型,`someInts` 的值类型被推断为 `[Int]`。
|
||||
|
||||
或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:`[]`(一对空方括号):
|
||||
|
||||
```swift
|
||||
someInts.append(3)
|
||||
// someInts 现在包含一个 Int 值
|
||||
someInts = []
|
||||
// someInts 现在是空数组,但是仍然是 [Int] 类型的。
|
||||
```
|
||||
|
||||
### 创建一个带有默认值的数组
|
||||
|
||||
Swift 中的 `Array` 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(`count`)和适当类型的初始值(`repeating`)传入数组构造函数:
|
||||
|
||||
```swift
|
||||
var threeDoubles = Array(repeating: 0.0, count: 3)
|
||||
// threeDoubles 是一种 [Double] 数组,等价于 [0.0, 0.0, 0.0]
|
||||
```
|
||||
|
||||
### 通过两个数组相加创建一个数组
|
||||
|
||||
我们可以使用加法操作符(`+`)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:
|
||||
|
||||
```swift
|
||||
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
|
||||
// anotherThreeDoubles 被推断为 [Double],等价于 [2.5, 2.5, 2.5]
|
||||
|
||||
var sixDoubles = threeDoubles + anotherThreeDoubles
|
||||
// sixDoubles 被推断为 [Double],等价于 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
|
||||
```
|
||||
|
||||
### 用数组字面量构造数组
|
||||
|
||||
我们可以使用*数组字面量*来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:
|
||||
|
||||
`[value 1, value 2, value 3]`。
|
||||
|
||||
下面这个例子创建了一个叫做 `shoppingList` 并且存储 `String` 的数组:
|
||||
|
||||
```swift
|
||||
var shoppingList: [String] = ["Eggs", "Milk"]
|
||||
// shoppingList 已经被构造并且拥有两个初始项。
|
||||
```
|
||||
|
||||
`shoppingList` 变量被声明为“字符串值类型的数组“,记作 `[String]`。 因为这个数组被规定只有 `String` 一种数据结构,所以只有 `String` 类型可以在其中被存取。 在这里,`shoppingList` 数组由两个 `String` 值(`"Eggs"` 和 `"Milk"`)构造,并且由数组字面量定义。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `shoppingList` 数组被声明为变量(`var` 关键字创建)而不是常量(`let` 创建)是因为以后可能会有更多的数据项被插入其中。
|
||||
|
||||
在这个例子中,字面量仅仅包含两个 `String` 值。匹配了该数组的变量声明(只能包含 `String` 的数组),所以这个字面量的分配过程可以作为用两个初始项来构造 `shoppingList` 的一种方式。
|
||||
|
||||
由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。`shoppingList` 的构造也可以这样写:
|
||||
|
||||
```swift
|
||||
var shoppingList = ["Eggs", "Milk"]
|
||||
```
|
||||
|
||||
因为所有数组字面量中的值都是相同的类型,Swift 可以推断出 `[String]` 是 `shoppingList` 中变量的正确类型。
|
||||
|
||||
### 访问和修改数组
|
||||
|
||||
我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。
|
||||
|
||||
可以使用数组的只读属性 `count` 来获取数组中的数据项数量:
|
||||
|
||||
```swift
|
||||
print("The shopping list contains \(shoppingList.count) items.")
|
||||
// 输出“The shopping list contains 2 items.”(这个数组有2个项)
|
||||
```
|
||||
|
||||
使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`:
|
||||
|
||||
```swift
|
||||
if shoppingList.isEmpty {
|
||||
print("The shopping list is empty.")
|
||||
} else {
|
||||
print("The shopping list is not empty.")
|
||||
}
|
||||
// 打印“The shopping list is not empty.”(shoppinglist 不是空的)
|
||||
```
|
||||
|
||||
也可以使用 `append(_:)` 方法在数组后面添加新的数据项:
|
||||
|
||||
```swift
|
||||
shoppingList.append("Flour")
|
||||
// shoppingList 现在有3个数据项,有人在摊煎饼
|
||||
```
|
||||
|
||||
除此之外,使用加法赋值运算符(`+=`)也可以直接在数组后面添加一个或多个拥有相同类型的数据项:
|
||||
|
||||
```swift
|
||||
shoppingList += ["Baking Powder"]
|
||||
// shoppingList 现在有四项了
|
||||
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
|
||||
// shoppingList 现在有七项了
|
||||
```
|
||||
|
||||
可以直接使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值放在直接放在数组名称的方括号中:
|
||||
|
||||
```swift
|
||||
var firstItem = shoppingList[0]
|
||||
// 第一项是“Eggs”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 第一项在数组中的索引值是 `0` 而不是 `1`。 Swift 中的数组索引总是从零开始。
|
||||
|
||||
我们也可以用下标来改变某个已有索引值对应的数据值:
|
||||
|
||||
```swift
|
||||
shoppingList[0] = "Six eggs"
|
||||
// 其中的第一项现在是“Six eggs”而不是“Eggs”
|
||||
```
|
||||
|
||||
还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 `"Chocolate Spread"`、`"Cheese"` 和 `"Butter"` 替换为 `"Bananas"` 和 `"Apples"`:
|
||||
|
||||
```swift
|
||||
shoppingList[4...6] = ["Bananas", "Apples"]
|
||||
// shoppingList 现在有6项
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 不可以用下标访问的形式去在数组尾部添加新项。
|
||||
|
||||
调用数组的 `insert(_:at:)` 方法来在某个具体索引值之前添加数据项:
|
||||
|
||||
```swift
|
||||
shoppingList.insert("Maple Syrup", at: 0)
|
||||
// shoppingList 现在有7项
|
||||
// 现在是这个列表中的第一项是“Maple Syrup”
|
||||
```
|
||||
|
||||
这次 `insert(_:at:)` 方法调用把值为 `"Maple Syrup"` 的新数据项插入列表的最开始位置,并且使用 `0` 作为索引值。
|
||||
|
||||
类似的我们可以使用 `remove(at:)` 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它):
|
||||
|
||||
```swift
|
||||
let mapleSyrup = shoppingList.remove(at: 0)
|
||||
// 索引值为0的数据项被移除
|
||||
// shoppingList 现在只有6项,而且不包括 Maple Syrup
|
||||
// mapleSyrup 常量的值等于被移除数据项“Maple Syrup”的值
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的 `count` 属性进行比较来在使用某个索引之前先检验是否有效。除了当 `count` 等于 0 时(说明这是个空数组),最大索引值一直是 `count - 1`,因为数组都是零起索引。
|
||||
|
||||
数据项被移除后数组中的空出项会被自动填补,所以现在索引值为 `0` 的数据项的值再次等于 `"Six eggs"`:
|
||||
|
||||
```swift
|
||||
firstItem = shoppingList[0]
|
||||
// firstItem 现在等于“Six eggs”
|
||||
```
|
||||
|
||||
如果我们只想把数组中的最后一项移除,可以使用 `removeLast()` 方法而不是 `remove(at:)` 方法来避免我们需要获取数组的 `count` 属性。就像后者一样,前者也会返回被移除的数据项:
|
||||
|
||||
```swift
|
||||
let apples = shoppingList.removeLast()
|
||||
// 数组的最后一项被移除了
|
||||
// shoppingList 现在只有5项,不包括 Apples
|
||||
// apples 常量的值现在等于“Apples”字符串
|
||||
```
|
||||
|
||||
### 数组的遍历
|
||||
|
||||
我们可以使用 `for-in` 循环来遍历所有数组中的数据项:
|
||||
|
||||
```swift
|
||||
for item in shoppingList {
|
||||
print(item)
|
||||
}
|
||||
// Six eggs
|
||||
// Milk
|
||||
// Flour
|
||||
// Baking Powder
|
||||
// Bananas
|
||||
```
|
||||
|
||||
如果我们同时需要每个数据项的值和索引值,可以使用 `enumerated()` 方法来进行数组遍历。`enumerated()` 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:
|
||||
|
||||
```swift
|
||||
for (index, value) in shoppingList.enumerated() {
|
||||
print("Item \(String(index + 1)): \(value)")
|
||||
}
|
||||
// Item 1: Six eggs
|
||||
// Item 2: Milk
|
||||
// Item 3: Flour
|
||||
// Item 4: Baking Powder
|
||||
// Item 5: Bananas
|
||||
```
|
||||
|
||||
更多关于 `for-in` 循环的介绍请参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。
|
||||
|
||||
## 集合(Sets)
|
||||
|
||||
*集合(Set)*用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。
|
||||
|
||||
> 注意 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)部分。
|
||||
|
||||
### 集合类型的哈希值
|
||||
|
||||
一个类型为了存储在集合中,该类型必须是*可哈希化*的——也就是说,该类型必须提供一个方法来计算它的*哈希值*。一个哈希值是 `Int` 类型的,相等的对象哈希值必须相同,比如 `a==b`,因此必须 `a.hashValue == b.hashValue`。
|
||||
|
||||
Swift 的所有基本类型(比如 `String`、`Int`、`Double` 和 `Bool`)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有关联值的枚举成员值(在 [枚举](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/08_Enumerations.html) 有讲述)默认也是可哈希化的。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型遵循 Swift 标准库中的 `Hashable` 协议。遵循 `Hashable` 协议的类型需要提供一个类型为 `Int` 的可读属性 `hashValue`。由类型的 `hashValue` 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。
|
||||
>
|
||||
> 因为 `Hashable` 协议遵循 `Equatable` 协议,所以遵循该协议的类型也必须提供一个“是否相等”运算符(`==`)的实现。这个 `Equatable` 协议要求任何遵循 `==` 实现的实例间都是一种相等的关系。也就是说,对于 `a,b,c` 三个值来说,`==` 的实现必须满足下面三种情况:
|
||||
>
|
||||
> - `a == a`(自反性)
|
||||
> - `a == b` 意味着 `b == a`(对称性)
|
||||
> - `a == b && b == c` 意味着 `a == c`(传递性)
|
||||
|
||||
关于遵循协议的更多信息,请看 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/22_Protocols.md)。
|
||||
|
||||
### 集合类型语法
|
||||
|
||||
Swift 中的 `Set` 类型被写为 `Set<Element>`,这里的 `Element` 表示 `Set` 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。
|
||||
|
||||
### 创建和构造一个空的集合
|
||||
|
||||
你可以通过构造器语法创建一个特定类型的空集合:
|
||||
|
||||
```swift
|
||||
var letters = Set<Character>()
|
||||
print("letters is of type Set<Character> with \(letters.count) items.")
|
||||
// 打印“letters is of type Set<Character> with 0 items.”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 通过构造器,这里的 `letters` 变量的类型被推断为 `Set<Character>`。
|
||||
|
||||
此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,我们可以通过一个空的数组字面量创建一个空的 `Set`:
|
||||
|
||||
```swift
|
||||
letters.insert("a")
|
||||
// letters 现在含有1个 Character 类型的值
|
||||
letters = []
|
||||
// letters 现在是一个空的 Set,但是它依然是 Set<Character> 类型
|
||||
```
|
||||
|
||||
### 用数组字面量创建集合
|
||||
|
||||
你可以使用数组字面量来构造集合,并且可以使用简化形式写一个或者多个值作为集合元素。
|
||||
|
||||
下面的例子创建一个称之为 `favoriteGenres` 的集合来存储 `String` 类型的值:
|
||||
|
||||
```swift
|
||||
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
|
||||
// favoriteGenres 被构造成含有三个初始值的集合
|
||||
```
|
||||
|
||||
这个 `favoriteGenres` 变量被声明为“一个 `String` 值的集合”,写为 `Set<String>`。由于这个特定的集合含有指定 `String` 类型的值,所以它只允许存储 `String` 类型值。这里的 `favoriteGenres` 变量有三个 `String` 类型的初始值(`"Rock"`,`"Classical"` 和 `"Hip hop"`),并以数组字面量的方式出现。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `favoriteGenres` 被声明为一个变量(拥有 `var` 标示符)而不是一个常量(拥有 `let` 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。
|
||||
|
||||
一个 `Set` 类型不能从数组字面量中被单独推断出来,因此 `Set` 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 `Set` 并且该数组字面量中的所有元素类型相同,那么你无须写出 `Set` 的具体类型。`favoriteGenres` 的构造形式可以采用简化的方式代替:
|
||||
|
||||
```swift
|
||||
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
|
||||
```
|
||||
|
||||
由于数组字面量中的所有元素类型相同,Swift 可以推断出 `Set<String>` 作为 `favoriteGenres` 变量的正确类型。
|
||||
|
||||
### 访问和修改一个集合
|
||||
|
||||
你可以通过 `Set` 的属性和方法来访问和修改一个 `Set`。
|
||||
|
||||
为了找出一个 `Set` 中元素的数量,可以使用其只读属性 `count`:
|
||||
|
||||
```swift
|
||||
print("I have \(favoriteGenres.count) favorite music genres.")
|
||||
// 打印“I have 3 favorite music genres.”
|
||||
```
|
||||
|
||||
使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`:
|
||||
|
||||
```swift
|
||||
if favoriteGenres.isEmpty {
|
||||
print("As far as music goes, I'm not picky.")
|
||||
} else {
|
||||
print("I have particular music preferences.")
|
||||
}
|
||||
// 打印“I have particular music preferences.”
|
||||
```
|
||||
|
||||
你可以通过调用 `Set` 的 `insert(_:)` 方法来添加一个新元素:
|
||||
|
||||
```swift
|
||||
favoriteGenres.insert("Jazz")
|
||||
// favoriteGenres 现在包含4个元素
|
||||
```
|
||||
|
||||
你可以通过调用 `Set` 的 `remove(_:)` 方法去删除一个元素,如果该值是该 `Set` 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 `Set` 不包含该值,则返回 `nil`。另外,`Set` 中的所有元素可以通过它的 `removeAll()` 方法删除。
|
||||
|
||||
```swift
|
||||
if let removedGenre = favoriteGenres.remove("Rock") {
|
||||
print("\(removedGenre)? I'm over it.")
|
||||
} else {
|
||||
print("I never much cared for that.")
|
||||
}
|
||||
// 打印“Rock? I'm over it.”
|
||||
```
|
||||
|
||||
使用 `contains(_:)` 方法去检查 `Set` 中是否包含一个特定的值:
|
||||
|
||||
```swift
|
||||
if favoriteGenres.contains("Funk") {
|
||||
print("I get up on the good foot.")
|
||||
} else {
|
||||
print("It's too funky in here.")
|
||||
}
|
||||
// 打印“It's too funky in here.”
|
||||
```
|
||||
|
||||
### 遍历一个集合
|
||||
|
||||
你可以在一个 `for-in` 循环中遍历一个 `Set` 中的所有值。
|
||||
|
||||
```swift
|
||||
for genre in favoriteGenres {
|
||||
print("\(genre)")
|
||||
}
|
||||
// Classical
|
||||
// Jazz
|
||||
// Hip hop
|
||||
```
|
||||
|
||||
更多关于 `for-in` 循环的信息,参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。
|
||||
|
||||
Swift 的 `Set` 类型没有确定的顺序,为了按照特定顺序来遍历一个 `Set` 中的值可以使用 `sorted()` 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符'<'对元素进行比较的结果来确定。
|
||||
|
||||
```swift
|
||||
for genre in favoriteGenres.sorted() {
|
||||
print("\(genre)")
|
||||
}
|
||||
// Classical
|
||||
// Hip hop
|
||||
// Jazz
|
||||
```
|
||||
|
||||
## 集合操作
|
||||
|
||||
你可以高效地完成 `Set` 的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。
|
||||
|
||||
### 基本集合操作
|
||||
|
||||
下面的插图描述了两个集合 `a` 和 `b`,以及通过阴影部分的区域显示集合各种操作的结果。
|
||||
|
||||

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

|
||||
|
||||
- 使用“是否相等”运算符(`==`)来判断两个集合是否包含全部相同的值。
|
||||
- 使用 `isSubset(of:)` 方法来判断一个集合中的值是否也被包含在另外一个集合中。
|
||||
- 使用 `isSuperset(of:)` 方法来判断一个集合中包含另一个集合中所有的值。
|
||||
- 使用 `isStrictSubset(of:)` 或者 `isStrictSuperset(of:)` 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
|
||||
- 使用 `isDisjoint(with:)` 方法来判断两个集合是否不含有相同的值(是否没有交集)。
|
||||
|
||||
```swift
|
||||
let houseAnimals: Set = ["🐶", "🐱"]
|
||||
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
|
||||
let cityAnimals: Set = ["🐦", "🐭"]
|
||||
|
||||
houseAnimals.isSubset(of: farmAnimals)
|
||||
// true
|
||||
farmAnimals.isSuperset(of: houseAnimals)
|
||||
// true
|
||||
farmAnimals.isDisjoint(with: cityAnimals)
|
||||
// true
|
||||
```
|
||||
|
||||
## 字典
|
||||
|
||||
*字典*是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 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) 部分。
|
||||
|
||||
### 字典类型简化语法
|
||||
|
||||
Swift 的字典使用 `Dictionary<Key, Value>` 定义,其中 `Key` 是字典中键的数据类型,`Value` 是字典中对应于这些键所存储值的数据类型。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 一个字典的 `Key` 类型必须遵循 `Hashable` 协议,就像 `Set` 的值类型。
|
||||
|
||||
我们也可以用 `[Key: Value]` 这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。
|
||||
|
||||
### 创建一个空字典
|
||||
|
||||
我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典:
|
||||
|
||||
```swift
|
||||
var namesOfIntegers = [Int: String]()
|
||||
// namesOfIntegers 是一个空的 [Int: String] 字典
|
||||
```
|
||||
|
||||
这个例子创建了一个 `[Int: String]` 类型的空字典来储存整数的英语命名。它的键是 `Int` 型,值是 `String` 型。
|
||||
|
||||
如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作 `[:]`(中括号中放一个冒号):
|
||||
|
||||
```swift
|
||||
namesOfIntegers[16] = "sixteen"
|
||||
// namesOfIntegers 现在包含一个键值对
|
||||
namesOfIntegers = [:]
|
||||
// namesOfIntegers 又成为了一个 [Int: String] 类型的空字典
|
||||
```
|
||||
|
||||
### 用字典字面量创建字典
|
||||
|
||||
我们可以使用*字典字面量*来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 `Dictionary` 集合的快捷途径。
|
||||
|
||||
一个键值对是一个 `key` 和一个 `value` 的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割:
|
||||
|
||||
```swift
|
||||
[key 1: value 1, key 2: value 2, key 3: value 3]
|
||||
```
|
||||
|
||||
下面的例子创建了一个存储国际机场名称的字典。在这个字典中键是三个字母的国际航空运输相关代码,值是机场名称:
|
||||
|
||||
```swift
|
||||
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
|
||||
```
|
||||
|
||||
`airports` 字典被声明为一种 `[String: String]` 类型,这意味着这个字典的键和值都是 `String` 类型。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `airports` 字典被声明为变量(用 `var` 关键字)而不是常量(`let` 关键字)因为后来更多的机场信息会被添加到这个示例字典中。
|
||||
|
||||
`airports` 字典使用字典字面量初始化,包含两个键值对。第一对的键是 `YYZ`,值是 `Toronto Pearson`。第二对的键是 `DUB`,值是 `Dublin`。
|
||||
|
||||
这个字典语句包含了两个 `String: String` 类型的键值对。它们对应 `airports` 变量声明的类型(一个只有 `String` 键和 `String` 值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的 `airport` 字典。
|
||||
|
||||
和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。 `airports` 字典也可以用这种简短方式定义:
|
||||
|
||||
```swift
|
||||
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
|
||||
```
|
||||
|
||||
因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 `Dictionary<String, String>` 是 `airports` 字典的正确类型。
|
||||
|
||||
### 访问和修改字典
|
||||
|
||||
我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。
|
||||
|
||||
和数组一样,我们可以通过字典的只读属性 `count` 来获取某个字典的数据项数量:
|
||||
|
||||
```swift
|
||||
print("The dictionary of airports contains \(airports.count) items.")
|
||||
// 打印“The dictionary of airports contains 2 items.”(这个字典有两个数据项)
|
||||
```
|
||||
|
||||
使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`:
|
||||
|
||||
```swift
|
||||
if airports.isEmpty {
|
||||
print("The airports dictionary is empty.")
|
||||
} else {
|
||||
print("The airports dictionary is not empty.")
|
||||
}
|
||||
// 打印“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:)` 方法会返回对应值的类型的可选值。举例来说:对于存储 `String` 值的字典,这个函数会返回一个 `String?` 或者“可选 `String`”类型的值。
|
||||
|
||||
如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 `nil`。
|
||||
|
||||
```swift
|
||||
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
|
||||
print("The old value for DUB was \(oldValue).")
|
||||
}
|
||||
// 输出“The old value for DUB was Dublin.”
|
||||
```
|
||||
|
||||
我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回 `nil`:
|
||||
|
||||
```swift
|
||||
if let airportName = airports["DUB"] {
|
||||
print("The name of the airport is \(airportName).")
|
||||
} else {
|
||||
print("That airport is not in the airports dictionary.")
|
||||
}
|
||||
// 打印“The name of the airport is Dublin Airport.”
|
||||
```
|
||||
|
||||
我们还可以使用下标语法来通过给某个键的对应值赋值为 `nil` 来从字典里移除一个键值对:
|
||||
|
||||
```swift
|
||||
airports["APL"] = "Apple Internation"
|
||||
// “Apple Internation”不是真的 APL 机场,删除它
|
||||
airports["APL"] = nil
|
||||
// APL 现在被移除了
|
||||
```
|
||||
|
||||
此外,`removeValue(forKey:)` 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回 `nil`:
|
||||
|
||||
```swift
|
||||
if let removedValue = airports.removeValue(forKey: "DUB") {
|
||||
print("The removed airport's name is \(removedValue).")
|
||||
} else {
|
||||
print("The airports dictionary does not contain a value for DUB.")
|
||||
}
|
||||
// 打印“The removed airport's name is Dublin Airport.”
|
||||
```
|
||||
|
||||
### 字典遍历
|
||||
|
||||
我们可以使用 `for-in` 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 `(key, value)` 元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组:
|
||||
|
||||
```swift
|
||||
for (airportCode, airportName) in airports {
|
||||
print("\(airportCode): \(airportName)")
|
||||
}
|
||||
// YYZ: Toronto Pearson
|
||||
// LHR: London Heathrow
|
||||
```
|
||||
|
||||
更多关于 `for-in` 循环的信息,参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。
|
||||
|
||||
通过访问 `keys` 或者 `values` 属性,我们也可以遍历字典的键或者值:
|
||||
|
||||
```swift
|
||||
for airportCode in airports.keys {
|
||||
print("Airport code: \(airportCode)")
|
||||
}
|
||||
// Airport code: YYZ
|
||||
// Airport code: LHR
|
||||
|
||||
for airportName in airports.values {
|
||||
print("Airport name: \(airportName)")
|
||||
}
|
||||
// Airport name: Toronto Pearson
|
||||
// Airport name: London Heathrow
|
||||
```
|
||||
|
||||
如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受 `Array` 实例的 API 的参数,可以直接使用 `keys` 或者 `values` 属性构造一个新数组:
|
||||
|
||||
```swift
|
||||
let airportCodes = [String](airports.keys)
|
||||
// airportCodes 是 ["YYZ", "LHR"]
|
||||
|
||||
let airportNames = [String](airports.values)
|
||||
// airportNames 是 ["Toronto Pearson", "London Heathrow"]
|
||||
```
|
||||
|
||||
Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 `keys` 或 `values` 属性使用 `sorted()` 方法。
|
||||
766
IOS/Task00:Swift基础语法学习/5.控制流.md
Normal file
@@ -0,0 +1,766 @@
|
||||
# 控制流
|
||||
|
||||
Swift 提供了多种流程控制结构,包括可以多次执行任务的 `while` 循环,基于特定条件选择执行不同代码分支的 `if`、`guard` 和 `switch` 语句,还有控制流程跳转到其他代码位置的 `break` 和 `continue` 语句。
|
||||
|
||||
Swift 还提供了 `for-in` 循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。
|
||||
|
||||
Swift 的 `switch` 语句比许多类 C 语言要更加强大。case 还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。`switch` 语句的 case 中匹配的值可以声明为临时常量或变量,在 case 作用域内使用,也可以配合 `where` 来描述更复杂的匹配条件。
|
||||
|
||||
## For-In 循环
|
||||
|
||||
你可以使用 `for-in` 循环来遍历一个集合中的所有元素,例如数组中的元素、范围内的数字或者字符串中的字符。
|
||||
|
||||
以下例子使用 `for-in` 遍历一个数组所有元素:
|
||||
|
||||
```swift
|
||||
let names = ["Anna", "Alex", "Brian", "Jack"]
|
||||
for name in names {
|
||||
print("Hello, \(name)!")
|
||||
}
|
||||
// Hello, Anna!
|
||||
// Hello, Alex!
|
||||
// Hello, Brian!
|
||||
// Hello, Jack!
|
||||
```
|
||||
|
||||
你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 `(key, value)` 元组的形式返回,你可以在 `for-in` 循环中使用显式的常量名称来解读 `(key, value)` 元组。下面的例子中,字典的键声明会为 `animalName` 常量,字典的值会声明为 `legCount` 常量:
|
||||
|
||||
```swift
|
||||
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
|
||||
for (animalName, legCount) in numberOfLegs {
|
||||
print("\(animalName)s have \(legCount) legs")
|
||||
}
|
||||
// cats have 4 legs
|
||||
// ants have 6 legs
|
||||
// spiders have 8 legs
|
||||
```
|
||||
|
||||
字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。关于数组和字典的细节,参见 [集合类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/04_Collection_Types.html)。
|
||||
|
||||
`for-in` 循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容:
|
||||
|
||||
```swift
|
||||
for index in 1...5 {
|
||||
print("\(index) times 5 is \(index * 5)")
|
||||
}
|
||||
// 1 times 5 is 5
|
||||
// 2 times 5 is 10
|
||||
// 3 times 5 is 15
|
||||
// 4 times 5 is 20
|
||||
// 5 times 5 is 25
|
||||
```
|
||||
|
||||
例子中用来进行遍历的元素是使用闭区间操作符(`...`)表示的从 `1` 到 `5` 的数字区间。`index` 被赋值为闭区间中的第一个数字(`1`),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 `index` 值所对应的乘 5 乘法表的结果。该语句执行后,`index` 的值被更新为闭区间中的第二个数字(`2`),之后 `print(_:separator:terminator:)` 函数会再执行一次。整个过程会进行到闭区间结尾为止。
|
||||
|
||||
上面的例子中,`index` 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,`index` 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 `let` 关键字声明。
|
||||
|
||||
如果你不需要区间序列内每一项的值,你可以使用下划线(`_`)替代变量名来忽略这个值:
|
||||
|
||||
```swift
|
||||
let base = 3
|
||||
let power = 10
|
||||
var answer = 1
|
||||
for _ in 1...power {
|
||||
answer *= base
|
||||
}
|
||||
print("\(base) to the power of \(power) is \(answer)")
|
||||
// 输出“3 to the power of 10 is 59049”
|
||||
```
|
||||
|
||||
这个例子计算 base 这个数的 power 次幂(本例中,是 `3` 的 `10` 次幂),从 `1`(`3` 的 `0` 次幂)开始做 `3` 的乘法, 进行 `10` 次,使用 `1` 到 `10` 的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 `_` (替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。
|
||||
|
||||
在某些情况下,你可能不想使用包括两个端点的闭区间。想象一下,你在一个手表上绘制分钟的刻度线。总共 `60` 个刻度,从 `0` 分开始。使用半开区间运算符(`..<`)来表示一个左闭右开的区间。有关区间的更多信息,请参阅 [区间运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/02_Basic_Operators.html#range_operators)。
|
||||
|
||||
```swift
|
||||
let minutes = 60
|
||||
for tickMark in 0..<minutes {
|
||||
// 每一分钟都渲染一个刻度线(60次)
|
||||
}
|
||||
```
|
||||
|
||||
一些用户可能在其 UI 中可能需要较少的刻度。他们可以每 5 分钟作为一个刻度。使用 `stride(from:to:by:)` 函数跳过不需要的标记。
|
||||
|
||||
```swift
|
||||
let minuteInterval = 5
|
||||
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
|
||||
// 每5分钟渲染一个刻度线(0, 5, 10, 15 ... 45, 50, 55)
|
||||
}
|
||||
```
|
||||
|
||||
可以在闭区间使用 `stride(from:through:by:)` 起到同样作用:
|
||||
|
||||
```swift
|
||||
let hours = 12
|
||||
let hourInterval = 3
|
||||
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
|
||||
// 每3小时渲染一个刻度线(3, 6, 9, 12)
|
||||
}
|
||||
```
|
||||
|
||||
## While 循环
|
||||
|
||||
`while` 循环会一直运行一段语句直到条件变成 `false`。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift 提供两种 `while` 循环形式:
|
||||
|
||||
- `while` 循环,每次在循环开始时计算条件是否符合;
|
||||
- `repeat-while` 循环,每次在循环结束时计算条件是否符合。
|
||||
|
||||
### While
|
||||
|
||||
`while` 循环从计算一个条件开始。如果条件为 `true`,会重复运行一段语句,直到条件变为 `false`。
|
||||
|
||||
下面是 `while` 循环的一般格式:
|
||||
|
||||
```swift
|
||||
while condition {
|
||||
statements
|
||||
}
|
||||
```
|
||||
|
||||
下面的例子来玩一个叫做*蛇和梯子*(也叫做*滑道和梯子*)的小游戏:
|
||||
|
||||

|
||||
|
||||
游戏的规则如下:
|
||||
|
||||
- 游戏盘面包括 25 个方格,游戏目标是达到或者超过第 25 个方格;
|
||||
- 每一轮,你通过掷一个六面体骰子来确定你移动方块的步数,移动的路线由上图中横向的虚线所示;
|
||||
- 如果在某轮结束,你移动到了梯子的底部,可以顺着梯子爬上去;
|
||||
- 如果在某轮结束,你移动到了蛇的头部,你会顺着蛇的身体滑下去。
|
||||
|
||||
游戏盘面可以使用一个 `Int` 数组来表达。数组的长度由一个 `finalSquare` 常量储存,用来初始化数组和检测最终胜利条件。游戏盘面由 26 个 `Int` 0 值初始化,而不是 25 个(由 `0` 到 `25`,一共 26 个):
|
||||
|
||||
```swift
|
||||
let finalSquare = 25
|
||||
var board = [Int](repeating: 0, count: finalSquare + 1)
|
||||
```
|
||||
|
||||
一些方格被设置成特定的值来表示有蛇或者梯子。梯子底部的方格是一个正值,使你可以向上移动,蛇头处的方格是一个负值,会让你向下移动:
|
||||
|
||||
```swift
|
||||
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
|
||||
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
|
||||
```
|
||||
|
||||
3 号方格是梯子的底部,会让你向上移动到 11 号方格,我们使用 `board[03]` 等于 `+08`(来表示 `11`和 `3` 之间的差值)。为了对齐语句,这里使用了一元正运算符(`+i`)和一元负运算符(`-i`),并且小于 10 的数字都使用 0 补齐(这些语法的技巧不是必要的,只是为了让代码看起来更加整洁)。
|
||||
|
||||
玩家由左下角空白处编号为 0 的方格开始游戏。玩家第一次掷骰子后才会进入游戏盘面:
|
||||
|
||||
```swift
|
||||
var square = 0
|
||||
var diceRoll = 0
|
||||
while square < finalSquare {
|
||||
// 掷骰子
|
||||
diceRoll += 1
|
||||
if diceRoll == 7 { diceRoll = 1 }
|
||||
// 根据点数移动
|
||||
square += diceRoll
|
||||
if square < board.count {
|
||||
// 如果玩家还在棋盘上,顺着梯子爬上去或者顺着蛇滑下去
|
||||
square += board[square]
|
||||
}
|
||||
}
|
||||
print("Game over!")
|
||||
```
|
||||
|
||||
本例中使用了最简单的方法来模拟掷骰子。`diceRoll` 的值并不是一个随机数,而是以 `0` 为初始值,之后每一次 `while` 循环,`diceRoll` 的值增加 1 ,然后检测是否超出了最大值。当 `diceRoll` 的值等于 7 时,就超过了骰子的最大值,会被重置为 `1`。所以 `diceRoll` 的取值顺序会一直是 `1`,`2`,`3`,`4`,`5`,`6`,`1`,`2` 等。
|
||||
|
||||
掷完骰子后,玩家向前移动 `diceRoll` 个方格,如果玩家移动超过了第 25 个方格,这个时候游戏将会结束,为了应对这种情况,代码会首先判断 `square` 的值是否小于 `board` 的 `count` 属性,只有小于才会在 `board[square]` 上增加 `square`,来向前或向后移动(遇到了梯子或者蛇)。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果没有这个检测(`square < board.count`),`board[square]` 可能会越界访问 `board` 数组,导致运行时错误。
|
||||
|
||||
当本轮 `while` 循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为 `false`,此时游戏结束。
|
||||
|
||||
`while` 循环比较适合本例中的这种情况,因为在 `while` 循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。
|
||||
|
||||
### Repeat-While
|
||||
|
||||
`while` 循环的另外一种形式是 `repeat-while`,它和 `while` 的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为 `false`。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> Swift 语言的 `repeat-while` 循环和其他语言中的 `do-while` 循环是类似的。
|
||||
|
||||
下面是 `repeat-while` 循环的一般格式:
|
||||
|
||||
```swift
|
||||
repeat {
|
||||
statements
|
||||
} while condition
|
||||
```
|
||||
|
||||
还是*蛇和梯子*的游戏,使用 `repeat-while` 循环来替代 `while` 循环。`finalSquare`、`board`、`square` 和 `diceRoll` 的值初始化同 `while` 循环时一样:
|
||||
|
||||
```swift
|
||||
let finalSquare = 25
|
||||
var board = [Int](repeating: 0, count: finalSquare + 1)
|
||||
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
|
||||
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
|
||||
var square = 0
|
||||
var diceRoll = 0
|
||||
```
|
||||
|
||||
`repeat-while` 的循环版本,循环中*第一步*就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。
|
||||
|
||||
游戏开始时,玩家在第 0 个方格上,`board[0]` 一直等于 0, 不会有什么影响:
|
||||
|
||||
```swift
|
||||
repeat {
|
||||
// 顺着梯子爬上去或者顺着蛇滑下去
|
||||
square += board[square]
|
||||
// 掷骰子
|
||||
diceRoll += 1
|
||||
if diceRoll == 7 { diceRoll = 1 }
|
||||
// 根据点数移动
|
||||
square += diceRoll
|
||||
} while square < finalSquare
|
||||
print("Game over!")
|
||||
```
|
||||
|
||||
检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动 `diceRoll` 个方格,本轮循环结束。
|
||||
|
||||
循环条件(`while square < finalSquare`)和 `while` 方式相同,但是只会在循环结束后进行计算。在这个游戏中,`repeat-while` 表现得比 `while` 循环更好。`repeat-while` 方式会在条件判断 `square` 没有超出后直接运行 `square += board[square]`,这种方式可以比起前面 `while` 循环的版本,可以省去数组越界的检查。
|
||||
|
||||
## 条件语句
|
||||
|
||||
根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用*条件语句*。
|
||||
|
||||
Swift 提供两种类型的条件语句:`if` 语句和 `switch` 语句。通常,当条件较为简单且可能的情况很少时,使用 `if` 语句。而 `switch` 语句更适用于条件较复杂、有更多排列组合的时候。并且 `switch` 在需要用到模式匹配(pattern-matching)的情况下会更有用。
|
||||
|
||||
### If
|
||||
|
||||
`if` 语句最简单的形式就是只包含一个条件,只有该条件为 `true` 时,才执行相关代码:
|
||||
|
||||
```swift
|
||||
var temperatureInFahrenheit = 30
|
||||
if temperatureInFahrenheit <= 32 {
|
||||
print("It's very cold. Consider wearing a scarf.")
|
||||
}
|
||||
// 输出“It's very cold. Consider wearing a scarf.”
|
||||
```
|
||||
|
||||
上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 `if` 块后面的代码。
|
||||
|
||||
当然,`if` 语句允许二选一执行,叫做 `else` 从句。也就是当条件为 `false` 时,执行 *else 语句*:
|
||||
|
||||
```swift
|
||||
temperatureInFahrenheit = 40
|
||||
if temperatureInFahrenheit <= 32 {
|
||||
print("It's very cold. Consider wearing a scarf.")
|
||||
} else {
|
||||
print("It's not that cold. Wear a t-shirt.")
|
||||
}
|
||||
// 输出“It's not that cold. Wear a t-shirt.”
|
||||
```
|
||||
|
||||
显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,`else` 分支就被触发了。
|
||||
|
||||
你可以把多个 `if` 语句链接在一起,来实现更多分支:
|
||||
|
||||
```swift
|
||||
temperatureInFahrenheit = 90
|
||||
if temperatureInFahrenheit <= 32 {
|
||||
print("It's very cold. Consider wearing a scarf.")
|
||||
} else if temperatureInFahrenheit >= 86 {
|
||||
print("It's really warm. Don't forget to wear sunscreen.")
|
||||
} else {
|
||||
print("It's not that cold. Wear a t-shirt.")
|
||||
}
|
||||
// 输出“It's really warm. Don't forget to wear sunscreen.”
|
||||
```
|
||||
|
||||
在上面的例子中,额外的 `if` 语句用于判断是不是特别热。而最后的 `else` 语句被保留了下来,用于打印既不冷也不热时的消息。
|
||||
|
||||
实际上,当不需要完整判断情况的时候,最后的 `else` 语句是可选的:
|
||||
|
||||
```swift
|
||||
temperatureInFahrenheit = 72
|
||||
if temperatureInFahrenheit <= 32 {
|
||||
print("It's very cold. Consider wearing a scarf.")
|
||||
} else if temperatureInFahrenheit >= 86 {
|
||||
print("It's really warm. Don't forget to wear sunscreen.")
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中,由于既不冷也不热,所以不会触发 `if` 或 `else if` 分支,也就不会打印任何消息。
|
||||
|
||||
### Switch
|
||||
|
||||
`switch` 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,`switch` 语句会执行对应的代码。当有可能的情况较多时,通常用 `switch` 语句替换 `if` 语句。
|
||||
|
||||
`switch` 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:
|
||||
|
||||
```swift
|
||||
switch some value to consider {
|
||||
case value 1:
|
||||
respond to value 1
|
||||
case value 2,
|
||||
value 3:
|
||||
respond to value 2 or 3
|
||||
default:
|
||||
otherwise, do something else
|
||||
}
|
||||
```
|
||||
|
||||
`switch` 语句由*多个 case* 构成,每个由 `case` 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。
|
||||
|
||||
与 `if` 语句类似,每一个 case 都是代码执行的一条分支。`switch` 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值*切换(switching)*。
|
||||
|
||||
`switch` 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(`default`)分支来涵盖其它所有没有对应的值,这个默认分支必须在 `switch` 语句的最后面。
|
||||
|
||||
下面的例子使用 `switch` 语句来匹配一个名为 `someCharacter` 的小写字符:
|
||||
|
||||
```swift
|
||||
let someCharacter: Character = "z"
|
||||
switch someCharacter {
|
||||
case "a":
|
||||
print("The first letter of the alphabet")
|
||||
case "z":
|
||||
print("The last letter of the alphabet")
|
||||
default:
|
||||
print("Some other character")
|
||||
}
|
||||
// 输出“The last letter of the alphabet”
|
||||
```
|
||||
|
||||
在这个例子中,第一个 case 分支用于匹配第一个英文字母 `a`,第二个 case 分支用于匹配最后一个字母 `z`。因为 `switch` 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 `default` 分支来匹配除了 `a` 和 `z` 外的所有值,这个分支保证了 swith 语句的完备性。
|
||||
|
||||
#### 不存在隐式的贯穿
|
||||
|
||||
与 C 和 Objective-C 中的 `switch` 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 `switch` 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 `break` 语句。这使得 `switch` 语句更安全、更易用,也避免了漏写 `break` 语句导致多个语言被执行的错误。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 虽然在 Swift 中 `break` 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 `break`跳出,详情请参见 [Switch 语句中的 break](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#break_in_a_switch_statement)。
|
||||
|
||||
每一个 case 分支都*必须*包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:
|
||||
|
||||
```swift
|
||||
let anotherCharacter: Character = "a"
|
||||
switch anotherCharacter {
|
||||
case "a": // 无效,这个分支下面没有语句
|
||||
case "A":
|
||||
print("The letter A")
|
||||
default:
|
||||
print("Not the letter A")
|
||||
}
|
||||
// 这段代码会报编译错误
|
||||
```
|
||||
|
||||
不像 C 语言里的 `switch` 语句,在 Swift 中,`switch` 语句不会一起匹配 `"a"` 和 `"A"`。相反的,上面的代码会引起编译期错误:`case "a": 不包含任何可执行语句`——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。
|
||||
|
||||
为了让单个 case 同时匹配 `a` 和 `A`,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
|
||||
|
||||
```swift
|
||||
let anotherCharacter: Character = "a"
|
||||
switch anotherCharacter {
|
||||
case "a", "A":
|
||||
print("The letter A")
|
||||
default:
|
||||
print("Not the letter A")
|
||||
}
|
||||
// 输出“The letter A”
|
||||
```
|
||||
|
||||
为了可读性,符合匹配可以写成多行形式,详情请参考 [复合匹配](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#compound_cases)
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果想要显式贯穿 case 分支,请使用 `fallthrough` 语句,详情请参考 [贯穿](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#fallthrough)。
|
||||
|
||||
#### 区间匹配
|
||||
|
||||
case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:
|
||||
|
||||
```swift
|
||||
let approximateCount = 62
|
||||
let countedThings = "moons orbiting Saturn"
|
||||
let naturalCount: String
|
||||
switch approximateCount {
|
||||
case 0:
|
||||
naturalCount = "no"
|
||||
case 1..<5:
|
||||
naturalCount = "a few"
|
||||
case 5..<12:
|
||||
naturalCount = "several"
|
||||
case 12..<100:
|
||||
naturalCount = "dozens of"
|
||||
case 100..<1000:
|
||||
naturalCount = "hundreds of"
|
||||
default:
|
||||
naturalCount = "many"
|
||||
}
|
||||
print("There are \(naturalCount) \(countedThings).")
|
||||
// 输出“There are dozens of moons orbiting Saturn.”
|
||||
```
|
||||
|
||||
在上例中,`approximateCount` 在一个 `switch` 声明中被评估。每一个 `case` 都与之进行比较。因为 `approximateCount` 落在了 12 到 100 的区间,所以 `naturalCount` 等于 `"dozens of"` 值,并且此后的执行跳出了 `switch` 语句。
|
||||
|
||||
#### 元组
|
||||
|
||||
我们可以使用元组在同一个 `switch` 语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(`_`)来匹配所有可能的值。
|
||||
|
||||
下面的例子展示了如何使用一个 `(Int, Int)` 类型的元组来分类下图中的点 (x, y):
|
||||
|
||||
```swift
|
||||
let somePoint = (1, 1)
|
||||
switch somePoint {
|
||||
case (0, 0):
|
||||
print("\(somePoint) is at the origin")
|
||||
case (_, 0):
|
||||
print("\(somePoint) is on the x-axis")
|
||||
case (0, _):
|
||||
print("\(somePoint) is on the y-axis")
|
||||
case (-2...2, -2...2):
|
||||
print("\(somePoint) is inside the box")
|
||||
default:
|
||||
print("\(somePoint) is outside of the box")
|
||||
}
|
||||
// 输出“(1, 1) is inside the box”
|
||||
```
|
||||
|
||||

|
||||
|
||||
在上面的例子中,`switch` 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。
|
||||
|
||||
不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有*四个 case*。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 `case (0, 0)`,因此剩下的能够匹配的分支都会被忽视掉。
|
||||
|
||||
#### 值绑定(Value Bindings)
|
||||
|
||||
case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为*值绑定*(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。
|
||||
|
||||
下面的例子将下图中的点 (x, y),使用 `(Int, Int)` 类型的元组表示,然后分类表示:
|
||||
|
||||
```swift
|
||||
let anotherPoint = (2, 0)
|
||||
switch anotherPoint {
|
||||
case (let x, 0):
|
||||
print("on the x-axis with an x value of \(x)")
|
||||
case (0, let y):
|
||||
print("on the y-axis with a y value of \(y)")
|
||||
case let (x, y):
|
||||
print("somewhere else at (\(x), \(y))")
|
||||
}
|
||||
// 输出“on the x-axis with an x value of 2”
|
||||
```
|
||||
|
||||

|
||||
|
||||
在上面的例子中,`switch` 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。
|
||||
|
||||
这三个 case 都声明了常量 `x` 和 `y` 的占位符,用于临时获取元组 `anotherPoint` 的一个或两个值。第一个 case ——`case (let x, 0)` 将匹配一个纵坐标为 `0` 的点,并把这个点的横坐标赋给临时的常量 `x`。类似的,第二个 case ——`case (0, let y)` 将匹配一个横坐标为 `0` 的点,并把这个点的纵坐标赋给临时的常量 `y`。
|
||||
|
||||
一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。
|
||||
|
||||
请注意,这个 `switch` 语句不包含默认分支。这是因为最后一个 case ——`case let(x, y)` 声明了一个可以匹配余下所有值的元组。这使得 `switch` 语句已经完备了,因此不需要再书写默认分支。
|
||||
|
||||
#### Where
|
||||
|
||||
case 分支的模式可以使用 `where` 语句来判断额外的条件。
|
||||
|
||||
下面的例子把下图中的点 (x, y)进行了分类:
|
||||
|
||||
```swift
|
||||
let yetAnotherPoint = (1, -1)
|
||||
switch yetAnotherPoint {
|
||||
case let (x, y) where x == y:
|
||||
print("(\(x), \(y)) is on the line x == y")
|
||||
case let (x, y) where x == -y:
|
||||
print("(\(x), \(y)) is on the line x == -y")
|
||||
case let (x, y):
|
||||
print("(\(x), \(y)) is just some arbitrary point")
|
||||
}
|
||||
// 输出“(1, -1) is on the line x == -y”
|
||||
```
|
||||
|
||||

|
||||
|
||||
在上面的例子中,`switch` 语句会判断某个点是否在绿色的对角线 `x == y` 上,是否在紫色的对角线 `x == -y` 上,或者不在对角线上。
|
||||
|
||||
这三个 case 都声明了常量 `x` 和 `y` 的占位符,用于临时获取元组 `yetAnotherPoint` 的两个值。这两个常量被用作 `where` 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 `where` 语句的条件为 `true` 时,匹配到的 case 分支才会被执行。
|
||||
|
||||
就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,`switch` 语句就已经完备了,因此不需要再书写默认分支。
|
||||
|
||||
#### 复合型 Cases
|
||||
|
||||
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 `case` 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
|
||||
|
||||
```swift
|
||||
let someCharacter: Character = "e"
|
||||
switch someCharacter {
|
||||
case "a", "e", "i", "o", "u":
|
||||
print("\(someCharacter) is a vowel")
|
||||
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
|
||||
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
|
||||
print("\(someCharacter) is a consonant")
|
||||
default:
|
||||
print("\(someCharacter) is not a vowel or a consonant")
|
||||
}
|
||||
// 输出“e is a vowel”
|
||||
```
|
||||
|
||||
这个 `switch` 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,`default` 分支匹配了其它所有字符。
|
||||
|
||||
复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。
|
||||
|
||||
```swift
|
||||
let stillAnotherPoint = (9, 0)
|
||||
switch stillAnotherPoint {
|
||||
case (let distance, 0), (0, let distance):
|
||||
print("On an axis, \(distance) from the origin")
|
||||
default:
|
||||
print("Not on an axis")
|
||||
}
|
||||
// 输出“On an axis, 9 from the origin”
|
||||
```
|
||||
|
||||
上面的 case 有两个模式:`(let distance, 0)` 匹配了在 x 轴上的值,`(0, let distance)` 匹配了在 y 轴上的值。两个模式都绑定了 `distance`,并且 `distance` 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 `distance` 值。
|
||||
|
||||
## 控制转移语句
|
||||
|
||||
控制转移语句改变你代码的执行顺序,通过它可以实现代码的跳转。Swift 有五种控制转移语句:
|
||||
|
||||
- `continue`
|
||||
- `break`
|
||||
- `fallthrough`
|
||||
- `return`
|
||||
- `throw`
|
||||
|
||||
我们将会在下面讨论 `continue`、`break` 和 `fallthrough` 语句。`return` 语句将会在 [函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html) 章节讨论,`throw` 语句会在 [错误抛出](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/18_Error_Handling.md#throwing_errors) 章节讨论。
|
||||
|
||||
### Continue
|
||||
|
||||
`continue` 语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。
|
||||
|
||||
下面的例子把一个小写字符串中的元音字母和空格字符移除,生成了一个含义模糊的短句:
|
||||
|
||||
```swift
|
||||
let puzzleInput = "great minds think alike"
|
||||
var puzzleOutput = ""
|
||||
for character in puzzleInput {
|
||||
switch character {
|
||||
case "a", "e", "i", "o", "u", " ":
|
||||
continue
|
||||
default:
|
||||
puzzleOutput.append(character)
|
||||
}
|
||||
}
|
||||
print(puzzleOutput)
|
||||
// 输出“grtmndsthnklk”
|
||||
```
|
||||
|
||||
在上面的代码中,只要匹配到元音字母或者空格字符,就调用 `continue` 语句,使本次循环结束,重新开始下次循环。这种行为使 `switch` 匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。
|
||||
|
||||
### Break
|
||||
|
||||
`break` 语句会立刻结束整个控制流的执行。`break` 可以在 `switch` 或循环语句中使用,用来提前结束 `switch` 或循环语句。
|
||||
|
||||
#### 循环语句中的 break
|
||||
|
||||
当在一个循环体中使用 `break` 时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号(`}`)后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生。
|
||||
|
||||
#### Switch 语句中的 break
|
||||
|
||||
当在一个 `switch` 代码块中使用 `break` 时,会立即中断该 `switch` 代码块的执行,并且跳转到表示 `switch` 代码块结束的大括号(`}`)后的第一行代码。
|
||||
|
||||
这种特性可以被用来匹配或者忽略一个或多个分支。因为 Swift 的 `switch` 需要包含所有的分支而且不允许有为空的分支,有时为了使你的意图更明显,需要特意匹配或者忽略某个分支。那么当你想忽略某个分支时,可以在该分支内写上 `break` 语句。当那个分支被匹配到时,分支内的 `break` 语句立即结束 `switch` 代码块。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 当一个 `switch` 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 `switch` 分支达到被忽略的效果。你应该使用 `break` 来忽略某个分支。
|
||||
|
||||
下面的例子通过 `switch` 来判断一个 `Character` 值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。
|
||||
|
||||
```swift
|
||||
let numberSymbol: Character = "三" // 简体中文里的数字 3
|
||||
var possibleIntegerValue: Int?
|
||||
switch numberSymbol {
|
||||
case "1", "١", "一", "๑":
|
||||
possibleIntegerValue = 1
|
||||
case "2", "٢", "二", "๒":
|
||||
possibleIntegerValue = 2
|
||||
case "3", "٣", "三", "๓":
|
||||
possibleIntegerValue = 3
|
||||
case "4", "٤", "四", "๔":
|
||||
possibleIntegerValue = 4
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let integerValue = possibleIntegerValue {
|
||||
print("The integer value of \(numberSymbol) is \(integerValue).")
|
||||
} else {
|
||||
print("An integer value could not be found for \(numberSymbol).")
|
||||
}
|
||||
// 输出“The integer value of 三 is 3.”
|
||||
```
|
||||
|
||||
这个例子检查 `numberSymbol` 是否是拉丁,阿拉伯,中文或者泰语中的 `1` 到 `4` 之一。如果被匹配到,该 `switch` 分支语句给 `Int?` 类型变量 `possibleIntegerValue` 设置一个整数值。
|
||||
|
||||
当 `switch` 代码块执行完后,接下来的代码通过使用可选绑定来判断 `possibleIntegerValue` 是否曾经被设置过值。因为是可选类型的缘故,`possibleIntegerValue` 有一个隐式的初始值 `nil`,所以仅仅当 `possibleIntegerValue` 曾被 `switch` 代码块的前四个分支中的某个设置过一个值时,可选的绑定才会被判定为成功。
|
||||
|
||||
在上面的例子中,想要把 `Character` 所有的的可能性都枚举出来是不现实的,所以使用 `default` 分支来包含所有上面没有匹配到字符的情况。由于这个 `default` 分支不需要执行任何动作,所以它只写了一条 `break` 语句。一旦落入到 `default` 分支中后,`break` 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 `if let` 语句。
|
||||
|
||||
### 贯穿(Fallthrough)
|
||||
|
||||
在 Swift 里,`switch` 语句不会从上一个 case 分支跳转到下一个 case 分支中。相反,只要第一个匹配到的 case 分支完成了它需要执行的语句,整个 `switch` 代码块完成了它的执行。相比之下,C 语言要求你显式地插入 `break` 语句到每个 case 分支的末尾来阻止自动落入到下一个 case 分支中。Swift 的这种避免默认落入到下一个分支中的特性意味着它的 `switch` 功能要比 C 语言的更加清晰和可预测,可以避免无意识地执行多个 case 分支从而引发的错误。
|
||||
|
||||
如果你确实需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 `fallthrough` 关键字。下面的例子使用 `fallthrough` 来创建一个数字的描述语句。
|
||||
|
||||
```swift
|
||||
let integerToDescribe = 5
|
||||
var description = "The number \(integerToDescribe) is"
|
||||
switch integerToDescribe {
|
||||
case 2, 3, 5, 7, 11, 13, 17, 19:
|
||||
description += " a prime number, and also"
|
||||
fallthrough
|
||||
default:
|
||||
description += " an integer."
|
||||
}
|
||||
print(description)
|
||||
// 输出“The number 5 is a prime number, and also an integer.”
|
||||
```
|
||||
|
||||
这个例子定义了一个 `String` 类型的变量 `description` 并且给它设置了一个初始值。函数使用 `switch` 逻辑来判断 `integerToDescribe` 变量的值。当 `integerToDescribe` 的值属于列表中的质数之一时,该函数在 `description` 后添加一段文字,来表明这个数字是一个质数。然后它使用 `fallthrough` 关键字来“贯穿”到 `default` 分支中。`default` 分支在 `description` 的最后添加一段额外的文字,至此 `switch` 代码块执行完了。
|
||||
|
||||
如果 `integerToDescribe` 的值不属于列表中的任何质数,那么它不会匹配到第一个 `switch` 分支。而这里没有其他特别的分支情况,所以 `integerToDescribe` 匹配到 `default` 分支中。
|
||||
|
||||
当 `switch` 代码块执行完后,使用 `print(_:separator:terminator:)` 函数打印该数字的描述。在这个例子中,数字 `5` 被准确的识别为了一个质数。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `fallthrough` 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。`fallthrough` 简单地使代码继续连接到下一个 case 中的代码,这和 C 语言标准中的 `switch` 语句特性是一样的。
|
||||
|
||||
### 带标签的语句
|
||||
|
||||
在 Swift 中,你可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且,循环体和条件语句都可以使用 `break` 语句来提前结束整个代码块。因此,显式地指明 `break` 语句想要终止的是哪个循环体或者条件语句,会很有用。类似地,如果你有许多嵌套的循环体,显式指明 `continue`语句想要影响哪一个循环体也会非常有用。
|
||||
|
||||
为了实现这个目的,你可以使用标签(*statement label*)来标记一个循环体或者条件语句,对于一个条件语句,你可以使用 `break` 加标签的方式,来结束这个被标记的语句。对于一个循环语句,你可以使用 `break` 或者 `continue` 加标签,来结束或者继续这条被标记语句的执行。
|
||||
|
||||
声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签,作为这个语句的前导关键字(introducor keyword),并且该标签后面跟随一个冒号。下面是一个针对 `while` 循环体的标签语法,同样的规则适用于所有的循环体和条件语句。
|
||||
|
||||
```swift
|
||||
label name: while condition {
|
||||
statements
|
||||
}
|
||||
```
|
||||
|
||||
下面的例子是前面章节中*蛇和梯子*的适配版本,在此版本中,我们将使用一个带有标签的 `while` 循环体中调用 `break` 和 `continue` 语句。这次,游戏增加了一条额外的规则:
|
||||
|
||||
- 为了获胜,你必须*刚好*落在第 25 个方块中。
|
||||
|
||||
如果某次掷骰子使你的移动超出第 25 个方块,你必须重新掷骰子,直到你掷出的骰子数刚好使你能落在第 25 个方块中。
|
||||
|
||||
游戏的棋盘和之前一样:
|
||||
|
||||

|
||||
|
||||
`finalSquare`、`board`、`square` 和 `diceRoll` 值被和之前一样的方式初始化:
|
||||
|
||||
```swift
|
||||
let finalSquare = 25
|
||||
var board = [Int](repeating: 0, count: finalSquare + 1)
|
||||
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
|
||||
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
|
||||
var square = 0
|
||||
var diceRoll = 0
|
||||
```
|
||||
|
||||
这个版本的游戏使用 `while` 循环和 `switch` 语句来实现游戏的逻辑。`while` 循环有一个标签名 `gameLoop`,来表明它是游戏的主循环。
|
||||
|
||||
该 `while` 循环体的条件判断语句是 `while square !=finalSquare`,这表明你必须刚好落在方格25中。
|
||||
|
||||
```swift
|
||||
gameLoop: while square != finalSquare {
|
||||
diceRoll += 1
|
||||
if diceRoll == 7 { diceRoll = 1 }
|
||||
switch square + diceRoll {
|
||||
case finalSquare:
|
||||
// 骰子数刚好使玩家移动到最终的方格里,游戏结束。
|
||||
break gameLoop
|
||||
case let newSquare where newSquare > finalSquare:
|
||||
// 骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子
|
||||
continue gameLoop
|
||||
default:
|
||||
// 合法移动,做正常的处理
|
||||
square += diceRoll
|
||||
square += board[square]
|
||||
}
|
||||
}
|
||||
print("Game over!")
|
||||
```
|
||||
|
||||
每次循环迭代开始时掷骰子。与之前玩家掷完骰子就立即移动不同,这里使用了 `switch` 语句来考虑每次移动可能产生的结果,从而决定玩家本次是否能够移动。
|
||||
|
||||
- 如果骰子数刚好使玩家移动到最终的方格里,游戏结束。`break gameLoop` 语句跳转控制去执行 `while` 循环体后的第一行代码,意味着游戏结束。
|
||||
- 如果骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子。`continue gameLoop` 语句结束本次 `while` 循环,开始下一次循环。
|
||||
- 在剩余的所有情况中,骰子数产生的都是合法的移动。玩家向前移动 `diceRoll` 个方格,然后游戏逻辑再处理玩家当前是否处于蛇头或者梯子的底部。接着本次循环结束,控制跳转到 `while` 循环体的条件判断语句处,再决定是否需要继续执行下次循环。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果上述的 `break` 语句没有使用 `gameLoop` 标签,那么它将会中断 `switch` 语句而不是 `while` 循环。使用 `gameLoop` 标签清晰的表明了 `break` 想要中断的是哪个代码块。
|
||||
>
|
||||
> 同时请注意,当调用 `continue gameLoop` 去跳转到下一次循环迭代时,这里使用 `gameLoop` 标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以 `continue` 语句会影响到哪个循环体是没有歧义的。然而,`continue` 语句使用 `gameLoop` 标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的 `break gameLoop`,能够使游戏的逻辑更加清晰和易于理解。
|
||||
|
||||
## 提前退出
|
||||
|
||||
像 `if` 语句一样,`guard` 的执行取决于一个表达式的布尔值。我们可以使用 `guard` 语句来要求条件必须为真时,以执行 `guard` 语句后的代码。不同于 `if` 语句,一个 `guard` 语句总是有一个 `else`从句,如果条件不为真则执行 `else` 从句中的代码。
|
||||
|
||||
```swift
|
||||
func greet(person: [String: String]) {
|
||||
guard let name = person["name"] else {
|
||||
return
|
||||
}
|
||||
|
||||
print("Hello \(name)!")
|
||||
|
||||
guard let location = person["location"] else {
|
||||
print("I hope the weather is nice near you.")
|
||||
return
|
||||
}
|
||||
|
||||
print("I hope the weather is nice in \(location).")
|
||||
}
|
||||
|
||||
greet(person: ["name": "John"])
|
||||
// 输出“Hello John!”
|
||||
// 输出“I hope the weather is nice near you.”
|
||||
greet(person: ["name": "Jane", "location": "Cupertino"])
|
||||
// 输出“Hello Jane!”
|
||||
// 输出“I hope the weather is nice in Cupertino.”
|
||||
```
|
||||
|
||||
如果 `guard` 语句的条件被满足,则继续执行 `guard` 语句大括号后的代码。将变量或者常量的可选绑定作为 `guard` 语句的条件,都可以保护 `guard` 语句后面的代码。
|
||||
|
||||
如果条件不被满足,在 `else` 分支上的代码就会被执行。这个分支必须转移控制以退出 `guard` 语句出现的代码段。它可以用控制转移语句如 `return`、`break`、`continue` 或者 `throw` 做这件事,或者调用一个不返回的方法或函数,例如 `fatalError()`。
|
||||
|
||||
相比于可以实现同样功能的 `if` 语句,按需使用 `guard` 语句会提升我们代码的可读性。它可以使你的代码连贯的被执行而不需要将它包在 `else` 块中,它可以使你在紧邻条件判断的地方,处理违规的情况。
|
||||
|
||||
## 检测 API 可用性
|
||||
|
||||
Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。
|
||||
|
||||
编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。
|
||||
|
||||
我们在 `if` 或 `guard` 语句中使用 `可用性条件(availability condition)`去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。
|
||||
|
||||
```swift
|
||||
if #available(iOS 10, macOS 10.12, *) {
|
||||
// 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API
|
||||
} else {
|
||||
// 使用先前版本的 iOS 和 macOS 的 API
|
||||
}
|
||||
```
|
||||
|
||||
以上可用性条件指定,`if` 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,`*`,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本,if 语句的代码块将会运行。
|
||||
|
||||
在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 `iOS`,`macOS`,`watchOS` 和 `tvOS`——请访问 [声明属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter3/06_Attributes.html) 来获取完整列表。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。
|
||||
|
||||
```swift
|
||||
if #available(平台名称 版本号, ..., *) {
|
||||
APIs 可用,语句将执行
|
||||
} else {
|
||||
APIs 不可用,语句将不执行
|
||||
}
|
||||
```
|
||||
|
||||
519
IOS/Task00:Swift基础语法学习/6.函数.md
Normal file
@@ -0,0 +1,519 @@
|
||||
# 函数
|
||||
|
||||
*函数*是一段完成特定任务的独立代码片段。你可以通过给函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来完成它的任务。
|
||||
|
||||
Swift 统一的函数语法非常的灵活,可以用来表示任何函数,包括从最简单的没有参数名字的 C 风格函数,到复杂的带局部和外部参数名的 Objective-C 风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数,也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。
|
||||
|
||||
在 Swift 中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型。你可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。
|
||||
|
||||
## 函数的定义与调用
|
||||
|
||||
当你定义一个函数时,你可以定义一个或多个有名字和类型的值,作为函数的输入,称为*参数*,也可以定义某种类型的值作为函数执行结束时的输出,称为*返回类型*。
|
||||
|
||||
每个函数有个*函数名*,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作*实参*)。函数的实参必须与函数参数表里参数的顺序一致。
|
||||
|
||||
下面例子中的函数的名字是 `greet(person:)`,之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,你需要定义一个输入参数——一个叫做 `person` 的 `String` 值,和一个包含给这个人问候语的 `String` 类型的返回值:
|
||||
|
||||
```swift
|
||||
func greet(person: String) -> String {
|
||||
let greeting = "Hello, " + person + "!"
|
||||
return greeting
|
||||
}
|
||||
```
|
||||
|
||||
所有的这些信息汇总起来成为函数的*定义*,并以 `func` 作为前缀。指定函数返回类型时,用返回箭头 `->`(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。
|
||||
|
||||
该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用:
|
||||
|
||||
```swift
|
||||
print(greet(person: "Anna"))
|
||||
// 打印“Hello, Anna!”
|
||||
print(greet(person: "Brian"))
|
||||
// 打印“Hello, Brian!”
|
||||
```
|
||||
|
||||
调用 `greet(person:)` 函数时,在圆括号中传给它一个 `String` 类型的实参,例如 `greet(person: "Anna")`。正如上面所示,因为这个函数返回一个 `String` 类型的值,所以 `greet` 可以被包含在 `print(_:separator:terminator:)` 的调用中,用来输出这个函数的返回值。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> `print(_:separator:terminator:)` 函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此是可选的。关于这些函数语法上的变化详见下方关于 函数参数标签和参数名以及默认参数值。
|
||||
|
||||
在 `greet(person:)` 的函数体中,先定义了一个新的名为 `greeting` 的 `String` 常量,同时,把对 `personName` 的问候消息赋值给了 `greeting` 。然后用 `return` 关键字把这个问候返回出去。一旦 `return greeting` 被调用,该函数结束它的执行并返回 `greeting` 的当前值。
|
||||
|
||||
你可以用不同的输入值多次调用 `greet(person:)`。上面的例子展示的是用 `"Anna"` 和 `"Brian"` 调用的结果,该函数分别返回了不同的结果。
|
||||
|
||||
为了简化这个函数的定义,可以将问候消息的创建和返回写成一句:
|
||||
|
||||
```swift
|
||||
func greetAgain(person: String) -> String {
|
||||
return "Hello again, " + person + "!"
|
||||
}
|
||||
print(greetAgain(person: "Anna"))
|
||||
// 打印“Hello again, Anna!”
|
||||
```
|
||||
|
||||
## 函数参数与返回值
|
||||
|
||||
函数参数与返回值在 Swift 中非常的灵活。你可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。
|
||||
|
||||
### 无参数函数
|
||||
|
||||
函数可以没有参数。下面这个函数就是一个无参数函数,当被调用时,它返回固定的 `String` 消息:
|
||||
|
||||
```swift
|
||||
func sayHelloWorld() -> String {
|
||||
return "hello, world"
|
||||
}
|
||||
print(sayHelloWorld())
|
||||
// 打印“hello, world”
|
||||
```
|
||||
|
||||
尽管这个函数没有参数,但是定义中在函数名后还是需要一对圆括号。当被调用时,也需要在函数名后写一对圆括号。
|
||||
|
||||
### 多参数函数
|
||||
|
||||
函数可以有多种输入参数,这些参数被包含在函数的括号之中,以逗号分隔。
|
||||
|
||||
下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当问候语:
|
||||
|
||||
```swift
|
||||
func greet(person: String, alreadyGreeted: Bool) -> String {
|
||||
if alreadyGreeted {
|
||||
return greetAgain(person: person)
|
||||
} else {
|
||||
return greet(person: person)
|
||||
}
|
||||
}
|
||||
print(greet(person: "Tim", alreadyGreeted: true))
|
||||
// 打印“Hello again, Tim!”
|
||||
```
|
||||
|
||||
你可以通过在括号内使用逗号分隔来传递一个 `String` 参数值和一个标识为 `alreadyGreeted` 的 `Bool` 值,来调用 `greet(person:alreadyGreeted:)` 函数。注意这个函数和上面 `greet(person:)`是不同的。虽然它们都有着同样的名字 `greet`,但是 `greet(person:alreadyGreeted:)` 函数需要两个参数,而 `greet(person:)` 只需要一个参数。
|
||||
|
||||
### 无返回值函数
|
||||
|
||||
函数可以没有返回值。下面是 `greet(person:)` 函数的另一个版本,这个函数直接打印一个 `String`值,而不是返回它:
|
||||
|
||||
```swift
|
||||
func greet(person: String) {
|
||||
print("Hello, \(person)!")
|
||||
}
|
||||
greet(person: "Dave")
|
||||
// 打印“Hello, Dave!”
|
||||
```
|
||||
|
||||
因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头(->)和返回类型。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 严格地说,即使没有明确定义返回值,该 `greet(Person:)` 函数仍然返回一个值。没有明确定义返回类型的函数的返回一个 `Void` 类型特殊值,该值为一个空元组,写成 ()。
|
||||
|
||||
调用函数时,可以忽略该函数的返回值:
|
||||
|
||||
```swift
|
||||
func printAndCount(string: String) -> Int {
|
||||
print(string)
|
||||
return string.count
|
||||
}
|
||||
func printWithoutCounting(string: String) {
|
||||
let _ = printAndCount(string: string)
|
||||
}
|
||||
printAndCount(string: "hello, world")
|
||||
// 打印“hello, world”,并且返回值 12
|
||||
printWithoutCounting(string: "hello, world")
|
||||
// 打印“hello, world”,但是没有返回任何值
|
||||
```
|
||||
|
||||
第一个函数 `printAndCount(string:)`,输出一个字符串并返回 `Int` 类型的字符数。第二个函数 `printWithoutCounting(string:)` 调用了第一个函数,但是忽略了它的返回值。当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 返回值可以被忽略,但定义了有返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译时错误。
|
||||
|
||||
### 多重返回值函数
|
||||
|
||||
你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。
|
||||
|
||||
下例中定义了一个名为 `minMax(array:)` 的函数,作用是在一个 `Int` 类型的数组中找出最小值与最大值。
|
||||
|
||||
```swift
|
||||
func minMax(array: [Int]) -> (min: Int, max: Int) {
|
||||
var currentMin = array[0]
|
||||
var currentMax = array[0]
|
||||
for value in array[1..<array.count] {
|
||||
if value < currentMin {
|
||||
currentMin = value
|
||||
} else if value > currentMax {
|
||||
currentMax = value
|
||||
}
|
||||
}
|
||||
return (currentMin, currentMax)
|
||||
}
|
||||
```
|
||||
|
||||
`minMax(array:)` 函数返回一个包含两个 `Int` 值的元组,这些值被标记为 `min` 和 `max` ,以便查询函数的返回值时可以通过名字访问它们。
|
||||
|
||||
在 `minMax(array:)` 的函数体中,在开始的时候设置两个工作变量 `currentMin` 和 `currentMax` 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 `currentMin` 和 `currentMax`更小或更大。最后数组中的最小值与最大值作为一个包含两个 `Int` 值的元组返回。
|
||||
|
||||
因为元组的成员值已被命名,因此可以通过 `.` 语法来检索找到的最小值与最大值:
|
||||
|
||||
```swift
|
||||
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
|
||||
print("min is \(bounds.min) and max is \(bounds.max)")
|
||||
// 打印“min is -6 and max is 109”
|
||||
```
|
||||
|
||||
需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。
|
||||
|
||||
### 可选元组返回类型
|
||||
|
||||
如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用*可选的* 元组返回类型反映整个元组可以是 `nil` 的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如 `(Int, Int)?` 或 `(String, Int, Bool)?`
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 可选元组类型如 `(Int, Int)?` 与元组包含可选类型如 `(Int?, Int?)` 是不同的。可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。
|
||||
|
||||
前面的 `minMax(array:)` 函数返回了一个包含两个 `Int` 值的元组。但是函数不会对传入的数组执行任何安全检查,如果 `array` 参数是一个空数组,如上定义的 `minMax(array:)` 在试图访问 `array[0]`时会触发一个运行时错误。
|
||||
|
||||
为了安全地处理这个“空数组”问题,将 `minMax(array:)` 函数改写为使用可选元组返回类型,并且当数组为空时返回 `nil`:
|
||||
|
||||
```swift
|
||||
func minMax(array: [Int]) -> (min: Int, max: Int)? {
|
||||
if array.isEmpty { return nil }
|
||||
var currentMin = array[0]
|
||||
var currentMax = array[0]
|
||||
for value in array[1..<array.count] {
|
||||
if value < currentMin {
|
||||
currentMin = value
|
||||
} else if value > currentMax {
|
||||
currentMax = value
|
||||
}
|
||||
}
|
||||
return (currentMin, currentMax)
|
||||
}
|
||||
```
|
||||
|
||||
你可以使用可选绑定来检查 `minMax(array:)` 函数返回的是一个存在的元组值还是 `nil`:
|
||||
|
||||
```swift
|
||||
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
|
||||
print("min is \(bounds.min) and max is \(bounds.max)")
|
||||
}
|
||||
// 打印“min is -6 and max is 109”
|
||||
```
|
||||
|
||||
### 隐式返回的函数
|
||||
|
||||
如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:
|
||||
|
||||
```
|
||||
func greeting(for person: String) -> String {
|
||||
"Hello, " + person + "!"
|
||||
}
|
||||
print(greeting(for: "Dave"))
|
||||
// 打印 "Hello, Dave!"
|
||||
|
||||
func anotherGreeting(for person: String) -> String {
|
||||
return "Hello, " + person + "!"
|
||||
}
|
||||
print(anotherGreeting(for: "Dave"))
|
||||
// 打印 "Hello, Dave!"
|
||||
```
|
||||
|
||||
`greeting(for:)` 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。`anothergreeting(for:)` 函数返回同样的内容,却因为 `return` 关键字显得函数更长。任何一个可以被写成一行 `return` 语句的函数都可以忽略 `return`。
|
||||
|
||||
正如你将会在 [简略的 Getter 声明](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html) 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。
|
||||
|
||||
## 函数参数标签和参数名称
|
||||
|
||||
每个函数参数都有一个*参数标签(argument label)*以及一个*参数名称(parameter name)*。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签。
|
||||
|
||||
```swift
|
||||
func someFunction(firstParameterName: Int, secondParameterName: Int) {
|
||||
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
|
||||
}
|
||||
someFunction(firstParameterName: 1, secondParameterName: 2)
|
||||
```
|
||||
|
||||
所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是可能的,但是一个唯一的函数标签能够使你的代码更具可读性。
|
||||
|
||||
### 指定参数标签
|
||||
|
||||
你可以在参数名称前指定它的参数标签,中间以空格分隔:
|
||||
|
||||
```swift
|
||||
func someFunction(argumentLabel parameterName: Int) {
|
||||
// 在函数体内,parameterName 代表参数值
|
||||
}
|
||||
```
|
||||
|
||||
这个版本的 `greet(person:)` 函数,接收一个人的名字和他的家乡,并且返回一句问候:
|
||||
|
||||
```swift
|
||||
func greet(person: String, from hometown: String) -> String {
|
||||
return "Hello \(person)! Glad you could visit from \(hometown)."
|
||||
}
|
||||
print(greet(person: "Bill", from: "Cupertino"))
|
||||
// 打印“Hello Bill! Glad you could visit from Cupertino.”
|
||||
```
|
||||
|
||||
参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。
|
||||
|
||||
### 忽略参数标签
|
||||
|
||||
如果你不希望为某个参数添加一个标签,可以使用一个下划线(`_`)来代替一个明确的参数标签。
|
||||
|
||||
```swift
|
||||
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
|
||||
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
|
||||
}
|
||||
someFunction(1, secondParameterName: 2)
|
||||
```
|
||||
|
||||
如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。
|
||||
|
||||
### 默认参数值
|
||||
|
||||
你可以在函数体中通过给参数赋值来为任意一个参数定义*默认值(Deafult Value)*。当默认值被定义后,调用这个函数时可以忽略这个参数。
|
||||
|
||||
```swift
|
||||
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
|
||||
// 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。
|
||||
}
|
||||
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
|
||||
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12
|
||||
```
|
||||
|
||||
将不带有默认值的参数放在函数参数列表的最前。一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前保证在函数调用时,非默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。
|
||||
|
||||
### 可变参数
|
||||
|
||||
一个*可变参数(variadic parameter)*可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(`...`)的方式来定义可变参数。
|
||||
|
||||
可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 `numbers` 的 `Double...` 型可变参数,在函数体内可以当做一个叫 `numbers` 的 `[Double]` 型的数组常量。
|
||||
|
||||
下面的这个函数用来计算一组任意长度数字的 *算术平均数(arithmetic mean)*:
|
||||
|
||||
```swift
|
||||
func arithmeticMean(_ numbers: Double...) -> Double {
|
||||
var total: Double = 0
|
||||
for number in numbers {
|
||||
total += number
|
||||
}
|
||||
return total / Double(numbers.count)
|
||||
}
|
||||
arithmeticMean(1, 2, 3, 4, 5)
|
||||
// 返回 3.0, 是这 5 个数的平均数。
|
||||
arithmeticMean(3, 8.25, 18.75)
|
||||
// 返回 10.0, 是这 3 个数的平均数。
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 一个函数最多只能拥有一个可变参数。
|
||||
|
||||
### 输入输出参数
|
||||
|
||||
函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为*输入输出参数(In-Out Parameters)*。
|
||||
|
||||
定义一个输入输出参数时,在参数定义前加 `inout` 关键字。一个 `输入输出参数`有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 [输入输出参数](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/doc/uid/TP40014097-CH34-ID545) 一节。
|
||||
|
||||
你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 `&` 符,表示这个值可以被函数修改。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 输入输出参数不能有默认值,而且可变参数不能用 `inout` 标记。
|
||||
|
||||
下例中,`swapTwoInts(_:_:)` 函数有两个分别叫做 `a` 和 `b` 的输入输出参数:
|
||||
|
||||
```swift
|
||||
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
|
||||
let temporaryA = a
|
||||
a = b
|
||||
b = temporaryA
|
||||
}
|
||||
```
|
||||
|
||||
`swapTwoInts(_:_:)` 函数简单地交换 `a` 与 `b` 的值。该函数先将 `a` 的值存到一个临时常量 `temporaryA` 中,然后将 `b` 的值赋给 `a`,最后将 `temporaryA` 赋值给 `b`。
|
||||
|
||||
你可以用两个 `Int` 型的变量来调用 `swapTwoInts(_:_:)`。需要注意的是,`someInt` 和 `anotherInt` 在传入 `swapTwoInts(_:_:)` 函数前,都加了 `&` 的前缀:
|
||||
|
||||
```swift
|
||||
var someInt = 3
|
||||
var anotherInt = 107
|
||||
swapTwoInts(&someInt, &anotherInt)
|
||||
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
|
||||
// 打印“someInt is now 107, and anotherInt is now 3”
|
||||
```
|
||||
|
||||
从上面这个例子中,我们可以看到 `someInt` 和 `anotherInt` 的原始值在 `swapTwoInts(_:_:)` 函数中被修改,尽管它们的定义在函数体外。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 输入输出参数和返回值是不一样的。上面的 `swapTwoInts` 函数并没有定义任何返回值,但仍然修改了 `someInt` 和 `anotherInt` 的值。输入输出参数是函数对函数体外产生影响的另一种方式。
|
||||
|
||||
## 函数类型
|
||||
|
||||
每个函数都有种特定的*函数类型*,函数的类型由函数的参数类型和返回类型组成。
|
||||
|
||||
例如:
|
||||
|
||||
```swift
|
||||
func addTwoInts(_ a: Int, _ b: Int) -> Int {
|
||||
return a + b
|
||||
}
|
||||
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
|
||||
return a * b
|
||||
}
|
||||
```
|
||||
|
||||
这个例子中定义了两个简单的数学函数:`addTwoInts` 和 `multiplyTwoInts`。这两个函数都接受两个 `Int` 值, 返回一个 `Int` 值。
|
||||
|
||||
这两个函数的类型是 `(Int, Int) -> Int`,可以解读为:
|
||||
|
||||
“这个函数类型有两个 `Int` 型的参数并返回一个 `Int` 型的值”。
|
||||
|
||||
下面是另一个例子,一个没有参数,也没有返回值的函数:
|
||||
|
||||
```swift
|
||||
func printHelloWorld() {
|
||||
print("hello, world")
|
||||
}
|
||||
```
|
||||
|
||||
这个函数的类型是:`() -> Void`,或者叫“没有参数,并返回 `Void` 类型的函数”。
|
||||
|
||||
### 使用函数类型
|
||||
|
||||
在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:
|
||||
|
||||
```swift
|
||||
var mathFunction: (Int, Int) -> Int = addTwoInts
|
||||
```
|
||||
|
||||
这段代码可以被解读为:
|
||||
|
||||
”定义一个叫做 `mathFunction` 的变量,类型是‘一个有两个 `Int` 型的参数并返回一个 `Int` 型的值的函数’,并让这个新变量指向 `addTwoInts` 函数”。
|
||||
|
||||
`addTwoInts` 和 `mathFunction` 有同样的类型,所以这个赋值过程在 Swift 类型检查(type-check)中是允许的。
|
||||
|
||||
现在,你可以用 `mathFunction` 来调用被赋值的函数了:
|
||||
|
||||
```swift
|
||||
print("Result: \(mathFunction(2, 3))")
|
||||
// Prints "Result: 5"
|
||||
```
|
||||
|
||||
有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:
|
||||
|
||||
```swift
|
||||
mathFunction = multiplyTwoInts
|
||||
print("Result: \(mathFunction(2, 3))")
|
||||
// Prints "Result: 6"
|
||||
```
|
||||
|
||||
就像其他类型一样,当赋值一个函数给常量或变量时,你可以让 Swift 来推断其函数类型:
|
||||
|
||||
```swift
|
||||
let anotherMathFunction = addTwoInts
|
||||
// anotherMathFunction 被推断为 (Int, Int) -> Int 类型
|
||||
```
|
||||
|
||||
### 函数类型作为参数类型
|
||||
|
||||
你可以用 `(Int, Int) -> Int` 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。
|
||||
|
||||
下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果:
|
||||
|
||||
```swift
|
||||
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
|
||||
print("Result: \(mathFunction(a, b))")
|
||||
}
|
||||
printMathResult(addTwoInts, 3, 5)
|
||||
// 打印“Result: 8”
|
||||
```
|
||||
|
||||
这个例子定义了 `printMathResult(_:_:_:)` 函数,它有三个参数:第一个参数叫 `mathFunction`,类型是 `(Int, Int) -> Int`,你可以传入任何这种类型的函数;第二个和第三个参数叫 `a` 和 `b`,它们的类型都是 `Int`,这两个值作为已给出的函数的输入值。
|
||||
|
||||
当 `printMathResult(_:_:_:)` 被调用时,它被传入 `addTwoInts` 函数和整数 `3` 和 `5`。它用传入 `3` 和 `5` 调用 `addTwoInts`,并输出结果:`8`。
|
||||
|
||||
`printMathResult(_:_:_:)` 函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型。这使得 `printMathResult(_:_:_:)` 能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。
|
||||
|
||||
### 函数类型作为返回类型
|
||||
|
||||
你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。
|
||||
|
||||
下面的这个例子中定义了两个简单函数,分别是 `stepForward(_:)` 和 `stepBackward(_:)`。`stepForward(_:)` 函数返回一个比输入值大 `1` 的值。`stepBackward(_:)` 函数返回一个比输入值小 `1` 的值。这两个函数的类型都是 `(Int) -> Int`:
|
||||
|
||||
```swift
|
||||
func stepForward(_ input: Int) -> Int {
|
||||
return input + 1
|
||||
}
|
||||
func stepBackward(_ input: Int) -> Int {
|
||||
return input - 1
|
||||
}
|
||||
```
|
||||
|
||||
如下名为 `chooseStepFunction(backward:)` 的函数,它的返回类型是 `(Int) -> Int` 类型的函数。`chooseStepFunction(backward:)` 根据布尔值 `backwards` 来返回 `stepForward(_:)` 函数或 `stepBackward(_:)` 函数:
|
||||
|
||||
```swift
|
||||
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
|
||||
return backward ? stepBackward : stepForward
|
||||
}
|
||||
```
|
||||
|
||||
你现在可以用 `chooseStepFunction(backward:)` 来获得两个函数其中的一个:
|
||||
|
||||
```swift
|
||||
var currentValue = 3
|
||||
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
|
||||
// moveNearerToZero 现在指向 stepBackward() 函数。
|
||||
```
|
||||
|
||||
上面这个例子中计算出从 `currentValue` 逐渐接近到0是需要向正数走还是向负数走。`currentValue`的初始值是 `3`,这意味着 `currentValue > 0` 为真(true),这将使得 `chooseStepFunction(_:)` 返回 `stepBackward(_:)` 函数。一个指向返回的函数的引用保存在了 `moveNearerToZero` 常量中。
|
||||
|
||||
现在,`moveNearerToZero` 指向了正确的函数,它可以被用来数到零:
|
||||
|
||||
```swift
|
||||
print("Counting to zero:")
|
||||
// Counting to zero:
|
||||
while currentValue != 0 {
|
||||
print("\(currentValue)... ")
|
||||
currentValue = moveNearerToZero(currentValue)
|
||||
}
|
||||
print("zero!")
|
||||
// 3...
|
||||
// 2...
|
||||
// 1...
|
||||
// zero!
|
||||
```
|
||||
|
||||
## 嵌套函数
|
||||
|
||||
到目前为止本章中你所见到的所有函数都叫*全局函数(global functions)*,它们定义在全局域中。你也可以把函数定义在别的函数体中,称作 *嵌套函数(nested functions)*。
|
||||
|
||||
默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。
|
||||
|
||||
你可以用返回嵌套函数的方式重写 `chooseStepFunction(backward:)` 函数:
|
||||
|
||||
```swift
|
||||
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
|
||||
func stepForward(input: Int) -> Int { return input + 1 }
|
||||
func stepBackward(input: Int) -> Int { return input - 1 }
|
||||
return backward ? stepBackward : stepForward
|
||||
}
|
||||
var currentValue = -4
|
||||
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
|
||||
// moveNearerToZero now refers to the nested stepForward() function
|
||||
while currentValue != 0 {
|
||||
print("\(currentValue)... ")
|
||||
currentValue = moveNearerToZero(currentValue)
|
||||
}
|
||||
print("zero!")
|
||||
// -4...
|
||||
// -3...
|
||||
// -2...
|
||||
// -1...
|
||||
// zero!
|
||||
```
|
||||
|
||||
420
IOS/Task00:Swift基础语法学习/7.闭包.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# 闭包
|
||||
|
||||
*闭包*是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。
|
||||
|
||||
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为*包裹*常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你不熟悉捕获(capturing)这个概念也不用担心,在 [值捕获](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/07_Closures.html#capturing_values) 章节有它更详细的介绍。
|
||||
|
||||
在 [函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html) 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:
|
||||
|
||||
- 全局函数是一个有名字但不会捕获任何值的闭包
|
||||
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
|
||||
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
|
||||
|
||||
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
|
||||
|
||||
- 利用上下文推断参数和返回值类型
|
||||
- 隐式返回单表达式闭包,即单表达式闭包可以省略 `return` 关键字
|
||||
- 参数名称缩写
|
||||
- 尾随闭包语法
|
||||
|
||||
## 闭包表达式
|
||||
|
||||
[嵌套函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#Nested_Functions) 作为复杂函数的一部分时,它自包含代码块式的定义和命名形式在使用上带来了方便。当然,编写未完整声明和没有函数名的类函数结构代码是很有用的,尤其是在编码中涉及到函数作为参数的那些方法时。
|
||||
|
||||
*闭包表达式*是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。下面通过对 `sorted(by:)` 这一个案例的多次迭代改进来展示这个过程,每次迭代都使用了更加简明的方式描述了相同功能。。
|
||||
|
||||
### 排序方法
|
||||
|
||||
Swift 标准库提供了名为 `sorted(by:)` 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,`sorted(by:)` 方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被 `sorted(by:)` 方法修改。
|
||||
|
||||
下面的闭包表达式示例使用 `sorted(by:)` 方法对一个 `String` 类型的数组进行字母逆序排序。以下是初始数组:
|
||||
|
||||
```swift
|
||||
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
|
||||
```
|
||||
|
||||
`sorted(by:)` 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值*前面*,排序闭包函数需要返回 `true`,反之返回 `false`。
|
||||
|
||||
该例子对一个 `String` 类型的数组进行排序,因此排序闭包函数类型需为 `(String, String) -> Bool`。
|
||||
|
||||
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 `sorted(by:)` 方法的参数传入:
|
||||
|
||||
```swift
|
||||
func backward(_ s1: String, _ s2: String) -> Bool {
|
||||
return s1 > s2
|
||||
}
|
||||
var reversedNames = names.sorted(by: backward)
|
||||
// reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
|
||||
```
|
||||
|
||||
如果第一个字符串(`s1`)大于第二个字符串(`s2`),`backward(_:_:)` 函数会返回 `true`,表示在新的数组中 `s1` 应该出现在 `s2` 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 `"B"` 大于字母 `"A"` ,字符串 `"Tom"` 大于字符串 `"Tim"`。该闭包将进行字母逆序排序,`"Barry"` 将会排在 `"Alex"` 之前。
|
||||
|
||||
然而,以这种方式来编写一个实际上很简单的表达式(`a > b`),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好地构造一个内联排序闭包。
|
||||
|
||||
### 闭包表达式语法
|
||||
|
||||
闭包表达式语法有如下的一般形式:
|
||||
|
||||
```swift
|
||||
{ (parameters) -> return type in
|
||||
statements
|
||||
}
|
||||
```
|
||||
|
||||
*闭包表达式参数* 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。
|
||||
|
||||
下面的例子展示了之前 `backward(_:_:)` 函数对应的闭包表达式版本的代码:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
|
||||
return s1 > s2
|
||||
})
|
||||
```
|
||||
|
||||
需要注意的是内联闭包参数和返回值类型声明与 `backward(_:_:)` 函数类型声明相同。在这两种方式中,都写成了 `(s1: String, s2: String) -> Bool`。然而在内联闭包表达式中,函数和返回值类型都写在*大括号内*,而不是大括号外。
|
||||
|
||||
闭包的函数体部分由关键字 `in` 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
|
||||
|
||||
由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
|
||||
```
|
||||
|
||||
该例中 `sorted(by:)` 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。
|
||||
|
||||
### 根据上下文推断类型
|
||||
|
||||
因为排序闭包函数是作为 `sorted(by:)` 方法的参数传入的,Swift 可以推断其参数和返回值的类型。`sorted(by:)` 方法被一个字符串数组调用,因此其参数必须是 `(String, String) -> Bool` 类型的函数。这意味着 `(String, String)` 和 `Bool` 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(`->`)和围绕在参数周围的括号也可以被省略:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
|
||||
```
|
||||
|
||||
实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。
|
||||
|
||||
尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 `sorted(by:)` 方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。
|
||||
|
||||
### 单表达式闭包的隐式返回
|
||||
|
||||
单行表达式闭包可以通过省略 `return` 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
|
||||
```
|
||||
|
||||
在这个例子中,`sorted(by:)` 方法的参数类型明确了闭包必须返回一个 `Bool` 类型值。因为闭包函数体只包含了一个单一表达式(`s1 > s2`),该表达式返回 `Bool` 类型值,因此这里没有歧义,`return`关键字可以省略。
|
||||
|
||||
### 参数名称缩写
|
||||
|
||||
Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 `$0`,`$1`,`$2` 来顺序调用闭包的参数,以此类推。
|
||||
|
||||
如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。`in` 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: { $0 > $1 } )
|
||||
```
|
||||
|
||||
在这个例子中,`$0` 和 `$1` 表示闭包中第一个和第二个 `String` 类型的参数。
|
||||
|
||||
### 运算符方法
|
||||
|
||||
实际上还有一种更*简短的*方式来编写上面例子中的闭包表达式。Swift 的 `String` 类型定义了关于大于号(`>`)的字符串实现,其作为一个函数接受两个 `String` 类型的参数并返回 `Bool` 类型的值。而这正好与 `sorted(by:)` 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted(by: >)
|
||||
```
|
||||
|
||||
更多关于运算符方法的内容请查看 [运算符方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#operator_methods)。
|
||||
|
||||
## 尾随闭包
|
||||
|
||||
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
|
||||
|
||||
```swift
|
||||
func someFunctionThatTakesAClosure(closure: () -> Void) {
|
||||
// 函数体部分
|
||||
}
|
||||
|
||||
// 以下是不使用尾随闭包进行函数调用
|
||||
someFunctionThatTakesAClosure(closure: {
|
||||
// 闭包主体部分
|
||||
})
|
||||
|
||||
// 以下是使用尾随闭包进行函数调用
|
||||
someFunctionThatTakesAClosure() {
|
||||
// 闭包主体部分
|
||||
}
|
||||
```
|
||||
|
||||
在 [闭包表达式语法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/07_Closures.html#closure_expression_syntax) 上章节中的字符串排序闭包可以作为尾随包的形式改写在 `sorted(by:)` 方法圆括号的外面:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted() { $0 > $1 }
|
||||
```
|
||||
|
||||
如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 `()` 省略掉:
|
||||
|
||||
```swift
|
||||
reversedNames = names.sorted { $0 > $1 }
|
||||
```
|
||||
|
||||
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。举例来说,Swift 的 `Array` 类型有一个 `map(_:)` 方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。
|
||||
|
||||
当提供给数组的闭包应用于每个数组元素后,`map(_:)` 方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。
|
||||
|
||||
下例介绍了如何在 `map(_:)` 方法中使用尾随闭包将 `Int` 类型数组 `[16, 58, 510]` 转换为包含对应 `String` 类型的值的数组 `["OneSix", "FiveEight", "FiveOneZero"]`:
|
||||
|
||||
```swift
|
||||
let digitNames = [
|
||||
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
|
||||
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
|
||||
]
|
||||
let numbers = [16, 58, 510]
|
||||
```
|
||||
|
||||
如上代码创建了一个整型数位和它们英文版本名字相映射的字典。同时还定义了一个准备转换为字符串数组的整型数组。
|
||||
|
||||
你现在可以通过传递一个尾随闭包给 `numbers` 数组的 `map(_:)` 方法来创建对应的字符串版本数组:
|
||||
|
||||
```swift
|
||||
let strings = numbers.map {
|
||||
(number) -> String in
|
||||
var number = number
|
||||
var output = ""
|
||||
repeat {
|
||||
output = digitNames[number % 10]! + output
|
||||
number /= 10
|
||||
} while number > 0
|
||||
return output
|
||||
}
|
||||
// strings 常量被推断为字符串类型数组,即 [String]
|
||||
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
|
||||
```
|
||||
|
||||
`map(_:)` 为数组中每一个元素调用了一次闭包表达式。你不需要指定闭包的输入参数 `number` 的类型,因为可以通过要映射的数组类型进行推断。
|
||||
|
||||
在该例中,局部变量 `number` 的值由闭包中的 `number` 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 `String`,以表明存储映射值的新数组类型为 `String`。
|
||||
|
||||
闭包表达式在每次被调用的时候创建了一个叫做 `output` 的字符串并返回。其使用求余运算符(`number % 10`)计算最后一位数字并利用 `digitNames` 字典获取所映射的字符串。这个闭包能够用于创建任意正整数的字符串表示。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 字典 `digitNames` 下标后跟着一个叹号(`!`),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定 `number % 10` 总是 `digitNames` 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 `String` 类型的值。
|
||||
|
||||
从 `digitNames` 字典中获取的字符串被添加到 `output` 的*前部*,逆序建立了一个字符串版本的数字。(在表达式 `number % 10` 中,如果 `number` 为 `16`,则返回 `6`,`58` 返回 `8`,`510` 返回 `0`。)
|
||||
|
||||
`number` 变量之后除以 `10`。因为其是整数,在计算过程中未除尽部分被忽略。因此 `16` 变成了 `1`,`58` 变成了 `5`,`510` 变成了 `51`。
|
||||
|
||||
整个过程重复进行,直到 `number /= 10` 为 `0`,这时闭包会将字符串 `output` 返回,而 `map(_:)`方法则会将字符串添加到映射数组中。
|
||||
|
||||
在上面的例子中,通过尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在 `map(_:)` 方法的括号内。
|
||||
|
||||
## 值捕获
|
||||
|
||||
闭包可以在其被定义的上下文中*捕获*常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
|
||||
|
||||
Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
|
||||
|
||||
举个例子,这有一个叫做 `makeIncrementer` 的函数,其包含了一个叫做 `incrementer` 的嵌套函数。嵌套函数 `incrementer()` 从上下文中捕获了两个值,`runningTotal` 和 `amount`。捕获这些值之后,`makeIncrementer` 将 `incrementer` 作为闭包返回。每次调用 `incrementer` 时,其会以 `amount` 作为增量增加 `runningTotal` 的值。
|
||||
|
||||
```swift
|
||||
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
|
||||
var runningTotal = 0
|
||||
func incrementer() -> Int {
|
||||
runningTotal += amount
|
||||
return runningTotal
|
||||
}
|
||||
return incrementer
|
||||
}
|
||||
```
|
||||
|
||||
`makeIncrementer` 返回类型为 `() -> Int`。这意味着其返回的是一个*函数*,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个 `Int` 类型的值。关于函数返回其他函数的内容,请查看 [函数类型作为返回类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#function_types_as_return_types)。
|
||||
|
||||
`makeIncrementer(forIncrement:)` 函数定义了一个初始值为 `0` 的整型变量 `runningTotal`,用来存储当前总计数值。该值为 `incrementer` 的返回值。
|
||||
|
||||
`makeIncrementer(forIncrement:)` 有一个 `Int` 类型的参数,其外部参数名为 `forIncrement`,内部参数名为 `amount`,该参数表示每次 `incrementer` 被调用时 `runningTotal` 将要增加的量。`makeIncrementer` 函数还定义了一个嵌套函数 `incrementer`,用来执行实际的增加操作。该函数简单地使 `runningTotal` 增加 `amount`,并将其返回。
|
||||
|
||||
如果我们单独考虑嵌套函数 `incrementer()`,会发现它有些不同寻常:
|
||||
|
||||
```swift
|
||||
func incrementer() -> Int {
|
||||
runningTotal += amount
|
||||
return runningTotal
|
||||
}
|
||||
```
|
||||
|
||||
`incrementer()` 函数并没有任何参数,但是在函数体内访问了 `runningTotal` 和 `amount` 变量。这是因为它从外围函数捕获了 `runningTotal` 和 `amount` 变量的*引用*。捕获引用保证了 `runningTotal`和 `amount` 变量在调用完 `makeIncrementer` 后不会消失,并且保证了在下一次执行 `incrementer` 函数时,`runningTotal` 依旧存在。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。
|
||||
>
|
||||
> Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。
|
||||
|
||||
下面是一个使用 `makeIncrementer` 的例子:
|
||||
|
||||
```swift
|
||||
let incrementByTen = makeIncrementer(forIncrement: 10)
|
||||
```
|
||||
|
||||
该例子定义了一个叫做 `incrementByTen` 的常量,该常量指向一个每次调用会将其 `runningTotal` 变量增加 `10` 的 `incrementer` 函数。调用这个函数多次可以得到以下结果:
|
||||
|
||||
```swift
|
||||
incrementByTen()
|
||||
// 返回的值为10
|
||||
incrementByTen()
|
||||
// 返回的值为20
|
||||
incrementByTen()
|
||||
// 返回的值为30
|
||||
```
|
||||
|
||||
如果你创建了另一个 `incrementer`,它会有属于自己的引用,指向一个全新、独立的 `runningTotal`变量:
|
||||
|
||||
```swift
|
||||
let incrementBySeven = makeIncrementer(forIncrement: 7)
|
||||
incrementBySeven()
|
||||
// 返回的值为7
|
||||
```
|
||||
|
||||
再次调用原来的 `incrementByTen` 会继续增加它自己的 `runningTotal` 变量,该变量和 `incrementBySeven` 中捕获的变量没有任何联系:
|
||||
|
||||
```swift
|
||||
incrementByTen()
|
||||
// 返回的值为40
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用。更多信息,请参考 [闭包引起的循环强引用](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html#strong_reference_cycles_for_closures)。
|
||||
|
||||
## 闭包是引用类型
|
||||
|
||||
上面的例子中,`incrementBySeven` 和 `incrementByTen` 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是*引用类型*。
|
||||
|
||||
无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的*引用*。上面的例子中,指向闭包的引用 `incrementByTen` 是一个常量,而并非闭包内容本身。
|
||||
|
||||
这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
|
||||
|
||||
```swift
|
||||
let alsoIncrementByTen = incrementByTen
|
||||
alsoIncrementByTen()
|
||||
// 返回的值为50
|
||||
```
|
||||
|
||||
## 逃逸闭包
|
||||
|
||||
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中*逃逸*。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 `@escaping`,用来指明这个闭包是允许“逃逸”出这个函数的。
|
||||
|
||||
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
|
||||
|
||||
```swift
|
||||
var completionHandlers: [() -> Void] = []
|
||||
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
|
||||
completionHandlers.append(completionHandler)
|
||||
}
|
||||
```
|
||||
|
||||
`someFunctionWithEscapingClosure(_:)` 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 `@escaping`,就会得到一个编译错误。
|
||||
|
||||
将一个闭包标记为 `@escaping` 意味着你必须在闭包中显式地引用 `self`。比如说,在下面的代码中,传递到 `someFunctionWithEscapingClosure(_:)` 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 `self`。相对的,传递到 `someFunctionWithNonescapingClosure(_:)` 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 `self`。
|
||||
|
||||
```swift
|
||||
func someFunctionWithNonescapingClosure(closure: () -> Void) {
|
||||
closure()
|
||||
}
|
||||
|
||||
class SomeClass {
|
||||
var x = 10
|
||||
func doSomething() {
|
||||
someFunctionWithEscapingClosure { self.x = 100 }
|
||||
someFunctionWithNonescapingClosure { x = 200 }
|
||||
}
|
||||
}
|
||||
|
||||
let instance = SomeClass()
|
||||
instance.doSomething()
|
||||
print(instance.x)
|
||||
// 打印出“200”
|
||||
|
||||
completionHandlers.first?()
|
||||
print(instance.x)
|
||||
// 打印出“100”
|
||||
```
|
||||
|
||||
## 自动闭包
|
||||
|
||||
*自动闭包*是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
|
||||
|
||||
我们经常会*调用*采用自动闭包的函数,但是很少去*实现*这样的函数。举个例子来说,`assert(condition:message:file:line:)` 函数接受自动闭包作为它的 `condition` 参数和 `message` 参数;它的 `condition` 参数仅会在 debug 模式下被求值,它的 `message` 参数仅当 `condition` 参数为 `false` 时被计算求值。
|
||||
|
||||
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。
|
||||
|
||||
```swift
|
||||
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
|
||||
print(customersInLine.count)
|
||||
// 打印出“5”
|
||||
|
||||
let customerProvider = { customersInLine.remove(at: 0) }
|
||||
print(customersInLine.count)
|
||||
// 打印出“5”
|
||||
|
||||
print("Now serving \(customerProvider())!")
|
||||
// Prints "Now serving Chris!"
|
||||
print(customersInLine.count)
|
||||
// 打印出“4”
|
||||
```
|
||||
|
||||
尽管在闭包的代码中,`customersInLine` 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,`customerProvider` 的类型不是 `String`,而是 `() -> String`,一个没有参数且返回值为 `String` 的函数。
|
||||
|
||||
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
|
||||
|
||||
```swift
|
||||
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
|
||||
func serve(customer customerProvider: () -> String) {
|
||||
print("Now serving \(customerProvider())!")
|
||||
}
|
||||
serve(customer: { customersInLine.remove(at: 0) } )
|
||||
// 打印出“Now serving Alex!”
|
||||
```
|
||||
|
||||
上面的 `serve(customer:)` 函数接受一个返回顾客名字的显式的闭包。下面这个版本的 `serve(customer:)` 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 `@autoclosure` 来接收一个自动闭包。现在你可以将该函数当作接受 `String` 类型参数(而非闭包)的函数来调用。`customerProvider` 参数将自动转化为一个闭包,因为该参数被标记了 `@autoclosure` 特性。
|
||||
|
||||
```swift
|
||||
// customersInLine is ["Ewa", "Barry", "Daniella"]
|
||||
func serve(customer customerProvider: @autoclosure () -> String) {
|
||||
print("Now serving \(customerProvider())!")
|
||||
}
|
||||
serve(customer: customersInLine.remove(at: 0))
|
||||
// 打印“Now serving Ewa!”
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 过度使用 `autoclosures` 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。
|
||||
|
||||
如果你想让一个自动闭包可以“逃逸”,则应该同时使用 `@autoclosure` 和 `@escaping` 属性。`@escaping` 属性的讲解见上面的`逃逸闭包`。
|
||||
|
||||
```swift
|
||||
// customersInLine i= ["Barry", "Daniella"]
|
||||
var customerProviders: [() -> String] = []
|
||||
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
|
||||
customerProviders.append(customerProvider)
|
||||
}
|
||||
collectCustomerProviders(customersInLine.remove(at: 0))
|
||||
collectCustomerProviders(customersInLine.remove(at: 0))
|
||||
|
||||
print("Collected \(customerProviders.count) closures.")
|
||||
// 打印“Collected 2 closures.”
|
||||
for customerProvider in customerProviders {
|
||||
print("Now serving \(customerProvider())!")
|
||||
}
|
||||
// 打印“Now serving Barry!”
|
||||
// 打印“Now serving Daniella!”
|
||||
```
|
||||
|
||||
在上面的代码中,`collectCustomerProviders(_:)` 函数并没有调用传入的 `customerProvider` 闭包,而是将闭包追加到了 `customerProviders` 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,`customerProvider` 参数必须允许“逃逸”出函数作用域。
|
||||
348
IOS/Task00:Swift基础语法学习/8.枚举.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# 枚举
|
||||
|
||||
*枚举*为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。
|
||||
|
||||
如果你熟悉 C 语言,你会知道在 C 语言中,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串、字符,或是一个整型值或浮点数。
|
||||
|
||||
此外,枚举成员可以指定*任意*类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值。
|
||||
|
||||
在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。
|
||||
|
||||
想了解更多相关信息,请参见 [属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html),[方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/11_Methods.html),[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html),[扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html) 和 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。
|
||||
|
||||
## 枚举语法
|
||||
|
||||
使用 `enum` 关键词来创建枚举并且把它们的整个定义放在一对大括号内:
|
||||
|
||||
```swift
|
||||
enum SomeEnumeration {
|
||||
// 枚举定义放在这里
|
||||
}
|
||||
```
|
||||
|
||||
下面是用枚举表示指南针四个方向的例子:
|
||||
|
||||
```swift
|
||||
enum CompassPoint {
|
||||
case north
|
||||
case south
|
||||
case east
|
||||
case west
|
||||
}
|
||||
```
|
||||
|
||||
枚举中定义的值(如 `north`,`south`,`east` 和 `west`)是这个枚举的*成员值*(或*成员*)。你可以使用 `case` 关键字来定义一个新的枚举成员值。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 与 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的 `CompassPoint` 例子中,`north`,`south`,`east` 和 `west` 不会被隐式地赋值为 `0`,`1`,`2` 和 `3`。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的 `CompassPoint` 类型。
|
||||
|
||||
多个成员值可以出现在同一行上,用逗号隔开:
|
||||
|
||||
```swift
|
||||
enum Planet {
|
||||
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
|
||||
}
|
||||
```
|
||||
|
||||
每个枚举定义了一个全新的类型。像 Swift 中其他类型一样,它们的名字(例如 `CompassPoint` 和 `Planet`)以一个大写字母开头。给枚举类型起一个单数名字而不是复数名字,以便于:
|
||||
|
||||
```swift
|
||||
var directionToHead = CompassPoint.west
|
||||
```
|
||||
|
||||
`directionToHead` 的类型可以在它被 `CompassPoint` 的某个值初始化时推断出来。一旦 `directionToHead` 被声明为 `CompassPoint` 类型,你可以使用更简短的点语法将其设置为另一个 `CompassPoint` 的值:
|
||||
|
||||
```swift
|
||||
directionToHead = .east
|
||||
```
|
||||
|
||||
当 `directionToHead` 的类型已知时,再次为其赋值可以省略枚举类型名。在使用具有显式类型的枚举值时,这种写法让代码具有更好的可读性。
|
||||
|
||||
## 使用 Switch 语句匹配枚举值
|
||||
|
||||
你可以使用 `switch` 语句匹配单个枚举值:
|
||||
|
||||
```swift
|
||||
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")
|
||||
}
|
||||
// 打印“Watch out for penguins”
|
||||
```
|
||||
|
||||
你可以这样理解这段代码:
|
||||
|
||||
“判断 `directionToHead` 的值。当它等于 `.north`,打印 `“Lots of planets have a north”`。当它等于 `.south`,打印 `“Watch out for penguins”`。”
|
||||
|
||||
……以此类推。
|
||||
|
||||
正如在 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html) 中介绍的那样,在判断一个枚举类型的值时,`switch` 语句必须穷举所有情况。如果忽略了 `.west` 这种情况,上面那段代码将无法通过编译,因为它没有考虑到 `CompassPoint` 的全部成员。强制穷举确保了枚举成员不会被意外遗漏。
|
||||
|
||||
当不需要匹配每个枚举成员的时候,你可以提供一个 `default` 分支来涵盖所有未明确处理的枚举成员:
|
||||
|
||||
```swift
|
||||
let somePlanet = Planet.earth
|
||||
switch somePlanet {
|
||||
case .earth:
|
||||
print("Mostly harmless")
|
||||
default:
|
||||
print("Not a safe place for humans")
|
||||
}
|
||||
// 打印“Mostly harmless”
|
||||
```
|
||||
|
||||
## 枚举成员的遍历
|
||||
|
||||
在一些情况下,你会需要得到一个包含枚举所有成员的集合。可以通过如下代码实现:
|
||||
|
||||
令枚举遵循 `CaseIterable` 协议。Swift 会生成一个 `allCases` 属性,用于表示一个包含枚举所有成员的集合。下面是一个例子:
|
||||
|
||||
```swift
|
||||
enum Beverage: CaseIterable {
|
||||
case coffee, tea, juice
|
||||
}
|
||||
let numberOfChoices = Beverage.allCases.count
|
||||
print("\(numberOfChoices) beverages available")
|
||||
// 打印“3 beverages available”
|
||||
```
|
||||
|
||||
在前面的例子中,通过 `Beverage.allCases` 可以访问到包含 `Beverage` 枚举所有成员的集合。`allCases` 的使用方法和其它一般集合一样——集合中的元素是枚举类型的实例,所以在上面的情况中,这些元素是 `Beverage` 值。在前面的例子中,统计了总共有多少个枚举成员。而在下面的例子中,则使用 `for` 循环来遍历所有枚举成员。
|
||||
|
||||
```swift
|
||||
for beverage in Beverage.allCases {
|
||||
print(beverage)
|
||||
}
|
||||
// coffee
|
||||
// tea
|
||||
// juice
|
||||
```
|
||||
|
||||
在前面的例子中,使用的语法表明这个枚举遵循 [CaseIterable](https://developer.apple.com/documentation/swift/caseiterable) 协议。想了解 protocols 相关信息,请参见 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。
|
||||
|
||||
## 关联值
|
||||
|
||||
枚举语法那一小节的例子演示了如何定义和分类枚举的成员。你可以为 `Planet.earth` 设置一个常量或者变量,并在赋值之后查看这个值。然而,有时候把其他类型的值和成员值一起存储起来会很有用。这额外的信息称为*关联值*,并且你每次在代码中使用该枚举成员时,还可以修改这个关联值。
|
||||
|
||||
你可以定义 Swift 枚举来存储任意类型的关联值,如果需要的话,每个枚举成员的关联值类型可以各不相同。枚举的这种特性跟其他语言中的可识别联合(discriminated unions),标签联合(tagged unions),或者变体(variants)相似。
|
||||
|
||||
例如,假设一个库存跟踪系统需要利用两种不同类型的条形码来跟踪商品。有些商品上标有使用 `0` 到 `9` 的数字的 UPC 格式的一维条形码。每一个条形码都有一个代表数字系统的数字,该数字后接五位代表厂商代码的数字,接下来是五位代表“产品代码”的数字。最后一个数字是检查位,用来验证代码是否被正确扫描:
|
||||
|
||||

|
||||
|
||||
其他商品上标有 QR 码格式的二维码,它可以使用任何 ISO 8859-1 字符,并且可以编码一个最多拥有 2,953 个字符的字符串:
|
||||
|
||||

|
||||
|
||||
这便于库存跟踪系统用包含四个整型值的元组存储 UPC 码,以及用任意长度的字符串储存 QR 码。
|
||||
|
||||
在 Swift 中,使用如下方式定义表示两种商品条形码的枚举:
|
||||
|
||||
```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` 时,可以存储的关联值的类型。
|
||||
|
||||
然后你可以使用任意一种条形码类型创建新的条形码,例如:
|
||||
|
||||
```swift
|
||||
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
|
||||
```
|
||||
|
||||
上面的例子创建了一个名为 `productBarcode` 的变量,并将 `Barcode.upc` 赋值给它,关联的元组值为 `(8, 85909, 51226, 3)`。
|
||||
|
||||
同一个商品可以被分配一个不同类型的条形码,例如:
|
||||
|
||||
```swift
|
||||
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
|
||||
```
|
||||
|
||||
这时,原始的 `Barcode.upc` 和其整数关联值被新的 `Barcode.qrCode` 和其字符串关联值所替代。`Barcode` 类型的常量和变量可以存储一个 `.upc` 或者一个 `.qrCode`(连同它们的关联值),但是在同一时间只能存储这两个值中的一个。
|
||||
|
||||
你可以使用一个 switch 语句来检查不同的条形码类型,和之前使用 Switch 语句来匹配枚举值的例子一样。然而,这一次,关联值可以被提取出来作为 switch 语句的一部分。你可以在 `switch` 的 case 分支代码中提取每个关联值作为一个常量(用 `let` 前缀)或者作为一个变量(用 `var` 前缀)来使用:
|
||||
|
||||
```swift
|
||||
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).")
|
||||
}
|
||||
// 打印“QR code: ABCDEFGHIJKLMNOP.”
|
||||
```
|
||||
|
||||
如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 `let` 或者 `var`:
|
||||
|
||||
```swift
|
||||
switch productBarcode {
|
||||
case let .upc(numberSystem, manufacturer, product, check):
|
||||
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
|
||||
case let .qrCode(productCode):
|
||||
print("QR code: \(productCode).")
|
||||
}
|
||||
// 打印“QR code: ABCDEFGHIJKLMNOP.”
|
||||
```
|
||||
|
||||
## 原始值
|
||||
|
||||
在 [关联值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/08_Enumerations.html#associated_values) 小节的条形码例子中,演示了如何声明存储不同类型关联值的枚举成员。作为关联值的替代选择,枚举成员可以被默认值(称为*原始值*)预填充,这些原始值的类型必须相同。
|
||||
|
||||
这是一个使用 ASCII 码作为原始值的枚举:
|
||||
|
||||
```swift
|
||||
enum ASCIIControlCharacter: Character {
|
||||
case tab = "\t"
|
||||
case lineFeed = "\n"
|
||||
case carriageReturn = "\r"
|
||||
}
|
||||
```
|
||||
|
||||
枚举类型 `ASCIIControlCharacter` 的原始值类型被定义为 `Character`,并设置了一些比较常见的 ASCII 控制字符。`Character` 的描述详见 [字符串和字符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html) 部分。
|
||||
|
||||
原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。
|
||||
|
||||
### 原始值的隐式赋值
|
||||
|
||||
在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。
|
||||
|
||||
例如,当使用整数作为原始值时,隐式赋值的值依次递增 `1`。如果第一个枚举成员没有设置原始值,其原始值将为 `0`。
|
||||
|
||||
下面的枚举是对之前 `Planet` 这个枚举的一个细化,利用整型的原始值来表示每个行星在太阳系中的顺序:
|
||||
|
||||
```swift
|
||||
enum Planet: Int {
|
||||
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
|
||||
}
|
||||
```
|
||||
|
||||
在上面的例子中,`Plant.mercury` 的显式原始值为 `1`,`Planet.venus` 的隐式原始值为 `2`,依次类推。
|
||||
|
||||
当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。
|
||||
|
||||
下面的例子是 `CompassPoint` 枚举的细化,使用字符串类型的原始值来表示各个方向的名称:
|
||||
|
||||
```swift
|
||||
enum CompassPoint: String {
|
||||
case north, south, east, west
|
||||
}
|
||||
```
|
||||
|
||||
上面例子中,`CompassPoint.south` 拥有隐式原始值 `south`,依次类推。
|
||||
|
||||
使用枚举成员的 `rawValue` 属性可以访问该枚举成员的原始值:
|
||||
|
||||
```swift
|
||||
let earthsOrder = Planet.earth.rawValue
|
||||
// earthsOrder 值为 3
|
||||
|
||||
let sunsetDirection = CompassPoint.west.rawValue
|
||||
// sunsetDirection 值为 "west"
|
||||
```
|
||||
|
||||
### 使用原始值初始化枚举实例
|
||||
|
||||
如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 `rawValue` 的参数,参数类型即为原始值类型,返回值则是枚举成员或 `nil`。你可以使用这个初始化方法来创建一个新的枚举实例。
|
||||
|
||||
这个例子利用原始值 `7` 创建了枚举成员 `Uranus`:
|
||||
|
||||
```swift
|
||||
let possiblePlanet = Planet(rawValue: 7)
|
||||
// possiblePlanet 类型为 Planet? 值为 Planet.uranus
|
||||
```
|
||||
|
||||
然而,并非所有 `Int` 值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个*可选*的枚举成员。在上面的例子中,`possiblePlanet` 是 `Planet?` 类型,或者说“可选的 `Planet`”。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。更多信息请参见 [可失败构造器](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter3/05_Declarations.html#failable_initializers)
|
||||
|
||||
如果你试图寻找一个位置为 `11` 的行星,通过原始值构造器返回的可选 `Planet` 值将是 `nil`:
|
||||
|
||||
```swift
|
||||
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)")
|
||||
}
|
||||
// 打印“There isn't a planet at position 11”
|
||||
```
|
||||
|
||||
这个例子使用了可选绑定(optional binding),试图通过原始值 `11` 来访问一个行星。`if let somePlanet = Planet(rawValue: 11)` 语句创建了一个可选 `Planet`,如果可选 `Planet` 的值存在,就会赋值给 `somePlanet`。在这个例子中,无法检索到位置为 `11` 的行星,所以 `else` 分支被执行。
|
||||
|
||||
## 递归枚举
|
||||
|
||||
*递归枚举*是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 `indirect` 来表示该成员可递归。
|
||||
|
||||
例如,下面的例子中,枚举类型存储了简单的算术表达式:
|
||||
|
||||
```swift
|
||||
enum ArithmeticExpression {
|
||||
case number(Int)
|
||||
indirect case addition(ArithmeticExpression, ArithmeticExpression)
|
||||
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
|
||||
}
|
||||
```
|
||||
|
||||
你也可以在枚举类型开头加上 `indirect` 关键字来表明它的所有成员都是可递归的:
|
||||
|
||||
```swift
|
||||
indirect enum ArithmeticExpression {
|
||||
case number(Int)
|
||||
case addition(ArithmeticExpression, ArithmeticExpression)
|
||||
case multiplication(ArithmeticExpression, ArithmeticExpression)
|
||||
}
|
||||
```
|
||||
|
||||
上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员 `addition` 和 `multiplication` 的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。例如,表达式 `(5 + 4) * 2`,乘号右边是一个数字,左边则是另一个表达式。因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。下面的代码展示了使用 `ArithmeticExpression` 这个递归枚举创建表达式 `(5 + 4) * 2`
|
||||
|
||||
```swift
|
||||
let five = ArithmeticExpression.number(5)
|
||||
let four = ArithmeticExpression.number(4)
|
||||
let sum = ArithmeticExpression.addition(five, four)
|
||||
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
|
||||
```
|
||||
|
||||
要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数:
|
||||
|
||||
```swift
|
||||
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))
|
||||
// 打印“18”
|
||||
```
|
||||
|
||||
该函数如果遇到纯数字,就直接返回该数字的值。如果遇到的是加法或乘法运算,则分别计算左边表达式和右边表达式的值,然后相加或相乘。
|
||||
250
IOS/Task00:Swift基础语法学习/9.结构体和类.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 结构体和类
|
||||
|
||||
*结构体*和*类*作为一种通用而又灵活的结构,成为了人们构建代码的基础。你可以使用定义常量、变量和函数的语法,为你的结构体和类定义属性、添加方法。
|
||||
|
||||
与其他编程语言所不同的是,Swift 并不要求你为自定义的结构体和类的接口与实现代码分别创建文件。你只需在单一的文件中定义一个结构体或者类,系统将会自动生成面向其它代码的外部接口。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 通常一个*类*的实例被称为*对象*。然而相比其他语言,Swift 中结构体和类的功能更加相近,本章中所讨论的大部分功能都可以用在结构体或者类上。因此,这里会使用*实例*这个更通用的术语。
|
||||
|
||||
## 结构体和类对比
|
||||
|
||||
Swift 中结构体和类有很多共同点。两者都可以:
|
||||
|
||||
- 定义属性用于存储值
|
||||
- 定义方法用于提供功能
|
||||
- 定义下标操作用于通过下标语法访问它们的值
|
||||
- 定义构造器用于设置初始值
|
||||
- 通过扩展以增加默认实现之外的功能
|
||||
- 遵循协议以提供某种标准功能
|
||||
|
||||
更多信息请参见 [属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html)、[方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/11_Methods.html)、[下标](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/12_Subscripts.html)、[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html)、[扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html) 和 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。
|
||||
|
||||
与结构体相比,类还有如下的附加功能:
|
||||
|
||||
- 继承允许一个类继承另一个类的特征
|
||||
- 类型转换允许在运行时检查和解释一个类实例的类型
|
||||
- 析构器允许一个类实例释放任何其所被分配的资源
|
||||
- 引用计数允许对一个类的多次引用
|
||||
|
||||
更多信息请参见 [继承](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/13_Inheritance.html)、[类型转换](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/18_Type_Casting.html)、[析构过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/15_Deinitialization.html) 和 [自动引用计数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html)。
|
||||
|
||||
类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。更多详细的比较参见 [在结构和类之间进行选择](https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes)。
|
||||
|
||||
### 类型定义的语法
|
||||
|
||||
结构体和类有着相似的定义方式。你通过 `struct` 关键字引入结构体,通过 `class` 关键字引入类,并将它们的具体定义放在一对大括号中:
|
||||
|
||||
```swift
|
||||
struct SomeStructure {
|
||||
// 在这里定义结构体
|
||||
}
|
||||
class SomeClass {
|
||||
// 在这里定义类
|
||||
}
|
||||
```
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 每当你定义一个新的结构体或者类时,你都是定义了一个新的 Swift 类型。请使用 `UpperCamelCase`这种方式来命名类型(如这里的 `SomeClass` 和 `SomeStructure`),以便符合标准 Swift 类型的大写命名风格(如 `String`,`Int` 和 `Bool`)。请使用 `lowerCamelCase` 这种方式来命名属性和方法(如 `framerate` 和 `incrementCount`),以便和类型名区分。
|
||||
|
||||
以下是定义结构体和定义类的示例:
|
||||
|
||||
```swift
|
||||
struct Resolution {
|
||||
var width = 0
|
||||
var height = 0
|
||||
}
|
||||
class VideoMode {
|
||||
var resolution = Resolution()
|
||||
var interlaced = false
|
||||
var frameRate = 0.0
|
||||
var name: String?
|
||||
}
|
||||
```
|
||||
|
||||
在上面的示例中定义了一个名为 `Resolution` 的结构体,用来描述基于像素的分辨率。这个结构体包含了名为 `width` 和 `height` 的两个存储属性。存储属性是与结构体或者类绑定的,并存储在其中的常量或变量。当这两个属性被初始化为整数 `0` 的时候,它们会被推断为 `Int` 类型。
|
||||
|
||||
在上面的示例还定义了一个名为 `VideoMode` 的类,用来描述视频显示器的某个特定视频模式。这个类包含了四个可变的存储属性。第一个, `resolution`,被初始化为一个新的 `Resolution` 结构体的实例,属性类型被推断为 `Resolution`。新 `VideoMode` 实例同时还会初始化其它三个属性,它们分别是初始值为 `false` 的 `interlaced`(意为“非隔行视频”),初始值为 `0.0` 的 `frameRate`,以及值为可选 `String` 的 `name`。因为 `name` 是一个可选类型,它会被自动赋予一个默认值 `nil`,意为“没有 `name` 值”。
|
||||
|
||||
### 结构体和类的实例
|
||||
|
||||
`Resolution` 结构体和 `VideoMode` 类的定义仅描述了什么是 `Resolution` 和 `VideoMode`。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为此,你需要创建结构体或者类的一个实例。
|
||||
|
||||
创建结构体和类实例的语法非常相似:
|
||||
|
||||
```swift
|
||||
let someResolution = Resolution()
|
||||
let someVideoMode = VideoMode()
|
||||
```
|
||||
|
||||
结构体和类都使用构造器语法来创建新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如 `Resolution()` 或 `VideoMode()`。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html) 章节会对类和结构体的初始化进行更详细的讨论。
|
||||
|
||||
### 属性访问
|
||||
|
||||
你可以通过使用*点语法*访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者以点号(`.`)分隔,不带空格:
|
||||
|
||||
```swift
|
||||
print("The width of someResolution is \(someResolution.width)")
|
||||
// 打印 "The width of someResolution is 0"
|
||||
```
|
||||
|
||||
在上面的例子中,`someResolution.width` 引用 `someResolution` 的 `width` 属性,返回 `width` 的初始值 `0`。
|
||||
|
||||
你也可以访问子属性,如 `VideoMode` 中 `resolution` 属性的 `width` 属性:
|
||||
|
||||
```swift
|
||||
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
|
||||
// 打印 "The width of someVideoMode is 0"
|
||||
```
|
||||
|
||||
你也可以使用点语法为可变属性赋值:
|
||||
|
||||
```swift
|
||||
someVideoMode.resolution.width = 1280
|
||||
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
|
||||
// 打印 "The width of someVideoMode is now 1280"
|
||||
```
|
||||
|
||||
### 结构体类型的成员逐一构造器
|
||||
|
||||
所有结构体都有一个自动生成的*成员逐一构造器*,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
|
||||
|
||||
```swift
|
||||
let vga = Resolution(width: 640, height: 480)
|
||||
```
|
||||
|
||||
与结构体不同,类实例没有默认的成员逐一构造器。[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html) 章节会对构造器进行更详细的讨论。
|
||||
|
||||
## 结构体和枚举是值类型
|
||||
|
||||
*值类型*是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被*拷贝*。
|
||||
|
||||
在之前的章节中,你已经大量使用了值类型。实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。
|
||||
|
||||
Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。
|
||||
|
||||
> 注意
|
||||
>
|
||||
> 标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。
|
||||
|
||||
请看下面这个示例,其使用了上一个示例中的 `Resolution` 结构体:
|
||||
|
||||
```swift
|
||||
let hd = Resolution(width: 1920, height: 1080)
|
||||
var cinema = hd
|
||||
```
|
||||
|
||||
在以上示例中,声明了一个名为 `hd` 的常量,其值为一个初始化为全高清视频分辨率(`1920` 像素宽,`1080` 像素高)的 `Resolution` 实例。
|
||||
|
||||
然后示例中又声明了一个名为 `cinema` 的变量,并将 `hd` 赋值给它。因为 `Resolution` 是一个结构体,所以会先创建一个现有实例的副本,然后将副本赋值给 `cinema` 。尽管 `hd` 和 `cinema` 有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。
|
||||
|
||||
下面,为了符合数码影院放映的需求(`2048` 像素宽,`1080` 像素高),`cinema` 的 `width` 属性被修改为稍微宽一点的 2K 标准:
|
||||
|
||||
```swift
|
||||
cinema.width = 2048
|
||||
```
|
||||
|
||||
查看 `cinema` 的 `width` 属性,它的值确实改为了 `2048`:
|
||||
|
||||
```swift
|
||||
print("cinema is now \(cinema.width) pixels wide")
|
||||
// 打印 "cinema is now 2048 pixels wide"
|
||||
```
|
||||
|
||||
然而,初始的 `hd` 实例中 `width` 属性还是 `1920`:
|
||||
|
||||
```swift
|
||||
print("hd is still \(hd.width) pixels wide")
|
||||
// 打印 "hd is still 1920 pixels wide"
|
||||
```
|
||||
|
||||
将 `hd` 赋值给 `cinema` 时,`hd` 中所存储的*值*会拷贝到新的 `cinema` 实例中。结果就是两个完全独立的实例包含了相同的数值。由于两者相互独立,因此将 `cinema` 的 `width` 修改为 `2048` 并不会影响 `hd` 中的 `width` 的值,如下图所示:
|
||||
|
||||

|
||||
|
||||
枚举也遵循相同的行为准则:
|
||||
|
||||
```swift
|
||||
enum CompassPoint {
|
||||
case north, south, east, west
|
||||
mutating func turnNorth() {
|
||||
self = .north
|
||||
}
|
||||
}
|
||||
var currentDirection = CompassPoint.west
|
||||
let rememberedDirection = currentDirection
|
||||
currentDirection.turnNorth()
|
||||
|
||||
print("The current direction is \(currentDirection)")
|
||||
print("The remembered direction is \(rememberedDirection)")
|
||||
// 打印 "The current direction is north"
|
||||
// 打印 "The remembered direction is west"
|
||||
```
|
||||
|
||||
当 `rememberedDirection` 被赋予了 `currentDirection` 的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改 `currentDirection` 的值并不影响 `rememberedDirection` 所储存的原始值的拷贝。
|
||||
|
||||
## 类是引用类型
|
||||
|
||||
与值类型不同,*引用类型*在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。
|
||||
|
||||
请看下面这个示例,其使用了之前定义的 `VideoMode` 类:
|
||||
|
||||
```swift
|
||||
let tenEighty = VideoMode()
|
||||
tenEighty.resolution = hd
|
||||
tenEighty.interlaced = true
|
||||
tenEighty.name = "1080i"
|
||||
tenEighty.frameRate = 25.0
|
||||
```
|
||||
|
||||
以上示例中,声明了一个名为 `tenEighty` 的常量,并让其引用一个 `VideoMode` 类的新实例。它的视频模式(video mode)被赋值为之前创建的 HD 分辨率(`1920`*`1080`)的一个拷贝。然后将它设置为隔行视频,名字设为 `“1080i”`,并将帧率设置为 `25.0` 帧每秒。
|
||||
|
||||
接下来,将 `tenEighty` 赋值给一个名为 `alsoTenEighty` 的新常量,并修改 `alsoTenEighty` 的帧率:
|
||||
|
||||
```swift
|
||||
let alsoTenEighty = tenEighty
|
||||
alsoTenEighty.frameRate = 30.0
|
||||
```
|
||||
|
||||
因为类是引用类型,所以 `tenEight` 和 `alsoTenEight` 实际上引用的是同一个 `VideoMode` 实例。换句话说,它们是同一个实例的两种叫法,如下图所示:
|
||||
|
||||

|
||||
|
||||
通过查看 `tenEighty` 的 `frameRate` 属性,可以看到它正确地显示了底层的 `VideoMode` 实例的新帧率 `30.0`:
|
||||
|
||||
```swift
|
||||
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
|
||||
// 打印 "The frameRate property of theEighty is now 30.0"
|
||||
```
|
||||
|
||||
这个例子也显示了为何引用类型更加难以理解。如果 `tenEighty` 和 `alsoTenEighty` 在你代码中的位置相距很远,那么就很难找到所有修改视频模式的地方。无论在哪使用 `tenEighty`,你都要考虑使用 `alsoTenEighty` 的代码,反之亦然。相反,值类型就更容易理解了,因为你的源码中与同一个值交互的代码都很近。
|
||||
|
||||
需要注意的是 `tenEighty` 和 `alsoTenEighty` 被声明为常量而不是变量。然而你依然可以改变 `tenEighty.frameRate` 和 `alsoTenEighty.frameRate`,这是因为 `tenEighty` 和 `alsoTenEighty`这两个常量的值并未改变。它们并不“存储”这个 `VideoMode` 实例,而仅仅是对 `VideoMode` 实例的引用。所以,改变的是底层 `VideoMode` 实例的 `frameRate` 属性,而不是指向 `VideoMode` 的常量引用的值。
|
||||
|
||||
### 恒等运算符
|
||||
|
||||
因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)
|
||||
|
||||
判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:
|
||||
|
||||
- 相同(`===`)
|
||||
- 不相同(`!==`)
|
||||
|
||||
使用这两个运算符检测两个常量或者变量是否引用了同一个实例:
|
||||
|
||||
```swift
|
||||
if tenEighty === alsoTenEighty {
|
||||
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
|
||||
}
|
||||
// 打印 "tenEighty and alsoTenEighty refer to the same VideoMode instance."
|
||||
```
|
||||
|
||||
请注意,“相同”(用三个等号表示,`===`)与“等于”(用两个等号表示,`==`)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。
|
||||
|
||||
当在定义你的自定义结构体和类的时候,你有义务来决定判定两个实例“相等”的标准。在章节 [等价操作符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#equivalence_operators)中将会详细介绍实现自定义 == 和 !== 运算符的流程。
|
||||
|
||||
### 指针
|
||||
|
||||
如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用*指针*来引用内存中的地址。Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(`*`)来表明你在创建一个引用。相反,Swift 中引用的定义方式与其它的常量或变量的一样。如果需要直接与指针交互,你可以使用标准库提供的指针和缓冲区类型 —— 参见 [手动管理内存](https://developer.apple.com/documentation/swift/swift_standard_library/manual_memory_management)。
|
||||
BIN
IOS/Task00:Swift基础语法学习/figures/accept.png
Normal file
|
After Width: | Height: | Size: 456 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/chooseinstall.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/chooseiso.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/chooseiso2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/chooselang.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/choosemacos.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/cleardisk.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/darwin15.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/disktool.png
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/exitiso.png
Normal file
|
After Width: | Height: | Size: 821 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/installed.png
Normal file
|
After Width: | Height: | Size: 931 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/installwmwaretools.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
IOS/Task00:Swift基础语法学习/figures/installxcode.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
IOS/Task00:Swift基础语法学习/figures/newvm.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/server.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
IOS/Task00:Swift基础语法学习/figures/unlocker.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/1.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/2.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/3.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/4.png
Normal file
|
After Width: | Height: | Size: 588 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/5.png
Normal file
|
After Width: | Height: | Size: 383 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/6.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
IOS/Task00:Swift基础语法学习/images/7.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
105
IOS/Task00:Swift基础语法学习/安装macOS.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Windows 系统下安装 macOS
|
||||
|
||||
由于 iOS 开发必须使用 XCode,而 XCode 又必须运行在 macOS 上,因此为了让使用 Windows 系统的朋友们也能进行 iOS 开发,我们将在 Windows 上通过虚拟机安装 macOS。
|
||||
|
||||
## 安装 VMware
|
||||
|
||||
操作系统版本:Windows 11 家庭中文版 21H2 22000.194
|
||||
|
||||
VMware 版本:16.0.0 build-16894299
|
||||
|
||||
macOS 版本:MacOS Catalina-10.15.6
|
||||
|
||||
|
||||
|
||||
我们将选用 VMware 来安装 macOS,首先下载并安装 VMware Workstation,建议前往官网下载:
|
||||
|
||||
https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html
|
||||
|
||||
安装完毕后需要输入许可密钥,在此提供三个许可密钥:
|
||||
|
||||
```
|
||||
ZF3R0-FHED2-M80TY-8QYGC-NPKYF
|
||||
YF390-0HF8P-M81RQ-2DXQE-M2UT6
|
||||
ZF71R-DMX85-08DQY-8YMNC-PPHV8
|
||||
```
|
||||
|
||||
## 安装 VMware macOS 解锁补丁
|
||||
|
||||
由于在 Windows 和 Linux 上的 VMware 对安装 macOS 进行了屏蔽,因此需要通过对 VMware 进行“插桩”来解除屏蔽。首先下载解锁补丁:
|
||||
|
||||
https://github.com/paolo-projects/unlocker/releases
|
||||
|
||||
在运行补丁前需要关闭 VMware 相关的服务:
|
||||
|
||||

|
||||
|
||||
将几个 VMware 开头的服务停止。
|
||||
|
||||
然后解压下载的 `unlocker.zip` ,以管理员身份运行 `win-install.cmd`,等待补丁自动下载安装完成:
|
||||
|
||||

|
||||
|
||||
## 安装 macOS
|
||||
|
||||
打开 VMware,新建虚拟机:
|
||||
|
||||

|
||||
|
||||
选择稍后安装操作系统:
|
||||
|
||||

|
||||
|
||||
如果上一步破解成功,这里会出现 macOS 的选项,选择 macOS 10.15:
|
||||
|
||||

|
||||
|
||||
剩下的名称、安装位置和磁盘容量根据个人需求选择。
|
||||
|
||||
创建完毕后,右击创建的虚拟机 - 设置 - CD/DVD - 连接 - 使用ISO映像文件,选择下载的 cdr 文件:
|
||||
|
||||

|
||||
|
||||
选择完毕后即可开启虚拟机启动安装程序:
|
||||
|
||||

|
||||
|
||||
选择磁盘工具:
|
||||
|
||||

|
||||
|
||||
选择并抹掉盘符:
|
||||
|
||||

|
||||
|
||||
完成之后关闭,并选择安装 macOS:
|
||||
|
||||

|
||||
|
||||
剩下的步骤根据提示完成即可:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
直接安装完毕后,会发现存在很多问题,如分辨率较低、鼠标移动缓慢等,此时需要安装 VMware Tool。
|
||||
|
||||
首先,将 Install macOS Catalina 推出:
|
||||
|
||||

|
||||
|
||||
修改使用的映像文件,改成 VMware 安装路径下的 darwin15.iso:
|
||||
|
||||

|
||||
|
||||
然后选择 VMware Tools:
|
||||
|
||||

|
||||
|
||||
随后根据提示依次选择继续 -> 安装,并在安全性与隐私窗口选择允许:
|
||||
|
||||

|
||||
|
||||
随后等待安装完毕,重新启动即可正常使用。随后,在 App Store 中下载 Xcode 即可进行开发:
|
||||
|
||||

|
||||