美文网首页
Swift进阶八: 函数2

Swift进阶八: 函数2

作者: Trigger_o | 来源:发表于2022-05-19 19:33 被阅读0次

    一:函数与代理

    class AlertView {
        var buttons: [String]
        weak var delegate: AlertViewDelegate?
        init(buttons: [String] = ["OK", "Cancel"]) {
            self.buttons = buttons
        }
        func fire() {
            delegate?.buttonTapped(atIndex: 1)
        }
    }
    
    protocol AlertViewDelegate: AnyObject {
        func buttonTapped(atIndex: Int)
    }
    
    class AAViewController: AlertViewDelegate {
        let alert: AlertView
        init() {
            alert = AlertView(buttons: ["OK", "Cancel"])
            alert.delegate = self
        }
        func buttonTapped(atIndex index: Int) {
            print("Button tapped: \(index)")
        }
    }
    

    在OC中,这是一段最常用的协议与代理的使用方法
    但是结构体实现不了AlertViewDelegate这个协议,因为它是一个针对类的协议.

    当然可以进行修改,去掉AnyObject,扩大协议的范围,这时候delegate也不能是weak了,代理会被强引用

    class AlertView {
        var buttons: [String]
        var delegate: AlertViewDelegate?
        init(buttons: [String] = ["OK", "Cancel"]) {
            self.buttons = buttons
        }
        func fire() {
            delegate?.buttonTapped(atIndex: 1)
        }
    }
    
    protocol AlertViewDelegate {
        mutating func buttonTapped(atIndex: Int)
    }
    

    然后创建一个结构体来作为alertView的代理

    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 // []
    

    可以想象到logger.taps是空的,因为结构体被复制了,数据其实在alert.delegate.taps里,更麻烦的是alert.delegate的类型是未知的,取数据还要转换.

    在OC中也会有声明一个block属性来实现代理的方案,在这个例子中,比协议更加方便
    改造一下AlertView,声明一个函数buttonTapped
    TapLogger去掉协议

    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(atIndex:) //报错:Cannot reference 'mutating' method as function value
    

    现在buttonTapped和logTap是一样的类型 ,但是却不能直接赋值,因为buttonTapped不能接收一个mutating,
    而且这个赋值也是不明确的,logger应该被复制还是应该被捕获.

    alert.buttonTapped = { logger.logTap(atIndex: $0) }
    

    这样就对了,目的是捕获变量logger,而不是获取值的拷贝

    再回到类的例子中

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

    显然这里alert.buttonTapped = buttonTapped(atIndex:)有循环引用,当alert.buttonTapped要执行它的内容的时候,它找到了CCViewController的buttonTapped,它需要持有对象才能找到这到这个方法
    因此需要弱化这个对象

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

    这里有有另一个问题:
    检查 ViewController.buttonTapped 这个表达式的类型时,发现它是(ViewController) -> (Int) -> (),这是一个接收一个ViewController类型的参数,然后返回一个(Int) -> ()函数的函数;
    总结来说,在底层,实例方法会被处理为这样一个函数:如果给定某个实例,它将返回另一个可以在该实例上进行操作的函数。someVC.buttonTapped 实际上只是 ViewController.buttonTapped(someVC) 的另一种写法, 两种表达式返回的都是类型为 (Int) -> () 的函数,这个函数强引用了someVC 实例.

    二:inout

    在C的印象中,&看起来像是传递引用,但是在inout的使用时还真不是,标准库这么解释:
    inout 参数将一个值传递给函数,函数可以改变这个值,然后将原来的值替换掉,并从函数中传出。

    为了了解到底什么样的表达式可以被当作 inout 参数传递,我们需要对 lvalue 和 rvalue 进行区分。lvalue 描述的是一个内存地址,它是 “左值 (left value)” 的缩写,因为 lvalues 是可以存在于赋值语句左侧的表达式。举例来说,array[0] 是一个 lvalue,它代表的是数组中第一个元素所在的内存位置。而 rvalue 描述的是一个值。2 + 2 是一个 rvalue,它描述的是 4 这个值。你不能把 2 + 2 或者 4 放到赋值语句的左侧.

    对于 inout 参数,你只能传递 lvalue 给他,因为我们不可能对一个 rvalue 进行改变;
    同样的let声明的变量也不行;
    只读的属性也不行;

    运算符其实也是inout模式,只不过&可以省略

    嵌套函数也可以捕获inout,不过当然不能让inout参数逃逸,否则复制->改变->传递的逻辑就没了

    func incrementTenTimes(value: inout Int) { 
        func inc() { 
            value += 1
         } 
        for _ in 0..<10 { inc() } 
    }
    //安全
    
    func escapeIncrement(value: inout Int) -> () -> () {
         func inc() { 
            value += 1
         } 
     return inc 
    }
     // error: 嵌套函数不能捕获 inout 参数
    

    需要注意的是,如果指定UnsafeMutablePointer(可变指针)类型的参数,&就真的会传递引用,而不是inout

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

    三:特殊的方法

    1.计算属性
    计算属性看起来和常规的属性很像,但是它并不使用任何内存来存储自己的值。相反,这个属性每次被访问时,返回值都将被实时计算出来
    实现 willSet 和 didSet 可以对属性进行观察,属性观察者必须在声明一个属性的时候就被定义,你无法在扩展里行追加。所以,这不是一个提供给类型用户的工具,它是专⻔为类型的设计者而设计的。willSet 和 didSet 本质上是一对属性的简写:一个负责为值提供存储的私有存储属性,以及一个公开的计算属性。这个计算属性的 setter 会在将值存储到存储属性中之前和/或之后,进行额外的工作。这和 Foundation中的键值观察有本质的不同,键值观察通常是对象的消费者来观察对象内部变化的手段,而与类的设计者是否希望如此无关.

    KVO 使用 Objective-C 的运行时特性,它动态地在类的 setter 中添加观察者,可以在运行时任意添加,这在现在的 Swift 中,特别是对值类型来说,是无法实现的。Swift 的属性观察是一个纯粹的编译时特性.

    2.下标方法

    下标是一个遵守特殊的定义和调用规则的方法,下标的行为很像普通的函数,只不过它们使用了特殊的语法,
    我们也可以为我们自己的类型添加下标支持,或者也可以为已经存在的类型添加新的下标重载,
    使用subscript定义下标方法

    extension Collection { 
        subscript(indices indexList: Index...) -> [Element] { 
            var result: [Element] = [] 
            for index in indexList { 
                result.append(self[index]) 
            } return result 
        } 
    }
    Array("abcdefghijklmnopqrstuvwxyz")[indices: 7, 4, 11, 11, 14]
    // ["h", "e", "l", "l", "o"]
    

    标准库的下标方法通常只有一个参数,并且没有参数标签,写起来都类似数组和字典(list[0],dic["name"]),上面的例子添加了参数标签来和标准库的方法做区分.
    另外下标方法也可以不止一个参数,比如说给字典添加一个方法把类型验证封装进去,就不用先get再转换类型,然后才能赋值了.

    extension Dictionary { 
        subscript<Result>(key: Key, as type: Result.Type) -> Result? { 
            get { return self[key] as? Result } 
            set { 
                guard let value = newValue as? Value else { return } 
                self[key] = value 
            }
         }
     }
    japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0
    

    四:键路径

    swift的keypath和Foundation有很大不同,Foundation的keypath只是字符串,具体使用取决于方法本身,swift的键路径 是一个类型,不需要用引号
    键路径表达式以一个反斜杠开头,比如 \String.count。反斜杠是为了将路径和同名的类型属性区分开来 (假如 String 也有一个 static count 属性的话,String.count 返回的就会是这个属性值了)。类型推断对键路径也是有效的,在上下文中如果编译器可以推断出类型的话,你可以将类型名省略,只留下 .count.

    struct Address { 
        var street: String 
        var city: String 
        var zipCode: Int
    }
    struct Person { 
        let name: String 
        var address: Address
    }
    let streetKeyPath = \Person.address.street
     // WritableKeyPath<Person, String>
    let nameKeyPath = \Person.name 
    // KeyPath<Person, String>
    

    <Person, String>是streetKeyPath和nameKeyPath的类型,表示这个keypath作用于Person类型并返回一个String类型.
    键路径可以由任意的存储和计算属性组合而成,其中还可以包括可选链操作符。编译器会自动为所有类型生成 [keyPath:] 的下标方法,但是读写性质取决于属性本身,可写的keypath类型叫做WritableKeyPath类型.

    let simpsonResidence = Address(street: "1094 Evergreen Terrace", city: "Springfeld", zipCode: 97475)
    var lisa = Person(name: "Lisa Simpson", address: simpsonResidence)
    lisa[keyPath: nameKeyPath] // Lisa Simpson
    
    lisa[keyPath: streetKeyPath] = "742 Evergreen Terrace"
    lisa[keyPath: nameKeyPath] = "Lisa Simpson" //报错
    

    可写的键路径
    用来读取或者写入一个值。和一对函数等效:一个负责获取属性值 ((Root) ->Value),另一个负责设置属性值 ((inout Root, Value) -> Void)。相比于只读的键路径,可写键路径要复杂的多。首先,它将很多代码包括在了简洁的语法中。将streetKeyPath 与等效的 getter 和 setter 对进行比较.

    let streetKeyPath = \Person.address.street 
    let getStreet: (Person) -> String = { 
        person in return person.address.street 
    }
    let setStreet: (inout Person, String) -> () = { 
        person, newValue in person.address.street = newValue
    }
    // 使⽤ 
    lisa[keyPath: streetKeyPath] // 742 Evergreen Terrace 
    getStreet(lisa) // 742 Evergreen Terrace
    

    设想你要将两个属性互相绑定:当属性 1 发生变化的时候,属性 2 的值会自动更新,反之亦然。可写的键路径在这种数据绑定的过程中会特别有用。比如,你可以将一个 model.name 属性绑定到 textField.text 上。API 的用户需要知道如何读写 model.name 和 textField.text,而键路径所解决的正是这个问题.

    实现双向绑定:

    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  }
                other[keyPath: otherKeyPath] = newValue
            }
    }
        
    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)
        }
        
    }
    

    (这段代码给我看麻了,先看看怎么用的,再来分析)

    fnal 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
    

    (还是不懂)

    从头开始
    首先NSKeyValueObservation是runtime对swift的扩展,它可以对一个 (Swift 的强类型) 键路径进行观察,需要给属性添加关键字@objc dynamic,它是这样使用的

    var observation: NSKeyValueObservation?
    @objc dynamic var name = ""
    observation = self.observe(\.name, options: [.new]) { (obj, change) in
      // do something
     }
    

    因此前面给NSObjectProtocol扩展的observe方法就是返回一个NSKeyValueObservation,可以把他叫做一个 token,调用者使用这个 token 来控制观察的生命周期:属性观察会在这个 token 对象被销毁或者调用者调用了它的 invalidate 方法时停止.当获取到newvalue时,通过另一个keypath将值赋给other.

    而bind方法实现了双重绑定,我们对两个对象都调用 observe,它们将返回两个观察 token.

    相关文章

      网友评论

          本文标题:Swift进阶八: 函数2

          本文链接:https://www.haomeiwen.com/subject/zsdaurtx.html