一:函数与代理
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.
网友评论