接着上一篇
控制流
For-In 循环
for-In
循环之前也接触过了。可以使用区间操作符控制循环次数:
1 | for index in 1...5 { |
上面的例子中, index
是一个自动被设置的值,不需要自己声明。
如果不需要循环的次数,可以用下划线 _
来替代 index
。
1 | let base = 3 |
While 循环
while
while 的形式:
while condition{
statements
}
就是 condition 的时候没有括号,其他没有特别的地方。
Repeat-While
和其他语言的 do-while 类似,只不过换成了 repeat:
repeat {
statements
} while condition
条件语句
if
if 之前也用到过许多次了,和其他语言一样,注意判断条件不加括号。else 以及 else if 类似。
switch
switch 基本用法也差不多,直接看个例子:
1 | let someCharacter: Character = "z" |
一定要注意,swift 中的 switch 一定要有 default,一定要有 default,一定要有 default。
没有隐性掉入
相比而言,没有 break
,匹配一个后,直接返回。也可以一条里面匹配多个,用逗号隔开:
1 | let someCharacter: Character = "z" |
这个就相当于是或操作,下面还有与操作
这样就匹配到了第一个情况。
另外,每个 case 的主干都需要至少一句可执行语句,不能只有 case:
1 | let anotherCharacter: Character = "a" |
这就引导我们,没有执行方法的条件,就不要写
范围匹配
在 case 中可以写一个范围:
1 | let approximateCount = 62 |
这种方式可以一定程度上替代一些判断数值的 if…else
元组
匹配的时候还可以匹配元组,用下划线 _
来匹配任意字符。当元组完全相等时,进入 case:
1 | let somePoint = (1, 1) |
元组其实相当于 if…else… 中 && 的操作
值绑定
在前面的基础上,前面用 _
代替任意值。如果在 case 中需要用到这个值怎么办呢?用 let 声明一个:
1 | let anotherPoint = (2, 0) |
这里面 let(x,y)
和 (let x,let y)
是一样的。
这种值绑定的方式,可以把函数中要用到的变量先确定下来。
最后一定要有一个
let(x,y)
,表示一个 default 操作,否则编译器会抛出异常。
where
switch 的 case 能使用 where 子句来进一步判断条件。
1 | let yetAnotherPoint = (1, -1) |
三个 switch 的 case 声明了占位常量 x 和 y,临时占用 point 中元组值。这些常量作为 where 子句的一部分,用来创建动态的筛选。只有当 where 子句的条件结果为 true,Switch 的 case 则会匹配现有 point 的值。
这里的 where 不仅适用于元组,也可以是普通的
case let x where x==1:
但是如果不用元组就没有 if…else… 简洁。
switch 能在一定程度上代替判断相等,以及判断在某一个范围的简单 if…else 操作
控制转移声明
控制转移声明包括:
- continue
- break
- fallthrough
- return
- throw
除了 fallthrough
其他都差不多。这里要注意一下在 switch 中使用的 break
。由于 switch 中的 case 里的执行语句不能为空,所以如果匹配到一种情况不需要操作,可以直接用 break
:
1 | let numberSymbol: Character = "三" // Chinese symbol for the number 3 |
Fallthrough
Swift 中在匹配成功后就不会掉入下一个 case 中。如果你就想要掉入的话,那么在执行语句后加上 fallthrough
即可:
1 | let integerToDescribe = 5 |
特别注意:调用 fallthrough
后,不检查 case 里的条件,会直接掉入下一个 case 。所以这里面直接执行了 default 的代码。
适用于那种满足了某个条件包含其他条件的操作,即要执行 A 那么 B 也要执行。最上面的永远是执行的最多的。
标签声明
主要诱因是循环与 switch 的嵌套。有时候,你需要跳出外层循环或者 switch,但是由于嵌套,你不得不 break
或者 continue
好几次。那么,标签声明为循环和 switch 提供了一个标记,这样在循环或者 switch 内部,可以轻松的终止外部的循环或 switch。
1 | gameLoop: while square != finalSquare { |
这样,通过 break gameLoop
和 continue gameLoop
就可以方便的跳出整个循环。
Guide 声明
这个是 Swift 3 的特性,和 if 类似。后面跟着一个 else,先看个例子:
1 | func greet(person: [String: String]) { |
当 guard
满足的时候代码直接往后走,如果条件不满足,那么执行 else 内的代码。其实逻辑和 if 是一样的。但是为什么要弄出这么个东西呢?为了让代码可读性更高。
有两点和 if 不同的注意点:
guard let
中必须要有return
,而if let
则不需要return
guard let
变量的作用域是外部作用域,if let
的作用域是内部作用域。也就是说,guard 方式 let 得到的值外部还能用到,if 方式 let 到的值外部已经无法使用。- 上面说到为什么作用域是外部的,这就要明白 guard 的目的。guard 是用来代替在一个函数中
if xxx 为空 return
的情况的。也就是一般都是用来做非空判断的。所以非空的情况下拿到 let 的值,在下面使用。
检查 API 是否可用
Swift 3 中提供了检查 API 是否可用的方法:
1 | if #available(iOS 10, macOS 10.12, *) { |
表示在 iOS 10,macOS 10.12,以及任意其他平台可用。*
表示任意其他平台。
函数
定义一个函数
直接上例子:
1 | func greet(person: String) -> String { |
这个函数名为 greet(person:)
。这个函数输入一个 String
类型叫做 person
的值,输出一段字符串。函数以 func
为前缀,->
指定的返回类型。
可以调用 print(_:separator:terminator:)
方法去打印上面函数的返回值:
1 | print(greet(person: "Anna")) |
这里的 print(_:separator:terminator:)
方法第一个入参没有名字,后面两个入参由于有默认值,因此可选。具体将在下面说到。
函数的参数和返回值
没有参数
示例:
1 | func sayHelloWorld() -> String { |
多个参数
函数可以有多个输入参数,把他们写到函数的括号内,并用逗号加以分隔:
1 | func greet(person: String, alreadyGreeted: Bool) -> String { |
无返回值
没有 return
没有返回类型 ->
:
1 | func greet(person: String) { |
严格来说,其实无返回类型的函数还是返回了一个值,即使没有返回值定义。函数没有定义返回类型但返 回了一个 void
返回类型的特殊值。它是一个空的元组,可以写为return ()
多个返回值
可以将返回类型设置为元组,来返回多个值:
1 | func minMax(array: [Int]) -> (min: Int, max: Int) { |
注意 ->
后面的返回类型书写方式:(min: Int, max: Int)
。
由于在定义返回类型的时候,元组中的元素都已经被命名好了。所以我们通过 .
操作符就能拿到对应的元素:
1 | let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) |
可选的元组返回类型
如果返回的元组可能没有值,那么可以使用可选的元组作为返回类型,来表示整个元组可以为 nil
。在元组之后加上 ?
来表示可选类型,例如 (Int,Int)?
(注意不要写成 (Int?,Int?)
这个表示元组中的元素是可选的)。
如果是可选元组,那么一定要先做非空判断。否则当你想要取出元组中元素后,就会触发运行时错误。你可以通过可选绑定来判断是否为空:
1 | if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) { |
函数的 argument label 和 parameter name
这两个词我不知道怎么翻译。每个函数都有 argument label 和 parameter name。argument label 用在调用函数的时候;parameter name 用在方法体中的参数使用。默认情况下两者是相等的。在一个方法中,所有的 parameter name 都应该是独一无二的,因为在方法体中会用到;argument label 则不必都不同。 (如果看不懂啥意思,可以看下面的几个例子就明白了)。
1 | func someFunction(firstParameterName: Int, secondParameterName: Int) { |
上面是一个 argument laebl 和 parameter name 默认相等的例子。
明确的 argument labels
现在考虑 argument labels 和 parameter name 不同的情况。可以将 argumentLabel 写在 parameterName 前面:
1 | func someFunction(argumentLabel parameterName: Int) { |
来看一个具体实例:
1 | func greet(person: String, from hometown: String) -> String { |
其中 from
是 argument labels,hometown
是 parameter name。注意在方法里用 hometown
,在调用的时候用 from
。
省略的 argument labels
如果不想要 argument labels,可以用 _
代替,调用的时候就什么都不用写了:
1 | func someFunction(_ firstParameterName: Int, secondParameterName: Int) { |
注意,参数顺序还是不能错的,否则产生异常。
默认的参数值
在定义函数的时候可以在参数类型后加上默认值:
1 | func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) { |
如果有默认值了,那么在调用的时候这个参数就可以省略了。
推荐将有默认值的参数放在入参的后面。因为没有默认值的参数一般更重要。
可变数量的入参
一个可变数量的入参可以传入零个或者更多的参数,在参数类型后面加上 …
就行了。这些入参将会以一个数组的形式在方法体中使用:
1 | func arithmeticMean(_ numbers: Double...) -> Double { |
这里入参的数字将会以名为 numbers
的 [Double]
数组的形式传入。
这里有几个注意点:
- 为什么不直接传一个数组呢?因为那样不直观
- 上面的是省略了 argument labels 的情况。如果不省略,比如将
_
替换成to
,那么调用的时候改为arithmeticMean(to:1, 2, 3, 4, 5)
即可。 - 一个函数里最多只能有一个可变数量的入参。
- 如果参数是可变的那么就没法设置默认值了。
- 还有一种情形:
func arithmeticMean(_ numbers: Double...,_ anotherNumber: Double)
,这种情况下由于两个入参都是缺省的,所有传入的参数都被numbers
接收,第二个参数无法接收参数。最好不要这样写。
输入输出参数
一般函数内参数值的变化不会影响外部传入的变量的值。现在可以通过 inout
标记,使参数值的变化同步影响到外部值的变化。
1 | func swapTwoInts(_ a: inout Int, _ b: inout Int) { |
这个示例中,该函数交换输入的两个值。在返回类型前加上 inout
标记。调用方法的时候需要在输入参数前加上 &
(其实就是 c 里面的引用嘛,只是多加个 inout 标记用来提示调用者)。使用示例:
1 | var someInt = 3 |
输入输出参数需要使用变量而不是常量,因为常量不能改变。
函数类型
上面有提到过,每个函数都有其函数类型,由输入参数类型和返回类型组成。比如:
1 | func addTwoInts(_ a: Int, _ b: Int) -> Int { |
上面的方法的函数类型为 (Int, Int) -> Int
。再比如一个没有入参和返回值的函数:
1 | func printHelloWorld() { |
这个函数的类型是 ()->Void
使用函数类型
你可以定义一个常量或变量为一个函数类型,并指定适当的函数给该变量:
1 | var mathFunction: (Int, Int) -> Int = addTwoInts |
可以直接写成
mathFunction = addTwoInts
通过类型推断决定类型
现在就可以像使用 addTwoInts
方法一样使用 mathFunction
了:
1 | print("Result: \(mathFunction(2, 3))") |
注意,使用函数类型的地方就不能再有 argument label 以及 parameter name 了
就算 addTwoInts 的两个入参是有 argument label 的,这里调用 mathFunction 的时候也要省略。mathFunction 不能自己定义 argument label 以及 parameter name ,只能赋予函数之后直接使用。例子:
注意,只有函数类型匹配才能够将 addTwoInts
赋给 mathFunction
。由于 mathFunction
和 multiplyTwoInts
类型也相同,所以可以继续对 mathFunction
赋值:
1 | mathFunction = multiplyTwoInts |
由于类型推断的作用,定义函数变量或者常量的时候可以不用写出函数类型:
1 | let anotherMathFunction = addTwoInts |
由于传递函数不能改变 argument label 以及 parameter name,所以使用类型推断更方便一些,不要再写一遍函数类型了。
函数类型作为参数类型
可以将一个函数作为参数传入另一个函数。这使你预留了一个函数的某些方面的函数实现,让调用者提供的函数时被调用:
1 | func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) { |
这里 mathFunction
就是一个函数类型 (Int,Int)->Int
。
函数类型作为返回类型
函数的返回类型可以是一个函数类型,即返回一个函数。
例如有两个函数,函数类型都为 (Int)->Int
:
1 | func stepForward(_ input: Int) -> Int { |
现在定义一个返回 (Int)->Int
类型的函数 chooseStepFunction(backward:)
,注意返回类型的写法:
1 | func chooseStepFunction(backward: Bool) -> (Int) -> Int { |
下面看看如何使用的:
1 | var currentValue = 3 |
嵌套函数
迄今为止碰到的所有函数都是在全局范围里的函数,我们也可以将函数定义在函数内,作为嵌套函数。嵌套函数对外是隐藏的,但仍然可以调用和使用其内部的函数。
1 | func chooseStepFunction(backward: Bool) -> (Int) -> Int { |
闭包
闭包表达式
sort 函数
Swift 提供了 sort(by:)
函数,会根据提供的闭包,将已知类型数组中的值进行排序。排序完成,函数会返回一个与原数组大小相同的新数组,该数组中包含已经正确排序的同类型元素。
sort(by:)
函数输入一个比较函数大小的方法:
1 | let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
闭包表达式语法
上面是输入一个函数的方式,还可以创建一个闭包表达式。闭包表达式的语法如下:
{(parameters) -> returntype in
statements
}
所以可以将上面的排序方法改为如下:
1 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in |
根据上下文推断类型
因为闭包是作为函数的参数传入的,Swift 能推断出它的参数和返回值的类型(注意,是只有作为参数传入的闭包,才能依据函数的参数类型推断,省略闭包的参数类型的定义。如果是自己单独定义的一个闭包,不能进行上下文推断类型。),所以可以省略类型:
1 | reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) |
实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数时,都可以推断出闭包的参数和返回值类型,这意味着您几乎不需要利用完整格式构造任何内联闭包。然而,你也可以使用明确的类型,如果你想它避免读者阅读可能存在的歧义,这样还是值得鼓励的。
单行表达式省略
单行表达式闭包可以通过隐藏 return
关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
1 | reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) |
参数名简写
Swift 为内联函数提供了参数名称简写功能。可以直接用 $0,$1,$2 等名字来引用的闭包的参数的值。此时,in
关键字可以被省略。在这个例子中,$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。
1 | reversedNames = names.sorted(by: { $0 > $1 } ) |
尾部闭包
如果定义的函数的最后一个参数是一个闭包,调用的时候可以使用尾部闭包(Trailing Closures 不知道怎么翻译好就解释为尾部闭包了)。注意书写语法:
1 | func someFunctionThatTakesAClosure(closure: () -> Void) { |
可以看到很明显的区别。没有使用尾部闭包的时候是非常正规的函数调用。如果是尾部闭包的情况,那么就单独拿出来放到外面。
比如上面的 sorted(by:)
方法可以改成尾部闭包的形式:
1 | reversedNames = names.sorted() { $0 > $1 } |
如果整个函数的参数只有这一个入参,那么还可以直接省略这个括号(虽然没有歧义,但是总感觉这种语法糖不太好):
1 | reversedNames = names.sorted { $0 > $1 } |
再举一个典型的例子:在 Swift 中的数组类型中,有一个 map(_:)
方法,这个方法输入一个处理函数,对数组中的每个元素进行处理,返回一个处理过的新数组:
1 | let digitNames = [ |
捕获值
闭包可以在捕获其所定义的上下文中的变量和常量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
下面举一个关于嵌套函数的例子。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量:
1 | func makeIncrementer(forIncrement amount: Int) -> () -> Int { |
incrementer
函数中并没有保存 amount
和 runningTotal
。incrementor
实际上捕获并存储了该变量的一个副本,而该副本随着 incrementor
一同被存储。
如果新建了一个新的 incrementer
,其会有一个属于自己的独立的 runningTotal
变量的引用:
1 | let incrementBySeven = makeIncrementer(forIncrement: 7) |
闭包是引用类型
上面的例子中,incrementBySeven
和 incrementByTen
是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。 这是因为函数和闭包都是引用类型。
无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen
指向闭包的引用是一个常量,而并非闭包内容本身。
这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:
1 | let alsoIncrementByTen = incrementByTen |
逃逸闭包
当一个闭包作为参数传到另一个函数中,并且这个闭包在函数返回后才调用(比如在函数中执行一个异步回调,回调时才执行这个闭包),我们称该闭包从函数中逃逸。需要在函数名前标注 @escaping
,用来表示这个闭包是允许逃逸出函数的。
下面模拟一下情景:将 completionHandler
参数保存在外部的一个数组中,此时必须要标记 @escaping
否则产生编译错误:
1 | var completionHandlers: [() -> Void] = [] |
将闭包标记为逃逸闭包,就必须在闭包中显式地引用 self
。看下面的示例:
1 | func someFunctionWithNonescapingClosure(closure: () -> Void) { |
其中,逃逸的闭包没有立刻执行,所以 instance.x
先被设置成了 100,然后闭包执行了后,才被设置成 200。
另外,看到 completionHandlers.first?()
了么,虽然这列数组是非可选的,但是这个 first
方法返回的是一个可选类型,所以使用的时候要加上 ?
或者 !
逃逸闭包要注意产生引用循环,需要和 oc 一样使用 weak 标记属性。
自动闭包
自动闭包是一种闭包的简写方式。不接受任何参数,当他被调用时,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
来看个例子就明白了,先看一下不用自动闭包的情况:
1 | // customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] |
上面这种情况的函数调用是正常的打开方式。下面来看下自动闭包的使用:
1 | func serve(customer customerProvider: @autoclosure () -> String) { |
上面用了 autoclosure
标记后,下面的闭包可以不用括号。(这玩意会有人用?毫无意义。就当记录一下吧)
貌似只针对一句话的闭包