函数
综述
按照重要程度排序的三件是:
1、函数可以像 Int
或者 String
那样被赋值给变量,也可以作为另一个函数的输入参数,或者另一个函数的返回值来使用。
2、函数能够捕获存在于其局部作用域之外的变量。
3、有两种方法可以创建函数,一种是使用 func 关键字,另一种是 {} 。在 Swift 中,后一种被称为闭包表达式。
1.函数可以被复制给变量,也能够作为函数的输入和输出
- Swift 和很多现代化编程语言相同,都把函数视为“头等对象”。
2.函数可以捕获在于它们作用域之外的变量
- 当函数引用了在其局部作用域之外的变量时,这个变量就被捕获了,它们将会继续存在,而不是在超过作用域后被摧毁。
- 在编程术语里,一个函数和它所捕获的变量环境组合起来被称为闭包。
3.函数可以使用{}来声明为闭包表达式
-
与
func
相比,闭包表达式是匿名的,它们没有被赋予一个名字。 -
相比
func
,闭包表达式可以简洁得多
[1,2,3].map { $0 * 2} // [2,4,6]
函数的灵活性
函数作为数据
- 把函数作为数据使用的这种方式 (例如:在运行时构建包含排序函数的数组),把语言的动态行为带到了一个新的高度。
函数作为代理
通过定义一个协议,然后让代理的所有者实现这个协议,最后将它本身注册为代理,这样你就能获得那些回调。
Cocoa风格的代理
- 将代理属性标记为
weak
在实践中非常常见,这个约定让内存管理变得很容易。实现代理协议的类不需要担心引入引用循环的问题。
结构体上实现代理
- 一句话总结: 在代理和协议的模式中,并不适合使用结构体。
使用函数,而非代理
- 如果代理协议中只定义了一个函数的话,完全可以用一个存储回调函数的属性来替换原来的代理属性。
inout参数和可变方法
一个 inout 参数持有一个传递给函数的值,函数可以改变这个值,然后从函数中传出并替换掉原来的值。
-
区分
lvalue
和rvalue
: lvalue 描述的是一个内存地址,它是“左值 (left value)”的缩写,因为 lcalues 是可以存在于赋值语句左侧的表达式。举例来说,array[0]
是一个 lvalue,因为它描述了数组中第一个元素所在的内存位置。而 rvalue 描述的是一个值。2+2
是一个 rvalue,它描述的是4
这个值。你不能把2+2
或者4
放到赋值语句的左侧。 -
对于 inout 参数,你只能传递左值,因为右值是不能被修改的。当你在普通的函数或者方法中使用 inout 时,需要显式地将它们传入: 即在每个左值前面加上 & 符号。
-
编译器可能会把 inout 变量优化成引用传递,而非传入和传出时的复制。不过,文档已经明确指出我们不应该依赖这个行为。
嵌套函数和inout
可以在嵌套函数中使用 inout
参数,Swift 会保证这样的使用是安全的。
func incrementTenTimes(value: inout Int) {
func inc() {
value += 1
}
for _ in 0..<10 {
inc()
}
}
var x = 0
incrementTenTimes(value: &x)
x // 10
不过,不能够让这个 inout 参数逃逸。可以这么理解,因为 inout 的值会在函数返回之前复制回去。
& 不意味 inout 的情况
- 说到不安全 (unsafe) 的函数,你应该小心 & 的另一种含义: 把一个函数参数转换为一个不安全指针。如果一个函数接受
UnsafeMutablepointer
作为参数,你可以用和inout
参数类似的方法,在一个var
变量前面加上&
传递给它。在这种情况下,你确实在传递引用,更确切地说,是在传递指针。
属性
- 有两种方法和其他普通复方法有所不同,那就是计算属性和下标操作符。计算属性看起来和常规的属性很像,但是它并不使用任何内存来存储自己的值。相反,这个属性每次被访问时,返回值都将被实时计算出来。计算属性实际上只是一个方法,只是他的定义和调用约定不太寻常。
变更观察者
- 我们也可以为属性和变量实现 willset 和 didSet 方法,每次当一个属性被设置时 (就算它的值没有发生变化),这两个方法都会被调用。
- wilSet 和 didSet 本质上是一对属性的简写: 一个是存储值的私有存储属性;另一个是读取值的公开计算属性,这个计算属性的 setter 会在将值存储到私有属性之前和/或之后,进行额外的工作。
- KVO 使用 Obkective-C 的运行时特性,动态地在类的 setter 中添加观察者,这在现在的 Swift 中,特别是 对值类型来说,是无法实现的。Swift 的属性观察是一个纯粹的编译时特性。
延迟存储属性
- 延迟初始化一个值在 Swift 中是一种常见的模式,为了定义一个延迟初始化的属性,Swift 提供了一个专用的关键字
lazy
来定义一个延迟属性 (lazy property)。要注意的是,延迟属性只能用 var 定义,因为在初始化方法完成后,它的初始值可能仍然是未设置的。而 Swift 对 let 常量则有这严格的规则,它必须在实例的初始化方法完成之前就拥有值。延迟修饰符是编程 记忆化 的一种具体的表现形式。 - 和计算属性不同,存储属性和需要存储属性的延迟属性不能被定义在
扩展
中。 - 在结构体中使用延迟属性通常不是一个好主意。
- 需要注意,
lazy
关键字不会进行任何线程同步。如果在一个延迟属性完成计算之前,多个线程同时尝试访问它的话,计算有可能进行多次,计算过程中的各种副作用也会发生多次。
下标
自定义下标操作
- 我们可以为自己的类型添加下标支持,也可以为已经存在的类型添加新的下标重载。
下标进阶
- 下标并不局限于单个参数。下标还可以在参数或者返回类型上使用泛型。不过,大多数情况下,可能为你的数据定义一个自定义类型,然后让这些类型满足 Codeable 协议,来在值和数据交换格式之间进行转换,会是更好的选择。
键路径
- 键路径 (key paths) 是一个指向属性的未调用的引用,它和对某个方法的未使用的引用很类似。
- 键路径表达式以一个反斜杠开头,比如 \string.count
可以通过函数建模的键路径
- 一个将基础类型 Root 映射为类型为 Value 的属性的键路径,和一个具有
(Root) -> Value
类型的函数十分相似。而对于可写的建路径来说,则对应着一对获取和设置值的函数。相对于这样的函数,键路径除了在语法上更简洁外,最大的优势在于它们是值。你可以测试键路径是否相等,也可以将它们用作字典的间 (因为它们遵守Hashable)。另外,不像函数,键路径是不包含状态的,所以它也不会捕获可变的状态。如果使用普通的函数的话,这些都是无法做到的。 - 不过,键路径并没有给我们和函数一样的灵活度。键路径依赖 Value 满足 Comparable 这一前提。
可写键路径
-
可写键路径比较特殊:你可以用它来读取或者写入一个值。因此,它和一对函数等效: 一个负责获取属性值 ((Root) -> Value),另一个负责设置属性值 ((inout Root, Value) -> Void)。
-
可写键路径对于数据绑定特别有用
extension NSObjectProtocol where Self: NSObject { func bind<A, Other>(_ keyPath: ReferenceWritableKeyPath<Self, A>, to other: Other, _ otherKeyPath: ReferenceWritableKeyPath<Other,A>) -> (NSKeyValueObservation, NSKeyValueObservation) where A: Equatable, Other: NSObject { let one = observe(keyPath, writeTo: other, otherKeyPath) let two = other.observe(otherKeyPath, writeTo: self, keyPath) return (one,two) } } final class Person: NSObkect { @objc dynamic var name: String = "" } class TextField: NSObject { @objc dynamic var text String = "" } let person = Person() let textField = TextField() let observation = person.bind(\.name, to: textField, \.text) person.name = "John" textField.text // John textField.text = "Sarah" person.name // Sarah
键路径层级
键路径有五种不同的类型,每种类型都在前一种上添加了更加精确的描述及功能:
- AnyKeyPath 和 (Any) -> Any? 类型的函数相似
- PatialKeyPath<Source> 和 (Source) -> Any? 函数相似。
- KeyPath<Source, Target> 和 (Source) -> Target 函数相似。
- WritableKeyPath<Source, Target> 和 (Source) -> Target 与 (inout Source, Target) -> () 这一对函数相似。
- ReferenceWritableKeyPath<Source, Target> 和 (Source) -> Target 与 (Source, Target) -> () 这一对函数相似。第二个函数可以用 Target 来更新 Source 的值,且要求 Source 是一个引用类型。对 WritableKeyPath 和 ReferenceWritableKeyPath 进行区分是必要的,前一个类型的 setter 要求它的参数是 inout 的。
这几种键路径的层级结构现在是通过类的继承来实现的。
对比 Objective-C 的键路径
- 在 Foundation 和 Objective-C 中,键路径是通过字符串来建模的
未来的方向
- 一个可能的特性是通过
Codable
进行序列化。
自动闭包
-
&&
和||
操作符的短路求值特性 -
为了让代码更漂亮,我们可以使用 @autoclosure 标注来告诉编译器它应该将一个特定的参数用闭包表达式包装起来。
-
自动闭包在实现日志函数的时候也很有用。比如,下面是一个只有在条件为 true 的时候才会对日志消息进行求值的log函数:
func log(ifFalse condition: Bool, message: @autoclosure () -> (String), file: String = #file, function: String = #function, line: Int = #line) { guard !condition else { return } print("Assertion failed:\(message()), \(file): \(function) (line \(line))") }
这意味着你可以在传入的表达式中进行昂贵的计算,而不必担心在这个值没有使用时所带来的开销。这个 log 函数使用了像是
#file
,#function
和#line
这样的调试标识符。当被用作一个函数的默认参数时,它们代表的值分别是调用者所在的文件名、函数名以及行号,这会非常有用。 -
谨慎使用自动闭包特性。
过度使用自动闭包可能会让你的代码难以理解。使用时的上下文和函数名应该清晰地指出实际求值会被推迟。
@escaping 标注
- 一个被保存在某个地方(比如一个属性中)等待稍后再调用的闭包叫做逃逸闭包。相对的,永远不会离开一个函数的局部作用域的闭包就是非逃逸闭包。对于逃逸闭包,编译器强制我们在闭包表达式中显式地使用
self
,因为无意中对于 self 的强引用,是发生引用循环的最常见原因之一。 - 注意默认非逃逸的规则只对函数参数,以及那些直接参数位置 (immediate parameter position) 的函数类型有效。也就是说,如果一个存储属性的类型是函数的话,那么它将会是逃逸的 (这很正常)。出乎意料的是,对于那些使用闭包作为参数的函数,如果闭包被封装到像是元祖或者可选值等类型的话,这个闭包参数也是逃逸的。因为在这种情况下闭包不是直接参数,它将自动变为逃逸闭包。这样的结果是,你不能写出一个函数,使它接受的函数参数同时满足可选值和非逃逸。很多情况下,你可以通过为闭包提供一个默认值来避免可选值。如果这样做行不通的话,可以通过重载函数,提供一个包含可选值 (逃逸) 的函数,以及一个不是可选值,非逃逸的函数来绕过这个限制。
withoutActuallyEscaping
- 我们可能会遇到这种情况:确实知道一个闭包不会逃逸,但是编译器无法证明这点,所以它会强制你添加
@escaping
标注。对于这种情况,Swift 提供了一个 withoutActuallyEscaping 函数来作为一种“安全出口”。这个函数允许你对一个接受逃逸闭包的函数,传入一个非逃逸的闭包。 - 注意,使用 withoutActuallyEscaping 后,就进入了 Swift 中不安全的领域。让闭包的复制从 withoutActuallyEscaping 调用的结果中逃逸的话,会造成不确定的行为。