Swift 轻松掌握喵神高阶心法 VII
quan452
8年前
<p>今天鹅厂的同学推荐新研发的iOS UI框架, 果然非同凡响, 粗略估计能够覆盖到85%以上的需求, 各方面的组件化及布局流等应有尽有, 可是自定义转场方面的还没有看到(也可能是眼拙), 不过是Objective-C的, 可能是最近Javascript和Swift敲多了, Objective-C的语法不免有种古老和陈旧的感觉(各位iOS的同学轻喷啊~) </p> <h3>Tip29 属性观察</h3> <p>属性观察(Property Observers)是Swift中一个很特殊的特性, 利用属性观察我们可以在当前类型内监视对于属性的设定, 并作出一些响应. Swift中为我们提供了两个属性观察的方法, 它们分别是willSet和didSet.</p> <p>使用这两个方法十分简单, 我们只要在属性声明的时候添加相应的代码块, 就可以对将要设定的值和已经设置的值进行监听了:</p> <pre> <code class="language-swift">class MyClass { var date: NSDate { willSet { let d = date print("即将将日期从\(d)设定至\(newValue)") } didSet { print("已经将日期从\(oldValue)设定至\(date)") } } init() { date = NSDate() } } let foo = MyClass() foo.date = foo.date.addingTimeInterval(10086) // 输出 // 即将将日期从2016-12-23 03:46:44 +0000设定至2016-12-23 06:34:50 +0000 // 已经将日期从2016-12-23 03:46:44 +0000设定至2016-12-23 06:34:50 +0000</code></pre> <p>在willSet和didSet中我们分别可以使用newValue和oldValue来获取将要设定的和已经设定的值. 属性观察的一个重要用处是作为设置值的验证, 比如上面的例子中我们不希望date超过一年以上的话, 我们可以将didSet修改一下</p> <pre> <code class="language-swift">class MyClass { let oneYearInSecond: TimeInterval = 365 * 24 * 60 * 60 var date: NSDate { willSet { let d = date print("即将将日期从\(d)设定至\(newValue)") } didSet { if date.timeIntervalSinceNow > oneYearInSecond { print("设定的时间太晚了!") date = NSDate().addingTimeInterval(oneYearInSecond) } print("已经将日期从\(oldValue)设定至\(date)") } } init() { date = NSDate() } }</code></pre> <p>更改一下调用, 我们就能看到效果:</p> <pre> <code class="language-swift">// 365 * 24 * 60 * 60 = 31_536_000 let foo = MyClass() foo.date = foo.date.addingTimeInterval(100_000_000) //输出 //即将将日期从2016-12-23 03:53:53 +0000设定至2020-02-23 13:40:33 +0000 //设定的时间太晚了! //已经将日期从2016-12-23 03:53:53 +0000设定至2017-12-23 03:53:53 +0000</code></pre> <p>初始化方法对属性的设定, 以及在willSet和didSet中对属性的再次设定都不会再次触发属性观察的调用, 一般来说这会是你所需要的行为, 可以放心使用.</p> <p>我们知道, 在Swift中所声明的属性包括存储属性和计算属性两种. 其中存储属性将会在内存中实际分配地址对属性进行存储, 而计算属性则不保罗背后的存储, 只是提供set和get两种方法. 在同一个类型中, 属性观察和计算属性是不能同时共存的. 也就是说, 想在一个属性定义中同时出现set和willSet或didSet是一件办不到的事情. 计算属性中我们可以通过改写set中的内容来达到和willSet及didSet同样的属性观察的目的. 如果我们无法改动这个类, 又想要通过属性观察做一些事情的话, 可能就需要子类化这个类, 并且重写它的属性了. 重写的属性并不知道父类属性的具体实现情况, 而只从父类属性中继承名字和类型, 因此在子类的重载属性中我们是可以对父类的属性任意地添加属性观察的, 而不用在意父类中到底是存储属性还是计算属性:</p> <pre> <code class="language-swift">class A { var number: Int { get { print("get") return 1 } set {print("set")} } } class B: A { override var number: Int { willSet {print("willSet")} didSet {print("didSet")} } } // 调用 number 的 set 方法可以看到工作的顺序 let b = B() b.number = 0 //输出 //get //willSet //set //didSet</code></pre> <p>set和对应的属性观察的调用都在我们的预想之中. 这里要处以的是get首先被调用了一次. 这是因为我们实现了didSet, didSet中会用到oldValue, 而这个值需要在整个set动作之前进行获取并存储代用, 否则将无法确保正确性. 如果我们不实现didSet的话, 这次get操作也将不存在.</p> <p>验证结果:</p> <pre> <code class="language-swift">class A { var number: Int { get { print("get") return 1 } set {print("set")} } } class B: A { override var number: Int { willSet {print("willSet")} } } let b = B() b.number = 0 //输出 //willSet //set</code></pre> <p>得到结论: didSet中获取oldValue的方法会主动调用get方法.</p> <h3>Tip30 final</h3> <p>final关键字可以用在class. func或者var前面进行修饰, 表示不允许对该内容进行继承或者重写操作. 这个关键字的作用和C#中的sealed相同, 而sealed其实在C#算是一个饱受争议的关键字. 有一派程序员认为, 类似这样的禁止继承和重写的做法是非常有益的, 它可以更好地对代码进行版本控制, 得到更佳的性能, 以及使代码更安全. 因此他们甚至认为语言应当是默认不允许继承的, 只有在显式地指明可以继承的时候才能子类化.</p> <p>在这里我不打算对这样的想法做出判断或者评价, 虽然上面列举的优点都是事实, 但是另一个事实是不论是Apple或者微软, 以及世界上很多其他语言都没有做出默认不让继承和重写的决定. 带着"这不是一个可以滥用的特性"的观点, 我们来看看在写Swift的时候可能会在什么情况下使用final.</p> <p>权限控制</p> <p>给一段代码加上final就意味着编译器向你做出保证, 这段代码不会再被修改; 同时, 这也意味着你认为这段代码已经完备并且没有再被进行继承或重写的必要, 因此这往往会是一个需要深思熟虑的决定. 在Cocoa开发中app开发是一块很大的内容 对于大多数我们自己完成的面向app开发代码, 其实不太会提供给别人使用, 这种情况下即时是将所有自己写的代码标记为final都是一件无可厚非的事情(但我并不是在鼓励这么做) --因为在需要的任何时候你都可以将这个管家你去掉以恢复其可继承性. 而在开发给其他开发者使用的库时, 就必须更深入考虑各种使用场景和需求了.</p> <p>一般来说, 不希望被继承和重写会有这几种情况:</p> <p>类或者方法的功能缺失已经完备了</p> <p>对于很多的辅助性质的工具类或者方法, 可能我们会考虑加上final. 这样的类又一个比较大的特点, 是很可能只包含类方法而没有实例方法. 比如我们很难想到一种情况需要继承或重写一个负责计算一段字符串的MD5或者AES加密解密的工具类. 这种工具类和方法的算法是经过完备验证和固定的, 使用者只需要调用, 而相对来说不可能有继承和重写的需求.</p> <p>这种情况很多时候遵循的是以往经验和主观判断, 而单个的开发者的判断其实往往并不可靠. 遇到希望把某个自己开发的类或者方法标为final的时候, 去找一个富有经验的开发者, 问问他们的意见或者看法, 应该是一个比较靠谱的做法.</p> <p>子类继承和修改是一件危险的事情</p> <p>在子类继承或重写某些方法后可能做一些破坏性的事情, 导致子类或者父类部分也武大正常工作的情况. 举个例子, 在某个公司管理的系统中我们对员工按照一定规则进行编号, 这样通过编号我们能迅速找到任一员工. 而假如我们在子类中重写了这个编号方法, 很可能就导致基类中的依赖员工编号的方法失效. 在这类情况下, 将编号方法标记为final以确保稳定, 可能是一种更好的做法.</p> <p>为了父类中某些代码一定会被执行</p> <p>有时候父类中有一些关键代码是在被继承重写后必须执行的(比如状态配置, 认证等等), 否则将导致运行时候的错误. 而在一般的方法中, 如果子类重写了父类方法, 是没有办法强制子类方法一定去调用相同的父类方法的. 在Objective-C的时候我们可以通过指定 <strong>attribute</strong> ((objc_requires_super)) 这样的属性来让编译器在子类没有调用父类方法时抛出警告. 在Swift中对原来的很多attribute的支持现在还缺失中, 为了达到类似的目的, 我们可以使用一个final的方法, 在其中进行一些必要的配置, 然后再调用某个需要子类实现的方法, 以确保正常运行:</p> <pre> <code class="language-swift">class Parent { final func method() { print("开始配置") // ..必要的代码 methodImpl() // ..必要的代码 print("结束配置") } func methodImpl() { fatalError("子类必须实现这个方法") // 或者也可以给出默认实现 } } class Child: Parent { override func methodImpl() { // ..子类的业务逻辑 } }</code></pre> <p>这样, 无论如何我们如何使用method, 都可以保证需要的代码一定会被运行过, 而同时又给了子类继承和重写自定义具体实现的机会.</p> <p>性能考虑</p> <p>使用 final 的另一个重要理由是可能带来的性能改善. 因为编译器能够从final中获取额外信息, 因此可以对类或者方法调用进行额外的优化处理. 但是这个又是在实际表现中可能带来的好处其实就算与Objective-C的动态派发相比也十分有限, 因此在项目还有其他方面可以优化(一般来说会是算法或者图形相关的内容导致性能瓶颈)的情况下, 并不建议使用将类或者方法转为final的方式来追求性能的提升.</p> <h3>Tip31 lazy 修饰符和 lazy 方法</h3> <p>延时加载或者是延时初始化是很常用的优化方法, 在构建和生成新的对象的时候, 内存分配会在运行时耗费不少时间, 如果有一些对象的属性和内容非常复杂的话, 这个时间更是不可忽略. 另外, 有些情况下, 我们并不会立即用到一个对象的所有属性, 而默认情况下初始化时, 那些在特定环境下不被使用的存储属性, 也一样要被初始化和赋值, 也是一种浪费.</p> <p>在其他语言(包括Objective-C)中延时加载的情况是很常见的. 我们在第一次访问某个属性时, 判断这个属性背后的存储是否已经存在, 如果存在则直接返回, 如果不存在则说明是首次访问, 那么就进行初始化并存储后再返回. 这样我们可以把这个属性的初始化时刻推迟, 与包含它的对象的初始化时刻分开, 以达到提升性能的目的. 以Objective-C举个例子(虽然这里既没有费时操作, 也不会因为使用延时加载而造成什么性能影响, 但是作为一个最简单的例子, 可以很好地说明问题):</p> <pre> <code class="language-swift">ClassA.h @property (nonatimic, copy) NSString * testString; ClassA.m - (NSString *)testString { if (!_testString) { _testString = @"Hello"; NSLog(@"只在首次访问输出"); } return _testString; }</code></pre> <p>在初始化ClassA 对象后, _testString是nil. 只有当首次访问testString属性时getter方法会被调用, 并检查如果还没有初始化的话, 就进行赋值. 为了方便确认, 我们还在赋值时打印一句log. 我们之后再多次访问这个属性的话, 因为_testString已经有值, 因此将直接返回.</p> <pre> <code class="language-swift">class ClassA { lazy var str: String = { let str = "Hello" print("只在首次访问输出") return str }() }</code></pre> <p>我们在使用lazy作为属性修饰符时, 只能声明属性是变量. 另外我们需要显式地指定属性类型, 并使用一个可以对这个属性进行赋值的语句来在首次访问属性时运行. 如果我们多次访问这个实例的str属性的话, 可以看到只有一次输出.</p> <p>为了简化, 我们如果不需要做什么额外工作的话, 也可以对这个lazy的属性直接写赋值语句:</p> <pre> <code class="language-swift">lazy var str: String = "Hello"</code></pre> <p>相比起在Objective-C中实现方法, 现在的lazy使用起来要方便的多.</p> <p>另外一个不太引起注意的是, 在Swift的标准库中, 我们还有一组lazy方法, 它们的定义是这样的:</p> <pre> <code class="language-swift">func lazy<S : SequenceType>(s: S) -> LazySequence<S> func lazy<S : CollectionType where S.Index : RandomAccessIndexType>(s: S) -> LazyRandomAccessCollection<S> func lazy<S : CollectionType where S.Index : BidirectionalIndexType>(s: S) -> LazyBidirectionalCollection<S> func lazy<S : CollectionType where S.Index : ForwardIndexType>(s: S) -> LazyForwardCollection<S></code></pre> <p>这些方法可以配合像map或者filter这类接受闭包并进行运行的方法一起, 让整个行为变成延时进行的. 在某些情况下这么做也对性能会有不小的帮助. 例如, 直接使用map时:</p> <pre> <code class="language-swift">let data = 1...3 let result = data.map { (i: Int) -> Int in print("正在处理\(i)") return i * 2 } print("准备访问结果") for i in result { print("操作后结果为\(i)") } print("操作完毕") 这么做的输出为: //正在处理1 //正在处理2 //正在处理3 //准备访问结果 //操作后结果为2 //操作后结果为4 //操作后结果为6 //操作完毕</code></pre> <p>而如果我们先进行一次lazy操作的话, 我们就能得到延时运行版本的容器:</p> <pre> <code class="language-swift">let data = 1...3 let result = data.lazy.map { (i: Int) -> Int in print("正在处理\(i)") return i * 2 } print("准备访问结果") for i in result { print("操作后结果为\(i)") } print("操作完毕") /* 此时的运行结果 //准备访问结果 //正在处理1 //操作后结果为2 //正在处理2 //操作后结果为4 //正在处理3 //操作后结果为6 //操作完毕</code></pre> <p>对于那些不需要完全运行, 可能提前退出的情况, 使用lazy来进行性能优化效果会非常有效.</p> <h3>Tip32 Reflection 和 Mirror</h3> <p>熟悉Java的读者可能会知道反射(Reflection). 这是一种在运行时检测, 访问或者修改类型的行为的特性. 一般的静态语言类型的结构和方法的调用等都需要在编译时决定, 开发者能做的很多时候只是使用控制流(比如if 或者 switch)来决定作出怎样的额设置或是调用哪个方法. 而反射特性可以让我们有机会在运行的时候通过某些条件实时地决定调用的方法, 或者甚至向某个类型动态地设置甚至加入属性及方法, 是一种非常灵活和强大的语言特性.</p> <p>Objective-C中我们不太会经常提及到"反射"这样的词语, 因为Objective-C的运行时比一般的反射还要灵活和强大. 可能很多读者已经习以为常的像是通过字符串生成类或者selector, 并且进而生成对象或者调用方法等, 其实都是反射的具体的表现. 而在Swift中其实就算抛开Objective-C的运行时的部分, 在纯Swift范畴内也存在有反射相关的一些内容, 只不过相对来说功能要弱的多.</p> <p>因为这部分内容并没有公开的文档说明, 所以随时可能发生变动, 或者甚至存在今后被从Swift的可调用标准库中去掉的可能(Apple已经干过这种事情, 最早的时候Swift中甚至有隐式的类型转换 __conversion, 但因为太过危险, 而被彻底去除了. 现在隐式转换必须使用字面量转换的方式进行了). 在实际的项目中, 也不建议使用这种没有文档说明的API, 不过有时候如果能稍微知道Swift中也存在这样的可能性的话, 也许会有帮助(也指不定哪天Apple就扔出一个完整版的反射功能呢).</p> <p>Swift中所有的类型都实现了 _Relectable, 这是一个内部协议, 我们可以通过 _reflect来获取任意对象的一个镜像, 这个镜像对象包含类型的基本信息, 在Swift2.0之前, 这是对某个类型的对象进行探索的一种方法. 在Swift2.0中, 这些方法已经从公开的标准库中移除了, 取而代之, 我们可以使用Mirror类型来做类似的事情:</p> <pre> <code class="language-swift">struct Person { let name: String let age: Int } let xiaoMing = Person(name: "XiaoMing", age: 16) let r = Mirror(reflecting: xiaoMing) // r是MirrorType print("xiaoMing 是\(r.displayStyle!)") print("属性个数:\(r.children.count)") for child in r.children { print("属性名:\(child.label), 值:\(child.value)") } //输出 //xiaoMing 是struct //属性个数:2 //属性名:Optional("name"), 值:XiaoMing //属性名:Optional("age"), 值:16</code></pre> <p>通过Mirror初始化得到的结果中包含的元素的描述都被结合在children属性下, 如果你有心可以到Swift标准库中查找它的定义, 它实际上是一个Child的集合, 而Child则是一对键值的多元组:</p> <pre> <code class="language-swift">public typealias Child = (label: String?, value: Any) public typealias Children = AnyCollection<Mirror.Type.Child></code></pre> <p>AnyForwardCollection是遵守CollectionType协议的, 因此我们可以简单地使用count来获取元素的个数, 而对于具体的 代表属性的多元组, 则使用下标进行访问. 在对于我们的例子中, 每个Child都是具有两个元素的多元组, 其中第一个是属性名, 第二个是这个属性所储存的值. 需要特别注意的是, 这个值有可能是多个元素组成嵌套的形式(例如属性值是数组或者字典的话, 就是这样的形式).</p> <p>如果觉得一个个打印太过于麻烦, 我们也可以简单地使用dump党阀来通过获取一个对象的镜像并进行标准输出的方式将其输出出来. 比如对上面的对象xiaoMing:</p> <pre> <code class="language-swift">dump(xiaoMing) //输出 //▿ Person // - name: "XiaoMing" // - age: 16</code></pre> <p>在这里因为篇幅有限, 而且这部分内容很可能随着版本而改变, 我们就不再一一介绍Mirror的更详细的内容了. 有兴趣的读者不妨打开Swift的定义文件并找到这个协议, 里面对每个属性和方法有非常详细的注释.</p> <p>对于一个从对象反射出来的Mirror, 它所包含的信息是完备的. 也就是说我们可以在运行时通过Mirror的手段了解一个Swift类型(当然NSObject类也可以)的实例的属性信息. 该特性最容易想到的应用的特性就是为任意model对象生成对应的JSON描述. 我们可以对等待处理的对象的Mirror值进行深度优先的访问, 并按照属性的valueType将它们归类对应到不同的格式化中.</p> <p>另一个常见的应用场景是类似对Swift类型的对象做像Objective-C中KVC那样的valueForKey: 的取值. 通过比较取到的属性的名字和我们想要取得的key值就行了, 非常简单:</p> <pre> <code class="language-swift">func valueFrom(_ object: Any,key: String) -> Any? { let mirror = Mirror(reflecting: object) for child in mirror.children { let (targetKey, targetMirror) = (child.label, child.value) if key == targetKey { return targetMirror } } return nil } //接上面的xiaoMing if let name = valueFrom(xiaoMing, key: "name") as? String { print("通过key得到值:\(name)") } //输出: //通过key得到值:XiaoMing</code></pre> <p>在现在的版本中, Swift的反射特性并不是非常强大, 我们只能对属性进行读取, 还不能对其设定, 不过我们有希望能在将来的版本中获得更为强大的反射特性. 另外需要特别注意的是, 虽然理论上将反射特性应用在实际app制作中是可行的, 但是这一套机制设计的最初目的是用于REPL环境和Playground中进行输出的. 所以我们最好遵守Apple的这一设定, 只在REPL和Playground中用它来对一个对象进行深层次的探索, 而避免将它用在app制作中 --因为你永远不知道什么时候它们就会失效或者被大幅改动.</p> <p> </p> <p>来自:http://www.jianshu.com/p/e80789c3e097</p> <p> </p>