AS-4.函数
按照重要性降序,关于函数的三件事:
- 函数可以像Int 和String 那样被赋值给变量,也可以作为另一个函数的输入参数,或者另一个函数的返回值来使用
- 函数能够捕获存在于其局部作用域之外的变量
- 有两种方法可以创建函数,一种是使用func 关键字,另一种是使用{}。在Swift 中,后一种被称为闭包表达式。—— 闭包和闭包表达式是两个截然不同的概念
1综述
1.1 函数可以被赋值给变量,也可以作为函数的输入和输出
// begin from
func printInt(i: Int){
print("You passed \(i).")
}
// 将函数赋值给变量
let funVar = printInt
funVar(2) // You passed 2.
// 调用被赋值给变量的函数时,不能包含参数标签
// 接受函数作为参数的函数
func useFunction(function: (Int)->()){
function(3)
}
useFunction(function: printInt) // You passed 3.
useFunction(function: funVar) // You passed 3.
// 返回其他函数的函数
func returnFunc() -> (Int)->String{
func innerFunc(i: Int) -> String{
return "you passed \(i)"
}
return innerFunc
}
let myFunc = returnFunc()
myFunc(3) // You passed 3.
1.2 函数可以捕获存在于它们作用域之外的变量
捕获:当函数引用了在起作用域之外的变量时,这个变量就被捕获了,它们将会继续存在,而非在超过作用域之后被摧毁
func counterFunc() -> (Int)->String{
var counter = 0
func innerFunc(i: Int) -> String{
counter += i
return "Running total \(counter)"
}
return innerFunc
}
var f = counterFunc()
f(3) // Running total: 3
f(4) // Running total: 7
let g = counterFunc() // 再次调用将生成并捕获一个新的counter 变量
g(2) // Running total: 2
g(2) // Running total: 4
// 再次调用f 将会继续使用之前的
f(2) // Running total: 9
f = counterFunc() // 重新为f 赋值,之前的函数被销毁,因此计数重新开始
f(2) // Running total: 2
// It will be easier to catch the usage if we consider these functions with their variables as a Class.
- 一般来讲,因为counter 时counterFunc 的局部变量,所以它在return 语句执行之后就应该离开作用域并被摧毁。但因为innerFunc 捕获了它,所以Swift 运行时将一直保证它的存在,知道捕获它的函数被销毁为止。
- 在编程术语中,一个函数和它所捕获的变量环境组合起来被称为闭包。上边的f 和g 都是闭包的例子,因为它们捕获并使用了一个在它们作用域之外声明的非局部变量
1.3 函数可以使用{} 来声明为闭包表达式
- 在Swift中,定义函数的方法有两种
- 使用func 关键字
- 使用闭包表达式
// 翻倍函数
func doubler(i: Int) -> Int{
return i * 2
}
[1,2,3,4].map(doubler) //[2,4,6,8]
// 使用闭包表达式完成相同的任务
let doublerAlt = {(i: Int)->Int in return i*2}
[1,2,3,4].map(doublerAlt) //[2,4,6,8]
- 使用闭包表达式定义的函数可以被看作函数的字面量「function literals」,就如1 是整数字面量
- 与func 相比,它的区别在于闭包表达式是匿名的,他们没有被赋予名字。使用它们的方式只有在它们被创建时将其赋值给一个变量,或者将其传递给另一个函数或者方法。
- 使用内置的Swift 语言特性可以使一个闭包表达式比一个等价的func 简洁100倍
[1,2,3].map{$0*2} // 上述doubler 的简化版
—— 用到的Swift 特性有:- 在该闭包的唯一应用场景中,不必将其存储到变量中,而可以将其直接传递给一个接受Int 的函数
[1,2,3].map({(i:Int)->Int in return i*2})
- 编译器从上下文中推断出了类型:1)从数组的类型推断出传递给map 的函数接受Int 作为参数;2)从闭包内的乘法结果的类型推断出闭包返回的也是Int。
[1,2,3].map({i in return i * 2})
- 如果闭包的主体只包括一个单一的表达式,它将自动返回这个表达式的结果而不必写return。
[1,2,3].map({i in i*2})
- $0 代表第一个参数,$1代表第二个参数,以此类推。
[1,2,3].map({$0*2})
- 若函数的最后一个参数是闭包表达式,可以直接将其迁移到函数外侧。
[1,2,3].map(){$0*2}
- 尾随闭包语法「trailing closure syntax」不需要函数原本的()。
[1,2,3].map{$0*2}
- 在该闭包的唯一应用场景中,不必将其存储到变量中,而可以将其直接传递给一个接受Int 的函数
2. 函数的灵活性
- 关于排序
// 简简单单sorted
let myArray = [1,3,2]
myArray.sorted() // [1,2,3]
myArray.sorted(by: >) // [3,2,1]
// 即使被排序元素不遵循Comparable
var numString = [(2,"two"), (1,"one"), (3,"three")]
numString.sort(by: <)
numString // [(1,"one"),(2,"two"),(3,"three")]
// 或者使用更加复杂的排序函数 rather than '>' or '<'
let animals = ["elephant", "zebra", "dog"]
animals.sorted{lhs, rhs in
let l = lhs.reverse()
let r = rhs.reverse()
return l.lex
} // ["zebra", "dog", "elephant"]
- Swift 的排序具有一个强大的功能:可以用任意的比较函数来对集合进行排序
let people = [
Person(first: "Emily", last: "Young", yearOfBirth: 2002),
Person(first: "David", last: "Gray", yearOfBirth: 1991),
Person(first: "Robert", last: "Barnes", yearOfBirth: 1985),
Person(first: "Ava", last: "Barnes", yearOfBirth: 2000),
Person(first: "Joanne", last: "Miller", yearOfBirth: 1994),
Person(first: "Ava", last: "Barnes", yearOfBirth: 1998),
]
// 对该数组进行排序,规则是先按照姓,再按照名,最后按照出生年份
people.sorted{p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right){
$0.localizedStandardCompares($1) == .orderedAscending
}
}
/*
[Ava Barnes (2000), Ava Barnes (1998), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)]
*/
3. 函数作为代理
- Cocoa 风格的代理
- 用类作为代理,很容易形成循环依赖,因此需要用weak 关键字修饰delegate 属性
protocol AlertViewDelegate: AnyObject{ // 继承自AnyObject 意味着只能有类来实现
func buttonTapped(atIndex: Int)
}
class AlertView{
var buttons: [String]
weak var delegate: AlertViewDelegate?
init(buttons:[String] = ["OK", "Cancel"]){
self.buttons = buttons
}
func fire(){
delegate?.buttonTapped(atIndex: 1)
}
}
class ViewController: AlertViewDelegate{
let alert: AlertView
init(){
alert = AlertView(buttons: ["OK", "Cancel"])
alert.delegate = self
}
func buttonTapped(atIndex index: Int){
print("Button tapped: \(index)")
}
}
- 在结构体中实现代理
- 放宽AlertViewDelegate 的定义,令其不只针对于类
- 将func 类型改为mutating,使结构体能够在方法被调用时改变自身的内容
protocol AlertViewDelegate{
mutating func buttonTapped(atIndex: Int)
}
class AlertView{
var buttons: [String]
var delegate: AlertViewDelegate?
init(buttons: [String] = ["OK", "Cancel"]){
self.buttons = buttons
}
func fire(){
delegate?.buttonTapped(atIndex: 1)
}
}
struct TapLogger: AlertViewDelegate{
var taps: [Int] = []
mutating func buttonTapped(atIndex index: Int){
taps.append(index)
}
}
let alert = AlertView()
var logger = TapLogger()
alert.delegate = logger
alert.fire()
logger.taps //[] 空,因为上alert.delegate 的赋值中,是将一个复制的值赋给了delegate
// what's worse
// 如此赋值之后,实现了AlertViewDelegate 的具体类型就丢了。为了找回类型需要使用一个条件类型转换
if let theLogger = alert.delegate as? TapLogger{
print(theLogger.taps)
} // [1]
-
使用函数,而非代理
-
由上可见,当使用类作为代理时,容易造成引用循环;而使用结构体作为代理时,原来的值则不会改变。因此在代理和协议的模式中,并不适合使用结构体和类
-
综上,当代理协议中只定义了一个函数时,我们完全可以用一个存储回调函数的属性来替换原来的代理属性。
class AlertView{ var buttons: [String] var buttonTapped:((_ buttonIndex: Int) -> ())? init(buttons: [String] = ["OK", "Cancel"]){ self.buttons = buttons } func fire(){ buttonTapped?(1) } } struct TapLogger{ var taps: [Int] = [] mutating func logTap(index: Int){ taps.append(index) } } let alert = AlertView() var logger = TapLogger() alert.buttonTapped = logger.logTap // error:不允许部分应用‘可变’方法 alert.buttonTapped = {logger.logTap(index: $0)}
-
4. inout 参数和可变方法
一个inout 参数持有一个传递给函数的值,函数可以改变这个值,然后从函数中传出并替换掉原来的值
-
Swift 中的inout + & 绑定使用看起来非常像传递引用。但事实上,inout 做的事情是传值,然后复制回来,而非传递引用
-
lvalue & rvalue。
-
lvalue 描述的是一个内存地址,是左值「left value」的缩写
-
rvalue 描述的是一个值
-
inout 只能传递左值,因为右值不可修改
-
inout 也不传递用let 定义的变量,同样因为不可修改
-
对于所有的下标操作符,只要同时定义了get 和set 方法,都可以作为inout 参数
-
类似的,只要同时定义get 和set 方法,属性也可以作为左值
-
运算符作为函数也可以接受inout 值,但为了简化,调用时不需要加&
postfix func ++(x: inout Int){ x += 1 } var x = 1 x++
-
-
编译器可能会把inout 变量优化为引用传递,而非传入和传出时的复制。不过,文档已经明确指出我们不该依赖这种行为
- 在下一章将区别mutating 方法和接受inout 参数的函数之间的区别
-
&不意味inout 的情况
- 如果一个函数接受UnsafeMutablePointer 作为参数,我们也可以用& 传递可变的函数,此时,我们确实是在传递指针
5. 属性
计算属性
- 计算属性看起来和常规属性一样,但它并不用任何内存来存储自己的值。相反,这个属性每次被访问时,返回值都将被实时计算出来。计算属性实际上只是一个方法,只是他的定义和调用约定不太寻常。
import CoreLocation
struct GPSTrack {
var record: [(CLLocation, Date)] = []
}
struct GPSTrack {
private(set) var record: [(CLLocation, Date)] = []
}
extension GPSTrack{
// 返回GPS 追踪的所有时间戳
// - 复杂度:O(n), n是记录点的数量
var timestamps: [Date]{
return record.map{$0.1}
}
}
// 因为没有指定setter,所有timestamps 属性是只读的。它的结果不会被缓存,每次访问这个属性,结果都要被计算一遍。
Swift API 指南推荐你对所有复杂度不为O(1) 的计算属性都应该在文档中写明,因为调用者可能会假设访问一个属性的耗时是常数时间。
变更观察者
- willSet:属性被设置前
- didSet:属性被设置后
延迟存储属性
- 关键字 - lazy
- 延迟属性只能由var 定义,因为在初始化方法完成后,它的初始值可能仍旧是未设置的。而Swift 严格要求let 常量在实例的初始化方法完成前就拥有值。
- 延迟修饰符是编程记忆化的一种具体表现形式
- 访问一个延迟属性是mutating 操作,因为这个属性的初始值会在第一次访问时被设置。
- lazy 不会和任何线程同步。如果一个延迟属性完成计算之前,多个线程同时尝试访问它的话,计算有可能进行多次,计算过程中的各种副作用也会发生多次。
6 下标
下标看起来像是函数和计算属性的混合体,因为它接受函数,所以它像函数;因为它要么是只读的,要么是可读写的,因此它也像计算属性
整一个新的下标重载
extension Collection{
subscript(indices indexList: Index...) -> [Element]{
// 三个. 表示indexList 是一个可变长度参数。调入者可以传入零个或多个以逗号分开的指定类型的值
var result: [Element] = []
for index in indexList{
result.append(self[index])
}
return result
}
}
Array("abcdefghijklmnopqrstuvwxyz")[indices: 7, 4, 11, 11, 14]
下标进阶
- 异值字典[String: Any] 中嵌套元素的嵌套下标访问(正常情况不可实现,因为Any 没有实现下标)
var japan: [String: Any] = [ "name": "Japan",
"capital": "Tokyo",
"population": 126_740_000,
"coordinates": [
"latitude": 35.0,
"longitude": 139.0
]
]
japan["coordinates"]?["latitude"] = 36 // error: 类型‘Any'没有下标成员
(japan["coordinates"] as? [String: Double])?["latitude"] = 36.0 // error:不能对不可变表达式赋值
// 被类型转换的表达式已经不在是一个左值
// 通过为Dictionary 提供一个泛型下标的扩展,我们可以更美观高效地完成这件事
// 这个下标方法的第二个参数接受目标类型,并在下标实现中进行类型转换的尝试
extension Dictionary{
subscript<Result>(key: Key, as type: Result.Type) -> Result?{
get{
return self[key] as Result
}
set{
guard let value = newValue else{
self[key] = nil
return
}
guard let value2 = value as? Value else{
return
}
self[key] = value2
}
}
}
japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0
japan["coordinates"] // Optional(["latitude": 36.0, "longitude": 139.0])
7. 键路径
- 键路径是一个指向属性的未调用的引用,他和对某个方法的未使用的引用很类似
- 键路径以反斜杠开头
8. 自动闭包
短路求值
-
对于逻辑与
&&
,仅当已求得左侧值为true时,才会计算右侧的值,依赖于短路求值,下述代码在运行时将不会报错let evens = [2,4,6] if !evens.isEmpty && evens[0] > 10{ dosomething() } // 另一种形式的短路求值:第二个条件仅在第一个条件成功后才会进行判断 if let first = evens.first, first > 10{ dosomething() } // 尝试定义一个模拟实现了短路逻辑的操作函数 and func and(_ l: Bool, _ r: () -> Bool) -> Bool{ guard l else {return false} return r() } if and(!evens.isEmpty, {even[0]>10}){ dosomething() } // 利用@autoclosure:告诉编译器它应该将一个特定的参数用闭包表达式包装起来,从而使代码更加美观 func and(_ l: Bool, _ r: @autoclosure ()->Bool) -> Bool{ guard l else{return fasle} return r() } if and(!evens.isEmpty, even[0]>10){ dosomething() }
@escapong 标注
- 基于是否会为了稍后的使用而把闭包保存下来(如网络请求),或者说闭包是否只在函数的作用域中被同步调用(如map 和filter),我们可以讲闭包分为:
- 逃逸闭包:一个被保存在某个地方(比如一个属性中)等待稍后再调用的闭包
- 编译器强制我们在闭包表达式中显式地使用self —— 因为无意中对self 的强引用将引起引用循环
- 非逃逸闭包:永远不会离开一个函数的局部作用域的闭包
- 由于当一个函数返回时,非逃逸闭包将自动销毁,所以它不会创建一个固定的引用循环
- 逃逸闭包:一个被保存在某个地方(比如一个属性中)等待稍后再调用的闭包
- @escaping 用于手动创建一个逃逸闭包(闭包参数默认是非逃逸的)
Summary
函数是Swift 中的头类对象。
将函数视作数据可以让我们的代码更加灵活。