Swift進階黃金之路(二)

Swift進階黃金之路(一)

上期遺留一個問題:爲什麼 rethrows 一般用在參數中含有可以 throws 的方法的高階函數中。

我們可以結合Swift的官方文檔對rethrows再做一遍回顧:

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.

返回rethrows的函數要求至少有一個可拋出異常的函數式參數,而有以函數作爲參數的函數就叫做高階函數。

這期分兩方面介紹Swift:特性修飾詞和一些重要的Swift概念。

特性修飾詞

在Swift語法中有很多@符號,這些@符號在Swift4之前的版本大多是兼容OC的特性,Swift4及之後則出現越來越多搭配@符號的新特性。以@開頭的修飾詞,在官網中叫Attributes,在SwiftGG的翻譯中叫特性,我並沒有找到這一類被@修飾的符號的統稱,就暫且叫他們特性修飾詞吧,如果有清楚的小夥伴可以告知我。

從Swift5的發佈來看(@dynamicCallable,@State),之後將會有更多的特性修飾詞出現,在他們出來之前,我們有必要先了解下現有的一些特性修飾詞以及它們的作用。

參考:Swift Attributes

@available

@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

帶返回的函數如果沒有處理返回值會被編譯器警告⚠️。但有時我們就是不需要返回值的,這個時候我們可以讓編譯器忽略警告,就是在方法名前用@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
    )
}
複製代碼

@inlinable

這個關鍵詞是可內聯的聲明,它來源於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對Arraymap函數的定義:

@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
複製代碼

其實Array中聲明的大部分函數前面都加了@inlinable,當應用某一處調用該方法時,編譯器會將調用處用具體實現代碼替換。

需要注意內聯聲明不能用於標記爲private或者fileprivate的地方。

這很好理解,對私有方法的內聯是沒有意義的。內聯的好處是運行時更快,因爲它省略了從標準庫調用map實現的步驟。但這個快也是有代價的,因爲是編譯時做替換,這增加了編譯的開銷,會相應的延長編譯時間。

內聯更多的是用於系統庫的特性,目前我瞭解的Swift三方庫中僅有CocoaLumberjack使用了@inlinable這個特性。

@warn_unqualified_access

通過命名我們可以推斷出其大概含義:對“不合規”的訪問進行警告。這是爲了解決對於相同名稱的函數,不同訪問對象可能產生歧義的問題。

比如說,Swift 標準庫中ArraySequence均實現了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()

@objc

把這個特性用到任何可以在 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 { }
複製代碼

@objcMembers

因爲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
}
複製代碼

參考:Swift3、4中的@objc、@objcMembers和dynamic

@testable

@testable是用於測試模塊訪問主target的一個關鍵詞。

因爲測試模塊和主工程是兩個不同的target,在swift中,每個target代表着不同的module,不同module之間訪問代碼需要public和open級別的關鍵詞支撐。但是主工程並不是對外模塊,爲了測試修改訪問權限是不應該的,所以有了@testable關鍵詞。使用如下:

import XCTest
@testable import Project

class ProjectTests: XCTestCase {
  /* code */
}
複製代碼

這時測試模塊就可以訪問那些標記爲internal或者public級別的類和成員了。

@frozen 和@unknown default

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結構體的屬性及屬性順序將不再變化。其實我們常用的類型像IntFloatArrayDictionarySet等都已被“凍結”。需要說明的是凍結僅針對structenum這種值類型,因爲他們在編譯器就確定好了內存佈局。對於class類型,不存在是否凍結的概念,可以想下爲什麼。

對於沒有標記爲frozen的枚舉AVPlayerItem.Status,則認爲該枚舉值在之後的系統版本中可能變化。

對於可能變化的枚舉,我們在列出所有case的時候還需要加上對@unknown default的判斷,這一步會有編譯器檢查:

switch currentItem.status {
    case .readyToPlay:
        /* code */
    case .failed:
        /* code */
    case .unknown:
        /* code */
    @unknown default:
        fatalError("not supported")
}
複製代碼

@State、@Binding、@ObservedObject、@EnvironmentObject

這幾個是SwiftUI中出現的特性修飾詞,因爲我對SwiftUI的瞭解不多,這裏就不做解釋了。附一篇文章供大家瞭解。

[譯]理解 SwiftUI 裏的屬性裝飾器@State, @Binding, @ObservedObject, @EnvironmentObject

幾個重要關鍵詞

lazy

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)只能被用在 mapflatMapfilter這樣的高階函數中" 其實是沒有filter的,因爲filter是過濾函數,它需要完整遍歷一遍序列才能完成過濾操作,是無法懶加載的,而且我查了LazySequence的定義,確實是沒有filter函數的。

unowned weak

Swift開發過程中我們會經常跟閉包打交道,而用到閉包就不可避免的遇到循環引用問題。在Swift處理循環引用可以使用unownedweak這兩個關鍵詞。看下面兩個例子:

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關鍵詞。

  • weak相當於oc裏面的weak,弱引用,不會增加循環計數。主體對象釋放時被weak修飾的屬性也會被釋放,所以weak修飾對象就是optional。
  • unowned相當於oc裏面的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。

參考:內存管理,WEAK 和 UNOWNED

Unowned 還是 Weak?生命週期和性能對比

KeyPath

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

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

參考:SwiftUI 的一些初步探索 (一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章