Functions

Functions

三个特征:

  1. 可以被赋值给变量, 可以当作参数传递(in / out)
    • 只能赋值函数名, 不能带参数, (将来可能允许)
  2. 能捕捉上下文变量
  3. 能用func关键字创建, 也可以用closure表达式创建(即{})
    • 可以理解为function literals, 就像1hello分别是IntStringliteral
    • 同时也是一个匿名函数
func counterFunc() -> (Int) -> String {
    var counter = 0
    func innerFunc(i: Int) -> String {
        counter += i // counter is captured
        return "Running total: \(counter)"
    }
    return innerFunc
}
let f = counterFunc()
f(3) // Running total: 3
f(4) // Running total: 7
let g = counterFunc()
g(2) // Running total: 2
g(2) // Running total: 4
f(2) // Running total: 9
  1. f 捕获了创建当时的上下文里counter的值, 这个counter在counterFuncreturn后会一直存在, 直到f被销毁.
  2. 但是重新调用counterFunc会创建新的counter
  3. In programming terminology, a combination of a function and an environment of captured variables is called a closure.

关于closure的几个约定:

  1. 如果你需要传递一个函数作为参数, 没必要先把函数赋值给一个变量, 直接传递闭包即可
  2. 如果闭包的参数类型是确定(可推断的)的, 可以省略类型声明, 入参出差均适用
  3. 如果函数体只有一个表达式, 那么它的返回值即为闭包的返回值
  4. 如果闭包的参数只有一个, 可以省略参数名, 只用$0表示第一个参数, 以此类推
  5. 如果函数的最后一个参数是一个闭包, 可以把闭包表达式放在函数参数列表的后面(从括号里挪出来)
  6. 如果函数只有一个参数, 并且这个参数是一个闭包, 那么可以省略括号

对照下面的例子看一下更直观:

[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } ) // 1. 直接传递闭包
[1, 2, 3].map( { i in return i * 2 } ) // 2. 类型推定
[1, 2, 3].map( { i in i * 2 } ) // 3. 返回值推定
[1, 2, 3].map( { $0 * 2 } ) // 4. 参数名省略
[1, 2, 3].map() { $0 * 2 } // 5. 闭包表达式挪到后面
[1, 2, 3].map { $0 * 2 } // 6. 参数只有一个, 省略括号

关于第4点, 注意两点:

  1. 使用位置参数如$0, 其实是省略掉了整个参数一节
  2. 而如果你的实现体根本不需要这些参数, 那么应该显式声明我知道参数的存在, 但不关心
(0..<3).map { _ in Int.random(in: 1..<100) } // [3, 63, 60]

关于类型推断, 下例中仍然能推断出入参为Int出参为Bool, 神不神奇:

let isEven = { $0 % 2 == 0 }

但是如果你的入参不是一个Int, 则需要显式声明类型了:

let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }

原因:

protocol ExpressibleByIntegerLiteral {
    associatedtype IntegerLiteralType
    /// Create an instance initialized to `value`.
    init(integerLiteral value: IntegerLiteralType)
}
    /// The default type for an otherwise-unconstrained integer literal.
    typealias IntegerLiteralType = Int

这个协议定义了数字字面量会被转化为什么类型. 如果你改为UInt32, 那么整数的字面量就会被推断为UInt32类型, 而不是Int了.

参考下别的解决从字典量推断类型的方法, 这些属于语法许可, 但是比较偏门, 因为使用closure的场景多数是有明确的上下文的, 而不是强行给一个上下文让其推断类型.

let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool

Flexibility through Functions

记一个按字典排序的知识点:

let animals = ["elephant", "zebra", "dog"]
animals.sorted { lhs, rhs in
    let l = lhs.reversed()
    let r = rhs.reversed()
    return l.lexicographicallyPrecedes(r) // 字典顺序
}
// ["zebra", "dog", "elephant"]

lexicographicallyPrecedes也叫电话簿排序, 它依次从两边的元素里取出一个值相比, 直到碰到不等的为止, 所以显然, 它更大的作用在于能为两个数组排序, 比如排序[1,2,3,5][1,3,4,5], 就是比较第一个不相等的元素(2和3)

Compare this with the way sorting works in Objective-C. If you want to sort an array using Foundation, you’re met with a long list of different options:

  • there are sort methods that take a selector,
  • block,
  • or function pointer as a comparison predicate,
  • or you can pass in an array of NSSortDescriptors to define the sorting criteria. All of these provide a lot of flexibility and power, but at the cost of complexity.

Support for selectors and dynamic dispatch is still there in Swift, but the Swift standard library favors a more function-based approach instead.

看一个OC方法栈的例子, 使用三个排序规则:

let lastDescriptor = NSSortDescriptor(key: #keyPath(Person.last),
    ascending: true,
    selector: #selector(NSString.localizedStandardCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: #keyPath(Person.first),
    ascending: true,
    selector: #selector(NSString.localizedStandardCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: #keyPath(Person.yearOfBirth),
    ascending: true)

let descriptors = [lastDescriptor, firstDescriptor, yearDescriptor]

(people as NSArray).sortedArray(using: descriptors)

上述使用到了两个Objective-CRuntime特性:

  • keyPath: 不同于Swift的原生的keyPath, 它只是一个字符串
  • key-value coding: (KVC), which looks up the value of a key at runtime.
    * `selector`也同样是一个字符串, 在`Runtime`里找到对应的函数
    

关于Swfit的改写请详阅原文.

Functions as Delegates

protocol AlertViewDelegate: AnyObject {
    func buttonTapped(atIndex: Int)
}

这种定法是class-only protocol, 因为它可以被class实现, 并且成为别人的weak属性, 永远不会被强持有, 有效避免循环引用.

weak var delegate: AlertViewDelegate?
func fire() {
    delegate?.buttonTapped(atIndex: 0)
}

但是如果需要被Struct实现的话, 应该怎么声明呢?

protocol AlertViewDelegate {
    mutating func buttonTapped(atIndex: Int)
}

但是丧失了weak的特性, 如果你用一个object实现了这个协议并设为某个类的代理, 那么这个类将强持有这个object. 比如你用一个结构体来记录点击的次数:

struct TapLogger: AlertViewDelegate {
    var taps: [Int] = []
    mutating func buttonTapped(atIndex index: Int) {
        taps.append(index)
    }
}

实际使用会碰到两个问题:

  1. 你tap了几次之后, 会发现taps仍然为空
  2. struct的delegate甚至丢失了类型牲:
// 需要强转一下才能使用代理的方法
if let theLogger = alert.delegate as? TapLogger {
    print(theLogger.taps)
}

显然不是一种好的实践.

Functions Instead of Delegates

即使用var buttonTapped: ((_ buttonIndex: Int) -> ())?这样的行为去替换掉代理(特别是只有一个方法的代理).

class ViewController {
    let alert: AlertView
    init() {
        alert = AlertView(buttons: ["OK", "Cancel"])
        alert.buttonTapped = self.buttonTapped(atIndex:)
    }
    func buttonTapped(atIndex index: Int) {
        print("Button tapped: \(index)")
    }
}

按钮事件的回调, 不再到代理里去找, 而是一个函数, 但上面这种写法显然造成了循环引用, self持有alert, alert持有self的引用, 所以需要使用weak:

alert.buttonTapped = { [weak self] index in
    self?.buttonTapped(atIndex: index)
}

对任何实例的属性或方法的引用都会造成持有, 所以使用的时候需要特别注意.

inout Parameters and Mutating Methods

  • &形式使用的intout参数可能会让人误以为是引用, 但实际上并不是, 它只是把参数的值拷贝了一份, 然后把拷贝的值传给函数, 函数修改了拷贝的值, 然后返回给调用者.
  • 你只能传递能被赋值的变量, 比如array[0]可以, 但2+2不可以
    • 同样, let声明的变量也不可以
    • gettersetter才行, 所以计算属性也是不可以的
    • 书上定义为lvaluervalue, 即写在等号左边还是右边的值, 显然, 左边的能被赋值.
var point = Point(x: 0, y: 0)
increment(value: &point.x)
point // Point(x: 1, y: 0)

这样也是可以的, 所以&只是个语法而已, 不是代表地址.

编译器可能会优化为引用传递而不是复制, 但我们不应依赖这一点.

此外, 嵌套的inout是没问题的, 但escaping是不可以的(因为你不知道这个函数什么时候被调用, 所以不能保证inout参数的值返回的时候数据源还在不在).

& 不意味 inout 的情况

& 除了在将变量传递给 inout 以外,还可以⽤来将变量转换为⼀个不安全的指针。(这就是我们最初误以为的那个场景:)

func incref(pointer: UnsafeMutablePointer<Int>) -> () -> Int {
    // 将指针复制存储在闭包中
    return {
        pointer.pointee += 1
        return pointer.pointee
    }
}

因为Swift会将Array隐式地退化为指针, 所以会造成这样的调用:

let fun: () -> Int
do {
    var array = [0]
    fun = incref(pointer: &array)
}
fun()
  1. array在出了do后就不存在了, 这个地址就是个时指针
  2. 对野指针指向的对象加1后果也是不可知的.

Properities

computed properties

  • computed properties其实就是一个方法, 加上语法支持让其表现得像访问一个property一样.
  • Apple鼓励你在定义computed properties的时候注释上它的时间复杂度, 因为调用者很可能误以为它就是一个属性, 那样复杂度只是O(1), 调用者可能就会频繁调用, 实际上它可能是 O(n) 的复杂度(比如遍历数组后得到的结果), 这样就会造成性能问题.

Observe Changes

属性观察者必须在声明⼀个属性的时候就被定义,你⽆法在扩展⾥进⾏追加。所以,这不是⼀个提供给类型⽤⼾的⼯具,它是专⻔为类型的设计者⽽设计的。 不过,你可以在⼦类中重写⼀个属性,来添加观察者。下⾯就是⼀个例⼦:

class Robot {
    enum State {
        case stopped, movingForward, turningRight, turningLeft
    }
    var state = State.stopped
}
class ObservableRobot: Robot {
    override var state: State {
        willSet {
            print("状态从 \(state) 迁移到 \(newValue)")
        }
    }
}
var robot = ObservableRobot()
robot.state = .movingForward // 状态从 stopped 迁移到 movingForward

Lazy Stored Properties

class GPSTrackViewController: UIViewController {
    var track: GPSTrack = GPSTrack()
    lazy var preview: UIImage = {
        for point in track.record {
            // Do some expensive computation.
        }
        return UIImage(/* ... */)
    }()
}
  • lazy本质上是执行一个闭包, 只不过编译器会把闭包的执行放到第一次访问的时候, 而不是在初始化的时候.
  • 所以它是一个执行闭包后的结果, 而不是一个闭包, 所以后续访问这个属性的时候就是一个确定的值了
    • 所以lazy不是一个计算属性, 它的值在第一次确认后就不会再变了.
  • 存储变量和Lazy变量都不能在Extension里定义(就是因为需要存储空间)
  • 第一次访问会赋值, 所以lazy是一个mutating operation
    • 所以如果一个结构体含有lazy属性, 那么这个结构体将不能被let定义(除非你没用到lazy属性)
    • 但是没有任何线程保护, 在多线程下可能会被初始化多次
var point = Point(x: 3, y: 4)
point.distanceFromOrigin // 5.0 // sqrt(3*3+4*4)
point.x += 10
point.distanceFromOrigin // 5.0 // lazy不是计算属性

Subscripts

Custom Subscripts

extension Collection {
    subscript(indices indexList: Index...) -> [Element] {
        var result: [Element] = []
        for index in indexList {
            result.append(self[index])
        }
        return result
    }
    
    subscript(safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Array("abcdefghijklmnopqrstuvwxyz")[indices: 7, 4, 11, 11, 14]
// ["h", "e", "l", "l", "o"]
[1,2,3][safe: 4] // nil

其中, Index...表⽰indexList 是⼀个可变参数 (variadic parameter)

Advanced Subscripts

假定你有一个值为Any的字典, 你想逐层访问到最内层的值一般是不可行的, 因为你第一层取出来后就已经不知道类型了:

var japan: [String: Any] = [
    "name": "Japan",
    "capital": "Tokyo",
    "population": 126_440_000,
    "coordinates": [
        "latitude": 35.0,
        "longitude": 139.0
    ]
]
// Error: Type 'Any' has no subscript members.
japan["coordinates"]?["latitude"] = 36.0

// 强转是可以, 但是它就不能被赋值了
// Error: Cannot assign to immutable expression.
(japan["coordinates"] as? [String: Double])?["latitude"] = 36.0

考虑做一个新型的下标, 支持传入类型, 这样在取出的时候就有类型了:

extension Dictionary {
    subscript<Result>(key: Key, as type: Result.Type) -> Result? {
        get {
            return self[key] as? Result
        }
        set {
            // Delete existing value if caller passed nil.
            guard let value = newValue else {
            self[key] = nil
            return
        }
            // Ignore if types don't match.
            guard let value2 = value as? Value else {
            return
        }
        self[key] = value2
        }
    }
}

// 直接使用走的不是自定义下标
japan["coordinates"] // Optional(["longitude": 139.0, "latitude": 36.0])
// 自定义下标, 带类型转换
japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0

注意, 方法里的第二个参数as type在方法体里并没有用到, 但是约束了方法声明里的<Result>泛型是什么, 方法体里用的也是Result而不是type, 这是一种非常奇怪的用法, 要特别留意一下.

Key Paths

  • Swift 4 added the concept of key paths to the language.
  • A key path is an uninvoked reference(未调用引用) to a property, analogous to an unapplied method reference(未应用的方法引用).
  • Key-path expressions start with a backslash, e.g. \String.count.
    • 如果类型能被推断, 可以省略类型, 如\.count
  • The compiler automatically generates a new [keyPath:] subscript for all types.
    * `"hello"[keyPath: \.count]`等同于`"hello".count`
    
  • Key Path可以拼接: nameKeyPath.appending(path: \.count)
  • Key path除了表达属性, 也可以表达下标:
var bart = Person(name: "Bart Simpson", address: simpsonResidence)
let people = [lisa, bart]
people[keyPath: \.[1].name] // Bart Simpson

前面写过一个排序器, 为了自定义用哪个键排序, 还让用记传一个closure进来, 有了keyPath就简单多了:

func sortDescriptor<Root, Value>(key: KeyPath<Root, Value>)
-> SortDescriptor<Root> where Value: Comparable {
    return { $0[keyPath: key] < $1[keyPath: key] }
}
// Usage
let streetSDKeyPath: SortDescriptor<Person> =
sortDescriptor(key: \.address.street)

Witable Key Paths和双向绑定

演示一个实现属性绑定的例子:d

extension NSObjectProtocol where Self: NSObject {
    func observe<A, Other>(_ keyPath: KeyPath<Self, A>,
    writeTo other: Other,
    _ otherKeyPath: ReferenceWritableKeyPath<Other, A>)
    -> NSKeyValueObservation
    where A: Equatable, Other: NSObjectProtocol
    {
        return observe(keyPath, options: .new) { _, change in
            guard let newValue = change.newValue,
            other[keyPath: otherKeyPath] != newValue else {
                return // prevent endless feedback loop
            }
            other[keyPath: otherKeyPath] = newValue
        }
    }
}
  1. 扩展的是NSObjectProtocol而不是NSObject
  2. ReferenceWritableKeyPath保证了可以对let声明的变量进行写操作

有了observe(_:writeTo:_)方法, 我们就可以绑定属性了:

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 Sample: NSObject {
    @objc dynamic var name: String = ""
}
class MyObj: NSObject {
    @objc dynamic var test: String = ""
}
let sample = Sample()
let other = MyObj()
let observation = sample.bind(\Sample.name, to: other, \.test)
sample.name = "NEW"
other.test // NEW
other.test = "HI"
sample.name // HI

Autoclosures

配合Interview看看.

  • In the standard library, functions like assert and fatalError also use autoclosures

之前碰到autoclosure的时候我有疑问有什么作用, 把closure写成表达式, 为什么不直接传个表达式过去, 现在明白了, 延迟执行没讲到核心, 而是可以不执行, 比如说在assert里, 如果条件为false, 那么后面的autoclosure就不会执行了, 这可以避免一些不必要的计算.

@escaping (Private)

  • we need to use an explicit self in the completion handler of a network request,

  • whereas we don’t need to be explicit about self in closures passed to map or filter.

  • 要不要显式self取决于这个闭包是存起来稍后使用(escaping), 还是在作用域范围内同步使用. 这是刻意设计的提醒你这里可能会出现循环引用.

  • 不标记为escaping的闭包, 编译器是不会允许你保存或者传递的(比如传给调用者)

  • closures that never leave a function’s local scope are non-escaping.

  • A non-escaping closure can’t create a permanent reference cycle because it’s automatically destroyed when the function it’s defined in returns.

  • `