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的翻譯中叫特性,我並無找到這一類被@
修飾的符號的統稱,就暫且叫他們特性修飾詞
吧,若是有清楚的小夥伴能夠告知我。閉包
從Swift5的發佈來看(@dynamicCallable
,@State
),以後將會有更多的特性修飾詞出現,在他們出來以前,咱們有必要先了解下現有的一些特性修飾詞以及它們的做用。app
參考:Swift Attributesdom
@available
: 可用來標識計算屬性、函數、類、協議、結構體、枚舉等類型的生命週期。(依賴於特定的平臺版本 或 Swift 版本)。它的後面通常跟至少兩個參數,參數之間以逗號隔開。其中第一個參數是固定的,表明着平臺和語言,可選值有如下這幾個:函數
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
。