Swift进阶黄金之路(一)html
上期遗留一个问题:为何 rethrows
通常用在参数中含有能够 throws
的方法的高阶函数中。git
咱们能够结合Swift的官方文档对rethrows
再作一遍回顾:github
A function or method can be declared with the
rethrows
keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.swift
返回rethrows
的函数要求至少有一个可抛出异常的函数式参数,而有以函数做为参数的函数就叫作高阶函数。api
这期分两方面介绍Swift:特性修饰词和一些重要的Swift概念。安全
在Swift语法中有不少@
符号,这些@
符号在Swift4以前的版本大可能是兼容OC的特性,Swift4及以后则出现愈来愈多搭配@
符号的新特性。以@
开头的修饰词,在官网中叫Attributes
,在SwiftGG的翻译中叫特性,我并无找到这一类被@
修饰的符号的统称,就暂且叫他们特性修饰词
吧,若是有清楚的小伙伴能够告知我。markdown
从Swift5的发布来看(@dynamicCallable
,@State
),以后将会有更多的特性修饰词出现,在他们出来以前,咱们有必要先了解下现有的一些特性修饰词以及它们的做用。闭包
参考:Swift Attributesapp
@available
: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面通常跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,表明着平台和语言,可选值有如下这几个:dom
iOS
iOSApplicationExtension
macOS
macOSApplicationExtension
watchOS
watchOSApplicationExtension
tvOS
tvOSApplicationExtension
swift
可使用*
指代支持全部这些平台。
有一个咱们经常使用的例子,当须要关闭ScrollView
的自动调整inset
功能时:
// 指定该方法仅在iOS11及以上的系统设置 if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } else { automaticallyAdjustsScrollViewInsets = false } 复制代码
还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台:
@available(iOS 12.0, *) func adjustDarkMode() { /* code */ } @available(iOS 12.0, *) struct DarkModeConfig { /* code */ } @available(iOS 12.0, *) protocol DarkModeTheme { /* code */ } 复制代码
版本和平台的限定能够写多个:
@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>? 复制代码
注意:做为条件语句的available
前面是#
,做为标记位时是@
刚才说了,available后面参数至少要有两个,后面的可选参数这些:
deprecated
:从指定平台标记为过时,能够指定版本号obsoleted=版本号
:从指定平台某个版本开始废弃(注意弃用的区别,deprecated
是还能够继续使用,只不过是不推荐了,obsoleted
是调用就会编译错误)该声明message=信息内容
:给出一些附加信息unavailable
:指定平台上是无效的renamed=新名字
:重命名声明咱们看几个例子,这个是Array里flatMap
的函数说明:
@available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value") public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] 复制代码
它的含义是针对swift语言,该方式在swift4.1版本以后标记为过时,对应该函数的新名字为compactMap(_:)
,若是咱们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(_:) for the case where closure returns an optional value
。
在Realm库里,有一个销毁NotificationToken
的方法,被标记为unavailable
:
extension RLMNotificationToken { @available(*, unavailable, renamed: "invalidate()") @nonobjc public func stop() { fatalError() } } 复制代码
标记为unavailable
就不会被编译器联想到。这个主要是为升级用户的迁移作准备,从可用stop()
的版本升上了,会红色报错,提示该方法不可用。由于有renamed
,编译器会推荐你用invalidate()
,点击fix
就直接切换了。因此这两个标记参数常一块儿出现。
带返回的函数若是没有处理返回值会被编译器警告⚠️。但有时咱们就是不须要返回值的,这个时候咱们可让编译器忽略警告,就是在方法名前用@discardableResult
声明一下。能够参考Alamofire中request
的写法:
@discardableResult public func request( _ url: URLConvertible, method: HTTPMethod = .get, parameters: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) -> DataRequest { return SessionManager.default.request( url, method: method, parameters: parameters, encoding: encoding, headers: headers ) } 复制代码
这个关键词是可内联的声明,它来源于C语言中的inline
。C中通常用于函数前,作内联函数,它的目的是防止当某一函数屡次调用形成函数栈溢出的状况。由于声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么作能够省去函数调用的时间。
内联函数常出如今系统库中,OC中的runtim中就有大量的inline
使用:
static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; } 复制代码
Swift中的@inlinable
和C中的inline
基本相同,它在标准库的定义中也普遍出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。
例如Swift对Array
中map
函数的定义:
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] 复制代码
其实Array中声明的大部分函数前面都加了@inlinable
,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。
须要注意内联声明不能用于标记为private
或者fileprivate
的地方。
这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,由于它省略了从标准库调用map
实现的步骤。但这个快也是有代价的,由于是编译时作替换,这增长了编译的开销,会相应的延长编译时间。
内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable
这个特性。
经过命名咱们能够推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不一样访问对象可能产生歧义的问题。
好比说,Swift 标准库中Array
和Sequence
均实现了min()
方法,而系统库中也定义了min(::)
,对于可能存在的二义性问题,咱们能够借助于@warn_unqualified_access
。
extension Array where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Element? } extension Sequence where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Self.Element? } 复制代码
这个特性声明会由编译器在可能存在二义性的场景中对咱们发出警告。这里有一个场景能够便于理解它的含义,咱们自定义一个求Array
中最小值的函数:
extension Array where Element: Comparable { func minValue() -> Element? { return min() } } 复制代码
咱们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning
。它告诉咱们编译器推断咱们当前使用的是Sequence中的min()
,这与咱们的想法是违背的。由于有这个@warn_unqualified_access
限定,咱们能及时的发现问题,并解决问题:self.min()
。
把这个特性用到任何能够在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。
用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。若是你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。若是一个类继承自另外一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。
@objc还有一个用处是当你想在OC的代码中暴露一个不一样的名字时,能够用这个特性,它能够用于类,函数,枚举,枚举成员,协议,getter,setter等。
// 当在OC代码中访问enabled的getter方法时,是经过isEnabled class ExampleClass: NSObject { @objc var enabled: Bool { @objc(isEnabled) get { // Return the appropriate value } } } 复制代码
这一特性还能够用于解决潜在的命名冲突问题,由于Swift有命名空间,经常不带前缀声明,而OC没有命名空间是须要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,能够选择一个带前缀的名字供OC代码使用。
Charts做为一个在OC和Swift中都很经常使用的图标库,是须要较好的同时兼容两种语言的使用的,因此也能够看到里面有大量经过@objc
标记对OC调用时的重命名代码:
@objc(ChartAnimator) open class Animator: NSObject { } @objc(ChartComponentBase) open class ComponentBase: NSObject { } 复制代码
由于Swift中定义的方法默认是不能被OC调用的,除非咱们手动添加@objc标识。但若是一个类的方法属性较多,这样会很麻烦,因而有了这样一个标识符@objcMembers
,它可让整个类的属性方法都隐式添加@objc
,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,固然对于OC不支持的类型,仍然没法被OC调用:
@objcMembers class MyClass : NSObject { func foo() { } // implicitly @objc func bar() -> (Int, Int) // not @objc, because tuple returns // aren't representable in Objective-C } extension MyClass { func baz() { } // implicitly @objc } class MySubClass : MyClass { func wibble() { } // implicitly @objc } extension MySubClass { func wobble() { } // implicitly @objc } 复制代码
参考:Swift三、4中的@objc、@objcMembers和dynamic
@testable
是用于测试模块访问主target的一个关键词。
由于测试模块和主工程是两个不一样的target,在swift中,每一个target表明着不一样的module,不一样module之间访问代码须要public和open级别的关键词支撑。可是主工程并非对外模块,为了测试修改访问权限是不该该的,因此有了@testable
关键词。使用以下:
import XCTest @testable import Project class ProjectTests: XCTestCase { /* code */ } 复制代码
这时测试模块就能够访问那些标记为internal或者public级别的类和成员了。
frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证以后不会作出改变。为何须要这么作以及这么作有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,以后会单独出一篇文章介绍,这里只介绍这两个字段的含义。
@frozen public enum ComparisonResult : Int { case orderedAscending = -1 case orderedSame = 0 case orderedDescending = 1 } @frozen public struct String {} extension AVPlayerItem { public enum Status : Int { case unknown = 0 case readyToPlay = 1 case failed = 2 } } 复制代码
ComparisonResult
这个枚举值被标记为@frozen
即便保证以后该枚举值不会再变。注意到String
做为结构体也被标记为@frozen
,意为String结构体的属性及属性顺序将再也不变化。其实咱们经常使用的类型像Int
、Float
、Array
、Dictionary
、Set
等都已被“冻结”。须要说明的是冻结仅针对struct
和enum
这种值类型,由于他们在编译器就肯定好了内存布局。对于class类型,不存在是否冻结的概念,能够想下为何。
对于没有标记为frozen的枚举AVPlayerItem.Status
,则认为该枚举值在以后的系统版本中可能变化。
对于可能变化的枚举,咱们在列出全部case的时候还须要加上对@unknown default
的判断,这一步会有编译器检查:
switch currentItem.status { case .readyToPlay: /* code */ case .failed: /* code */ case .unknown: /* code */ @unknown default: fatalError("not supported") } 复制代码
这几个是SwiftUI中出现的特性修饰词,由于我对SwiftUI的了解很少,这里就不作解释了。附一篇文章供你们了解。
[译]理解 SwiftUI 里的属性装饰器@State, @Binding, @ObservedObject, @EnvironmentObject
lazy是懒加载的关键词,当咱们仅须要在使用时进行初始化操做就能够选用该关键词。举个例子:
class Avatar { lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize) var largeImage: UIImage init(largeImage: UIImage) { self.largeImage = largeImage } } 复制代码
对于smallImage,咱们声明了lazy,若是咱们不去调用它是不会走后面的图片缩放计算的。可是若是没有lazy,由于是初始化方法,它会直接计算出smallImage的值。因此lazy很好的避免的没必要要的计算。
另外一个经常使用lazy的地方是对于UI属性的定义:
lazy var dayLabel: UILabel = { let label = UILabel() label.text = self.todayText() return label }() 复制代码
这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。
使用lazy你可能会发现它只能经过var初始而不能经过let,这是由 lazy
的具体实现细节决定的:它在没有值的状况下以某种方式被初始化,而后在被访问时改变本身的值,这就要求该属性是可变的。
另外咱们能够在Sequences中使用lazy,在讲解它以前咱们先看一个例子:
func increment(x: Int) -> Int { print("Computing next value of \(x)") return x+1 } let array = Array(0..<1000) let incArray = array.map(increment) print("Result:") print(incArray[0], incArray[4]) 复制代码
在执行print("Result:")
以前,Computing next value of ...
会被执行1000次,但实际上咱们只须要0和4这两个index对应的值。
上面说了序列也可使用lazy,使用的方式是:
let array = Array(0..<1000) let incArray = array.lazy.map(increment) print("Result:") print(incArray[0], incArray[4]) // Result: // 1 5 复制代码
在执行print("Result:")
以前,并不会打印任何东西,只打印了咱们用到的1和5。就是说这里的lazy能够延迟到咱们取值时才去计算map里的结果。
咱们看下这个lazy的定义:
@inlinable public var lazy: LazySequence<Array<Element>> { get } 复制代码
它返回一个LazySequence
的结构体,这个结构体里面包含了Array<Element>
,而map
的计算在LazySequence
里又从新定义了一下:
/// Returns a `LazyMapSequence` over this `Sequence`. The elements of /// the result are computed lazily, each time they are read, by /// calling `transform` function on a base element. @inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyMapSequence<Base, U> 复制代码
这里完成了lazy序列的实现。LazySequence
类型的lazy只能被用于map、flatMap、compactMap这样的高阶函数中。
参考: “懒”点儿好
纠错:参考文章中说:"这些类型(LazySequence)只能被用在 map
,flatMap
,filter
这样的高阶函数中" 实际上是没有filter的,由于filter是过滤函数,它须要完整遍历一遍序列才能完成过滤操做,是没法懒加载的,并且我查了LazySequence
的定义,确实是没有filter
函数的。
Swift开发过程当中咱们会常常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可使用unowned
和weak
这两个关键词。看下面两个例子:
class Dog { var name: String init (name: String ) { self.name = name } deinit { print("\(name) is deinitialized") } } class Bone { // weak 修饰词 weak var owner: Dog? init(owner: Dog?) { self.owner = owner } deinit { print("bone is deinitialized" ) } } var lucky: Dog? = Dog(name: "Lucky") var bone: Bone? = Bone(owner: lucky!) lucky = nil // Lucky is deinitialized 复制代码
这里Dog和Bone是相互引用的关系,若是没有weak var owner: Dog?
这里的weak声明,将不会打印Lucky is deinitialized
。还有一种解决循环应用的方式是把weak
替换为unowned
关键词。
unsafe_unretained
,它不会增长引用计数,即便它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil
。若是此时为无效引用,再去尝试访问它就会crash。这二者还有一个更经常使用的地方是在闭包里面:
lazy var someClosure: () -> Void = { [weak self] in // 被weak修饰后self为optional,这里是判断self非空的操做 guard let self = self else { retrun } self.doSomethings() } 复制代码
这里若是是unowned
修饰self的话,就不须要用guard作解包操做了。可是咱们不能为了省略解包的操做就用unowned
,也不能为了安全起见所有weak
,弄清楚二者的适用场景很是重要。
根据苹果的建议:
Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.
当闭包和它捕获的实例老是相互引用,而且老是同时释放时,即相同的生命周期,咱们应该用unowned,除此以外的场景就用weak。
KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又作了更普遍的扩展。
// KVC问题,支持struct、class struct User { let name: String var age: Int } var user1 = User() user1.name = "ferry" user1.age = 18 //使用KVC取值 let path: KeyPath = \User.name user1[keyPath: path] = "zhang" let name = user1[keyPath: path] print(name) //zhang // KVO的实现仍是仅限于继承自NSObject的类型 // playItem为AVPlayerItem对象 playItem.observe(\.status, changeHandler: { (_, change) in /* code */ }) 复制代码
这个KeyPath的定义是这样的:
public class AnyKeyPath : Hashable, _AppendKeyPath {} /// A partially type-erased key path, from a concrete root type to any /// resulting value type. public class PartialKeyPath<Root> : AnyKeyPath {} /// A key path from a specific root type to a specific resulting value type. public class KeyPath<Root, Value> : PartialKeyPath<Root> {} 复制代码
定义一个KeyPath
须要指定两个类型,根类型和对应的结果类型。对应上面示例中的path:
let path: KeyPath<User, String> = \User.name 复制代码
根类型就是User,结果类型就是String。也能够不指定,由于编译器能够从\User.name
推断出来。那为何叫根类型的?能够注意到KeyPath遵循一个协议_AppendKeyPath
,它里面定义了不少append
的方法,KeyPath是多层能够追加的,就是若是属性是自定义的Address类型,形如:
struct Address { var country: String = "" } let path: KeyPath<User, String> = \User.address.country 复制代码
这里根类型为User
,次级类型是Address
,结果类型是String
。因此path
的类型依然是KeyPath<User, String>
。
明白了这些咱们能够用KeyPath作一些扩展:
extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { return sorted { a, b in return a[keyPath: keyPath] < b[keyPath: keyPath] } } } // users is Array<User> let newUsers = users.sorted(by: \.age) 复制代码
这个自定义sorted
函数实现了经过传入keyPath进行升序排列功能。
参考:The power of key paths in Swift
some
是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,可是用 some
修饰后,编译器会让 protocol 的实例类型对外透明。
能够经过一个例子理解这段话的含义,当咱们尝试定义一个遵循Equatable
协议的value时:
// Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements var value: Equatable { return 1 } var value: Int { return 1 } 复制代码
编译器提示咱们Equatable
只能被用来作泛型的约束,它不是一个具体的类型,这里咱们须要使用一个遵循Equatable
的具体类型(Int)进行定义。但有时咱们并不想指定具体的类型,这时就能够在协议名前加上some
,让编译器本身去推断value的类型:
var value: some Equatable { return 1 } 复制代码
在SwiftUI里some随处可见:
struct ContentView: View { var body: some View { Text("Hello World") } } 复制代码
这里使用some
就是由于View
是一个协议,而不是具体类型。
当咱们尝试欺骗编译器,每次随机返回不一样的Equatable
类型:
var value: some Equatable { if Bool.random() { return 1 } else { return "1" } } 复制代码
聪明的编译器是会发现的,并警告咱们Function declares an opaque return type, but the return statements in its body do not have matching underlying types
。