木又的《Swift进阶》读书笔记——函数

408 阅读8分钟

函数

综述

按照重要程度排序的三件是:

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 参数持有一个传递给函数的值,函数可以改变这个值,然后从函数中传出并替换掉原来的值。

  • 区分 lvaluervalue : 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 变量前面加上 & 传递给它。在这种情况下,你确实在传递引用,更确切地说,是在传递指针。

属性

  • 有两种方法和其他普通复方法有所不同,那就是计算属性和下标操作符。计算属性看起来和常规的属性很像,但是它并不使用任何内存来存储自己的值。相反,这个属性每次被访问时,返回值都将被实时计算出来。计算属性实际上只是一个方法,只是他的定义和调用约定不太寻常。

变更观察者

  • 我们也可以为属性和变量实现 willsetdidSet 方法,每次当一个属性被设置时 (就算它的值没有发生变化),这两个方法都会被调用。
  • wilSetdidSet 本质上是一对属性的简写: 一个是存储值的私有存储属性;另一个是读取值的公开计算属性,这个计算属性的 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 调用的结果中逃逸的话,会造成不确定的行为。