Swift 輕松掌握喵神高階心法 VII

quan452 8年前發布 | 12K 次閱讀 Swift Apple Swift開發

今天鵝廠的同學推薦新研發的iOS UI框架, 果然非同凡響, 粗略估計能夠覆蓋到85%以上的需求, 各方面的組件化及布局流等應有盡有, 可是自定義轉場方面的還沒有看到(也可能是眼拙), 不過是Objective-C的, 可能是最近Javascript和Swift敲多了, Objective-C的語法不免有種古老和陳舊的感覺(各位iOS的同學輕噴啊~) 

Tip29 屬性觀察

屬性觀察(Property Observers)是Swift中一個很特殊的特性, 利用屬性觀察我們可以在當前類型內監視對于屬性的設定, 并作出一些響應. Swift中為我們提供了兩個屬性觀察的方法, 它們分別是willSet和didSet.

使用這兩個方法十分簡單, 我們只要在屬性聲明的時候添加相應的代碼塊, 就可以對將要設定的值和已經設置的值進行監聽了:

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

在willSet和didSet中我們分別可以使用newValue和oldValue來獲取將要設定的和已經設定的值. 屬性觀察的一個重要用處是作為設置值的驗證, 比如上面的例子中我們不希望date超過一年以上的話, 我們可以將didSet修改一下

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()
    }
}

更改一下調用, 我們就能看到效果:

// 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

初始化方法對屬性的設定, 以及在willSet和didSet中對屬性的再次設定都不會再次觸發屬性觀察的調用, 一般來說這會是你所需要的行為, 可以放心使用.

我們知道, 在Swift中所聲明的屬性包括存儲屬性和計算屬性兩種. 其中存儲屬性將會在內存中實際分配地址對屬性進行存儲, 而計算屬性則不保羅背后的存儲, 只是提供set和get兩種方法. 在同一個類型中, 屬性觀察和計算屬性是不能同時共存的. 也就是說, 想在一個屬性定義中同時出現set和willSet或didSet是一件辦不到的事情. 計算屬性中我們可以通過改寫set中的內容來達到和willSet及didSet同樣的屬性觀察的目的. 如果我們無法改動這個類, 又想要通過屬性觀察做一些事情的話, 可能就需要子類化這個類, 并且重寫它的屬性了. 重寫的屬性并不知道父類屬性的具體實現情況, 而只從父類屬性中繼承名字和類型, 因此在子類的重載屬性中我們是可以對父類的屬性任意地添加屬性觀察的, 而不用在意父類中到底是存儲屬性還是計算屬性:

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

set和對應的屬性觀察的調用都在我們的預想之中. 這里要處以的是get首先被調用了一次. 這是因為我們實現了didSet, didSet中會用到oldValue, 而這個值需要在整個set動作之前進行獲取并存儲代用, 否則將無法確保正確性. 如果我們不實現didSet的話, 這次get操作也將不存在.

驗證結果:

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

得到結論: didSet中獲取oldValue的方法會主動調用get方法.

Tip30 final

final關鍵字可以用在class. func或者var前面進行修飾, 表示不允許對該內容進行繼承或者重寫操作. 這個關鍵字的作用和C#中的sealed相同, 而sealed其實在C#算是一個飽受爭議的關鍵字. 有一派程序員認為, 類似這樣的禁止繼承和重寫的做法是非常有益的, 它可以更好地對代碼進行版本控制, 得到更佳的性能, 以及使代碼更安全. 因此他們甚至認為語言應當是默認不允許繼承的, 只有在顯式地指明可以繼承的時候才能子類化.

在這里我不打算對這樣的想法做出判斷或者評價, 雖然上面列舉的優點都是事實, 但是另一個事實是不論是Apple或者微軟, 以及世界上很多其他語言都沒有做出默認不讓繼承和重寫的決定. 帶著"這不是一個可以濫用的特性"的觀點, 我們來看看在寫Swift的時候可能會在什么情況下使用final.

權限控制

給一段代碼加上final就意味著編譯器向你做出保證, 這段代碼不會再被修改; 同時, 這也意味著你認為這段代碼已經完備并且沒有再被進行繼承或重寫的必要, 因此這往往會是一個需要深思熟慮的決定. 在Cocoa開發中app開發是一塊很大的內容 對于大多數我們自己完成的面向app開發代碼, 其實不太會提供給別人使用, 這種情況下即時是將所有自己寫的代碼標記為final都是一件無可厚非的事情(但我并不是在鼓勵這么做) --因為在需要的任何時候你都可以將這個管家你去掉以恢復其可繼承性. 而在開發給其他開發者使用的庫時, 就必須更深入考慮各種使用場景和需求了.

一般來說, 不希望被繼承和重寫會有這幾種情況:

類或者方法的功能缺失已經完備了

對于很多的輔助性質的工具類或者方法, 可能我們會考慮加上final. 這樣的類又一個比較大的特點, 是很可能只包含類方法而沒有實例方法. 比如我們很難想到一種情況需要繼承或重寫一個負責計算一段字符串的MD5或者AES加密解密的工具類. 這種工具類和方法的算法是經過完備驗證和固定的, 使用者只需要調用, 而相對來說不可能有繼承和重寫的需求.

這種情況很多時候遵循的是以往經驗和主觀判斷, 而單個的開發者的判斷其實往往并不可靠. 遇到希望把某個自己開發的類或者方法標為final的時候, 去找一個富有經驗的開發者, 問問他們的意見或者看法, 應該是一個比較靠譜的做法.

子類繼承和修改是一件危險的事情

在子類繼承或重寫某些方法后可能做一些破壞性的事情, 導致子類或者父類部分也武大正常工作的情況. 舉個例子, 在某個公司管理的系統中我們對員工按照一定規則進行編號, 這樣通過編號我們能迅速找到任一員工. 而假如我們在子類中重寫了這個編號方法, 很可能就導致基類中的依賴員工編號的方法失效. 在這類情況下, 將編號方法標記為final以確保穩定, 可能是一種更好的做法.

為了父類中某些代碼一定會被執行

有時候父類中有一些關鍵代碼是在被繼承重寫后必須執行的(比如狀態配置, 認證等等), 否則將導致運行時候的錯誤. 而在一般的方法中, 如果子類重寫了父類方法, 是沒有辦法強制子類方法一定去調用相同的父類方法的. 在Objective-C的時候我們可以通過指定 attribute ((objc_requires_super)) 這樣的屬性來讓編譯器在子類沒有調用父類方法時拋出警告. 在Swift中對原來的很多attribute的支持現在還缺失中, 為了達到類似的目的, 我們可以使用一個final的方法, 在其中進行一些必要的配置, 然后再調用某個需要子類實現的方法, 以確保正常運行:

class Parent {

    final func method() {
        print("開始配置")
        // ..必要的代碼

        methodImpl()
        // ..必要的代碼
        print("結束配置")
    }

    func methodImpl() {
        fatalError("子類必須實現這個方法")
        // 或者也可以給出默認實現
    }
}

class Child: Parent {
    override func methodImpl() {
        // ..子類的業務邏輯
    }
}

這樣, 無論如何我們如何使用method, 都可以保證需要的代碼一定會被運行過, 而同時又給了子類繼承和重寫自定義具體實現的機會.

性能考慮

使用 final 的另一個重要理由是可能帶來的性能改善. 因為編譯器能夠從final中獲取額外信息, 因此可以對類或者方法調用進行額外的優化處理. 但是這個又是在實際表現中可能帶來的好處其實就算與Objective-C的動態派發相比也十分有限, 因此在項目還有其他方面可以優化(一般來說會是算法或者圖形相關的內容導致性能瓶頸)的情況下, 并不建議使用將類或者方法轉為final的方式來追求性能的提升.

Tip31 lazy 修飾符和 lazy 方法

延時加載或者是延時初始化是很常用的優化方法, 在構建和生成新的對象的時候, 內存分配會在運行時耗費不少時間, 如果有一些對象的屬性和內容非常復雜的話, 這個時間更是不可忽略. 另外, 有些情況下, 我們并不會立即用到一個對象的所有屬性, 而默認情況下初始化時, 那些在特定環境下不被使用的存儲屬性, 也一樣要被初始化和賦值, 也是一種浪費.

在其他語言(包括Objective-C)中延時加載的情況是很常見的. 我們在第一次訪問某個屬性時, 判斷這個屬性背后的存儲是否已經存在, 如果存在則直接返回, 如果不存在則說明是首次訪問, 那么就進行初始化并存儲后再返回. 這樣我們可以把這個屬性的初始化時刻推遲, 與包含它的對象的初始化時刻分開, 以達到提升性能的目的. 以Objective-C舉個例子(雖然這里既沒有費時操作, 也不會因為使用延時加載而造成什么性能影響, 但是作為一個最簡單的例子, 可以很好地說明問題):

ClassA.h
 @property (nonatimic, copy) NSString * testString;

 ClassA.m
 - (NSString *)testString {
    if (!_testString) {
        _testString = @"Hello";
        NSLog(@"只在首次訪問輸出");
    }
    return _testString;
 }

在初始化ClassA 對象后, _testString是nil. 只有當首次訪問testString屬性時getter方法會被調用, 并檢查如果還沒有初始化的話, 就進行賦值. 為了方便確認, 我們還在賦值時打印一句log. 我們之后再多次訪問這個屬性的話, 因為_testString已經有值, 因此將直接返回.

class ClassA {
    lazy var str: String = {
        let str = "Hello"
        print("只在首次訪問輸出")
        return str
    }()
}

我們在使用lazy作為屬性修飾符時, 只能聲明屬性是變量. 另外我們需要顯式地指定屬性類型, 并使用一個可以對這個屬性進行賦值的語句來在首次訪問屬性時運行. 如果我們多次訪問這個實例的str屬性的話, 可以看到只有一次輸出.

為了簡化, 我們如果不需要做什么額外工作的話, 也可以對這個lazy的屬性直接寫賦值語句:

lazy var str: String = "Hello"

相比起在Objective-C中實現方法, 現在的lazy使用起來要方便的多.

另外一個不太引起注意的是, 在Swift的標準庫中, 我們還有一組lazy方法, 它們的定義是這樣的:

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>

這些方法可以配合像map或者filter這類接受閉包并進行運行的方法一起, 讓整個行為變成延時進行的. 在某些情況下這么做也對性能會有不小的幫助. 例如, 直接使用map時:

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
 //操作完畢

而如果我們先進行一次lazy操作的話, 我們就能得到延時運行版本的容器:

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
 //操作完畢

對于那些不需要完全運行, 可能提前退出的情況, 使用lazy來進行性能優化效果會非常有效.

Tip32 Reflection 和 Mirror

熟悉Java的讀者可能會知道反射(Reflection). 這是一種在運行時檢測, 訪問或者修改類型的行為的特性. 一般的靜態語言類型的結構和方法的調用等都需要在編譯時決定, 開發者能做的很多時候只是使用控制流(比如if 或者 switch)來決定作出怎樣的額設置或是調用哪個方法. 而反射特性可以讓我們有機會在運行的時候通過某些條件實時地決定調用的方法, 或者甚至向某個類型動態地設置甚至加入屬性及方法, 是一種非常靈活和強大的語言特性.

Objective-C中我們不太會經常提及到"反射"這樣的詞語, 因為Objective-C的運行時比一般的反射還要靈活和強大. 可能很多讀者已經習以為常的像是通過字符串生成類或者selector, 并且進而生成對象或者調用方法等, 其實都是反射的具體的表現. 而在Swift中其實就算拋開Objective-C的運行時的部分, 在純Swift范疇內也存在有反射相關的一些內容, 只不過相對來說功能要弱的多.

因為這部分內容并沒有公開的文檔說明, 所以隨時可能發生變動, 或者甚至存在今后被從Swift的可調用標準庫中去掉的可能(Apple已經干過這種事情, 最早的時候Swift中甚至有隱式的類型轉換 __conversion, 但因為太過危險, 而被徹底去除了. 現在隱式轉換必須使用字面量轉換的方式進行了). 在實際的項目中, 也不建議使用這種沒有文檔說明的API, 不過有時候如果能稍微知道Swift中也存在這樣的可能性的話, 也許會有幫助(也指不定哪天Apple就扔出一個完整版的反射功能呢).

Swift中所有的類型都實現了 _Relectable, 這是一個內部協議, 我們可以通過 _reflect來獲取任意對象的一個鏡像, 這個鏡像對象包含類型的基本信息, 在Swift2.0之前, 這是對某個類型的對象進行探索的一種方法. 在Swift2.0中, 這些方法已經從公開的標準庫中移除了, 取而代之, 我們可以使用Mirror類型來做類似的事情:

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

通過Mirror初始化得到的結果中包含的元素的描述都被結合在children屬性下, 如果你有心可以到Swift標準庫中查找它的定義, 它實際上是一個Child的集合, 而Child則是一對鍵值的多元組:

public typealias Child = (label: String?, value: Any)
    public typealias Children = AnyCollection<Mirror.Type.Child>

AnyForwardCollection是遵守CollectionType協議的, 因此我們可以簡單地使用count來獲取元素的個數, 而對于具體的 代表屬性的多元組, 則使用下標進行訪問. 在對于我們的例子中, 每個Child都是具有兩個元素的多元組, 其中第一個是屬性名, 第二個是這個屬性所儲存的值. 需要特別注意的是, 這個值有可能是多個元素組成嵌套的形式(例如屬性值是數組或者字典的話, 就是這樣的形式).

如果覺得一個個打印太過于麻煩, 我們也可以簡單地使用dump黨閥來通過獲取一個對象的鏡像并進行標準輸出的方式將其輸出出來. 比如對上面的對象xiaoMing:

dump(xiaoMing)
//輸出
//? Person
//  - name: "XiaoMing"
//  - age: 16

在這里因為篇幅有限, 而且這部分內容很可能隨著版本而改變, 我們就不再一一介紹Mirror的更詳細的內容了. 有興趣的讀者不妨打開Swift的定義文件并找到這個協議, 里面對每個屬性和方法有非常詳細的注釋.

對于一個從對象反射出來的Mirror, 它所包含的信息是完備的. 也就是說我們可以在運行時通過Mirror的手段了解一個Swift類型(當然NSObject類也可以)的實例的屬性信息. 該特性最容易想到的應用的特性就是為任意model對象生成對應的JSON描述. 我們可以對等待處理的對象的Mirror值進行深度優先的訪問, 并按照屬性的valueType將它們歸類對應到不同的格式化中.

另一個常見的應用場景是類似對Swift類型的對象做像Objective-C中KVC那樣的valueForKey: 的取值. 通過比較取到的屬性的名字和我們想要取得的key值就行了, 非常簡單:

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

在現在的版本中, Swift的反射特性并不是非常強大, 我們只能對屬性進行讀取, 還不能對其設定, 不過我們有希望能在將來的版本中獲得更為強大的反射特性. 另外需要特別注意的是, 雖然理論上將反射特性應用在實際app制作中是可行的, 但是這一套機制設計的最初目的是用于REPL環境和Playground中進行輸出的. 所以我們最好遵守Apple的這一設定, 只在REPL和Playground中用它來對一個對象進行深層次的探索, 而避免將它用在app制作中 --因為你永遠不知道什么時候它們就會失效或者被大幅改動.

 

來自:http://www.jianshu.com/p/e80789c3e097

 

 本文由用戶 quan452 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!