上一節(jié),我們分析了閉包 & Runtime & Any等類型。
介紹Runtime運行時時,我們知道swift是靜態(tài)語言,但可兼容OC類實現(xiàn)objc_msgSend消息發(fā)送機制。
-
swift中dynamic聲明的函數(shù)可在extension中進行函數(shù)替換。不過這個替換在編譯期就完成了。
那,swift有沒有自己的運行時機制呢?
- 有,
Mirror反射機制。雖然沒有OC運行時那么強大。但支持運行時獲取對象的類型和成員變量(HandyJSON就是基于Mirror思想,直接從內(nèi)存讀取來實現(xiàn)的??)
本節(jié),我們先體驗一下Mirror,下一節(jié),我們深入研究Mirror底層原理
- Mirror體驗
- protocol協(xié)議
- Error錯誤機制(try、throws、rethrows)
- defer
- assert
1. Mirror體驗
-
Mirror(反射),可以動態(tài)獲取類型、成員變量,在運行時可以調(diào)用方法、屬性等行為的特性。
對于一個純swift類來說,并不支持我們直接像OCRuntime那樣操作。但Swift標準庫給我們提供了簡單實用的反射機制。
- 測試案例:
class HTPerson { var name = "ht" var age = 18 } let p = HTPerson() let mirror = Mirror(reflecting: p.self) for pro in mirror.children { print("\(pro.label ?? ""):\(pro.value)") }
- 打印結(jié)果:
image.png
- 進入
Mirror內(nèi)部可以看到:
Mirror是一個
struct結(jié)構(gòu)體
image.pngMirror 初始化時,入?yún)?code>Any類型
image.png
children屬性為(label: String?, value: Any)元組類型
image.png
-
Mirror的用途很多,最常用的是用于JSON解析:
class HTPerson {
var name = "ht"
var age = 18
var student = HTStudent() // 持有對象
}
class HTStudent {
var double = 10.0
}
func test(_ obj: Any) -> Any{
let mirror = Mirror(reflecting: obj)
// 遞歸終止條件
guard !mirror.children.isEmpty else { return obj }
var keyValue: [String: Any] = [:] // 記錄屬性名和屬性內(nèi)容
for children in mirror.children{
if let keyName = children.label {
keyValue[keyName] = test(children.value) // 遞歸(繼續(xù)遍歷person中的對象屬性)
}else{
print("children.label ")
}
}
return keyValue
}
let t = HTPerson()
print(test(t))
-
打印結(jié)果:
image.png
分析:
mirror成功讀取對象的屬性名和屬性值,可使用字典存儲和輸出;每個
屬性都會默認當作對象來處理,通過mirror.children可判斷當前對象是否擁有屬性,沒有就跳出當前屬性的遞歸流程。 這樣可保證所有讀取children的完整層級信息。(ps:
遞歸實際是利用棧特性,直接或間接調(diào)用自身實現(xiàn)層級讀取的需求,遞歸需要有明確的終止條件)
- 上面代碼有個
不好的點:錯誤結(jié)果是print打印,外界并不知道。
在swift中,錯誤信息一般使用Error進行表示。
2. Protocol協(xié)議
在介紹
Error之前,我們先介紹一下Swift的協(xié)議。
Swift的協(xié)議非常強大,不僅可以聲明屬性、函數(shù),支持可選實現(xiàn),還可以在extension中完成屬性和函數(shù)的默認實現(xiàn)。以上面案例為例:
如果每次
讀取屬性,我們都需要調(diào)用test函數(shù),會顯得非常麻煩。
協(xié)議可以幫我們默認實現(xiàn)這個方法,只要對象遵守這個協(xié)議,都可以直接使用。
【思路】
- 創(chuàng)建一個協(xié)議(
CustomJSONMap),聲明jsonMap函數(shù),并在extension中默認實現(xiàn)jsonMap。- 創(chuàng)建一個枚舉
JSONMapError,對所有錯誤類型進行列舉。jsonMap內(nèi)部支持Mirror所有遵循CustomJSONMap協(xié)議的屬性。對未遵循協(xié)議的屬性和屬性名為空的屬性返回相應(yīng)的錯誤類型。
// 錯誤枚舉
enum JSONMapError {
case emptyError // 空
case notConformProtocol // 未遵守協(xié)議
}
// 協(xié)議(自帶jsonMap函數(shù))
protocol CustomJSONMap {
func jsonMap() ->Any
}
// extension中默認jsonMap實現(xiàn)
extension CustomJSONMap {
func jsonMap() -> Any {
let mirror = Mirror(reflecting: self)
guard !mirror.children.isEmpty else { return self }
var keyValue: [String: Any] = [:]
for children in mirror.children{
if let value = children.value as? CustomJSONMap {
if let keyName = children.label {
keyValue[keyName] = value.jsonMap() // 遞歸
} else {
return JSONMapError.emptyError // 屬性名為空
}
} else {
return JSONMapError.notConformProtocol // children未遵守CustomJSONMap協(xié)議
}
}
return keyValue
}
}
class HTPerson: CustomJSONMap {
var name = "ht"
var age = 18
}
let t = HTPerson()
print(t.jsonMap())
-
運行上述案例,可以看到打印了
notConformProtocol
因為name和age屬性分別是String和Int類型,他們沒有遵守CustomJSONMap協(xié)議。
image.png 既然如此,我們讓
String和Int都遵守CustomJSONMap協(xié)議,再運行。
extension String: CustomJSONMap {}
extension Int: CustomJSONMap {}

-
完美打印所有內(nèi)容
在我們的開發(fā)中,
protocol協(xié)議是一大利器,使用非常方便。
- 遇到一些
通用的函數(shù),我們可以使用協(xié)議包裝起來,只要遵循這個協(xié)議,就可以直接調(diào)用相應(yīng)的屬性和函數(shù)。很好的隔離代碼,而且讓代碼看起來非常簡潔。
- 上面的
錯誤信息,我們只是使用枚舉統(tǒng)一羅列,return時,常規(guī)結(jié)果和錯誤類型混在一起返回,讓我們很不舒服。
如何解決?
- 可以通過系統(tǒng)
Error協(xié)議搭配throw關(guān)鍵字,優(yōu)雅的拋出錯誤或返回常規(guī)結(jié)果,讓開發(fā)者自己選擇處理方式。
3. Error機制(try、throws、rethrows)
swift中,系統(tǒng)提供Error協(xié)議來表示當前應(yīng)用程序發(fā)生錯誤的情況,并支持使用throw關(guān)鍵字,優(yōu)雅的拋出錯誤。
3.1 基礎(chǔ)Error協(xié)議
public protocol Error { }
- 系統(tǒng)的
Error協(xié)議,就是一個空的公共協(xié)議, 不管是class、struct還是enum,都可以通過遵守這個協(xié)議,來表達錯誤。
以 enum為例:
// 錯誤枚舉,遵循Error
enum JSONMapError: Error {
case emptyError // 空
case notConformProtocol // 未遵守協(xié)議
}
// 協(xié)議(自帶jsonMap函數(shù))
protocol CustomJSONMap {
func jsonMap() throws ->Any
}
// extension中默認jsonMap實現(xiàn)
extension CustomJSONMap {
func jsonMap() throws -> Any {
let mirror = Mirror(reflecting: self)
guard !mirror.children.isEmpty else { return self }
var keyValue: [String: Any] = [:]
for children in mirror.children{
if let value = children.value as? CustomJSONMap {
if let keyName = children.label {
keyValue[keyName] = try value.jsonMap() // 加了throws后,需要使用try執(zhí)行 jsonmap()
} else {
throw JSONMapError.emptyError // 屬性名為空
}
} else {
throw JSONMapError.notConformProtocol // children未遵守CustomJSONMap協(xié)議
}
}
return keyValue
}
}
class HTPerson: CustomJSONMap {
var name = "ht" // String未繼承CustomJSONMap,會error
var age = 18 // Int未繼承CustomJSONMap,會error
}
let t = HTPerson()
// do...catch 分開處理,catch中有默認參數(shù)error。
do {
// 處理正常結(jié)果
print(try t.jsonMap())
} catch {
// 處理錯誤類型(其中error為Error類型)
print(error)
}
//print(try t.jsonMap()) // try : 向上甩鍋,將錯誤拋給上層函數(shù)處理(上層沒處理,就crash)
print(try? t.jsonMap()) // try?: 只關(guān)注正常結(jié)果,忽略錯誤(如果錯誤,就為nil, 不執(zhí)行后續(xù)流程)
//print(try! t.jsonMap()) // try!: 對代碼絕對自信,一定不會發(fā)生錯誤(如果發(fā)生,直接crash)
分析:
- 讓
JSONMapError枚舉遵守Error協(xié)議- 將
jsonMap函數(shù)的return 錯誤,全部改為throw拋出錯誤,只有正常結(jié)果才使用return返回。- 使用
throw拋出錯誤會發(fā)現(xiàn),函數(shù)需要使用throws標注,這是告訴使用者,這個函數(shù)可能拋出error,需要開發(fā)者決定是否處理。- 使用
throw后,發(fā)現(xiàn)不能直接調(diào)用jsonMap,需要使用try關(guān)鍵字來嘗試調(diào)用。
- 那
try什么意思呢?如何使用呢?
try有三種使用方式:
- try : 向上甩鍋,將
錯誤拋給上層函數(shù)處理(最上層都沒處理,就crash)- try?: 只關(guān)注
正常結(jié)果,忽略所有錯誤(如果錯誤,就為nil, 不執(zhí)行后續(xù)流程)- try!: 程序員對代碼
絕對自信,一定不會發(fā)生錯誤(如果發(fā)生,直接crash)
- 上面三種方式,只是說到了
拋出和不管,那如何正確處理throw拋出的錯誤呢?
do...catch:
- 我們可以通過
do...catch來處理。其中do處理正確結(jié)果,catch處理error,catch有個隱藏參數(shù),就是error(Error類型)do { // 處理正常結(jié)果 try t.jsonMap() // 這里try不會報錯了,因為錯誤都分給了catch } catch { // 處理錯誤類型(其中error為Error類型) print(error) }
-
上面案例中,HTPerosn內(nèi)的
name和age的類型都未遵循``CustomJSONMap協(xié)議,所以會拋出error
image.png 當前使用
throw,成功將錯誤和正常結(jié)果進行分離,并了解如何通過try和do-catch對結(jié)果進行處理。
3.2 LocalizedError
在
正常業(yè)務(wù)開發(fā)中,我們有時候并不滿足于知道錯誤類型,我還希望得到關(guān)于這個錯誤的描述,以及其他信息。
(比如網(wǎng)絡(luò)錯誤,我們希望獲取error能記錄errCode、errMsg等信息)系統(tǒng)基于
Error協(xié)議,再公開了一個LocalizedError協(xié)議。
public protocol LocalizedError : Error {
/// 錯誤的描述
var errorDescription: String? { get }
/// 錯誤的原因
var failureReason: String? { get }
/// 恢復(fù)的建議
var recoverySuggestion: String? { get }
/// 可提供的幫助
var helpAnchor: String? { get }
}
-
所以我們可使用
LocalizedError,把錯誤的信息表達得更詳細一些。
(枚舉類型的錯誤,可以使用switch每個屬性都獨立給error錯誤描述)
image.png 我們知道
LocalizedError就是遵守Error的一個協(xié)議。在業(yè)務(wù)開發(fā)中,我們完全可仿照LocalizedError的思維,直接自定義一個協(xié)議遵守Error協(xié)議。然后自己寫一些屬性、函數(shù)等。
3.3 CustomError (繼承自Error)
- OC中,系統(tǒng)提供了
NSError錯誤類,NSError遵守Error協(xié)議,可以攜帶更多錯誤信息。
open class NSError : NSObject, NSCopying, NSSecureCoding {
public init(domain: String, code: Int, userInfo dict: [String : Any]? = nil)
open var domain: String { get }
open var code: Int { get }
open var userInfo: [String : Any] { get }
open var localizedDescription: String { get }
open var localizedFailureReason: String? { get }
open var localizedRecoverySuggestion: String? { get }
open var recoveryAttempter: Any? { get }
open var helpAnchor: String? { get }
}
// NSError遵守Error協(xié)議
extension NSError : Error { }
- 在我們
swift中,對接OCNSError的是CustomNSError。結(jié)構(gòu)都一樣。
public protocol CustomNSError : Error {
static var errorDomain: String { get }
var errorCode: Int { get }
var errorUserInfo: [String : Any] { get }
}
- 其實
了解到這,我相信你,只要基于Error協(xié)議,你想要的,都可自定義協(xié)議來實現(xiàn)。
3.4 rethorws
當我們把函數(shù)作為另一個函數(shù)入?yún)?/code>時,如果入?yún)⒑瘮?shù)包含throws聲明,不處理會報錯:

-
【處理方式一】在
函數(shù)內(nèi)處理錯誤情況,外部可直接調(diào)用函數(shù)
image.png -
【處理方式二】如果
不想在函數(shù)內(nèi)處理錯誤情況,可以加上rethorws聲明,由外部處理。
image.png
rethrows適用于鏈式調(diào)用時,函數(shù)內(nèi)部不需要關(guān)心異常情況,最終再統(tǒng)一處理異常情況。
4. defer(延后處理)
-
defer:用來定義以任何方式(throw、return)離開代碼塊前必須執(zhí)行的代碼:
image.png -
多個defer時,執(zhí)行順序是反序執(zhí)行(與創(chuàng)建順序相反)。
image.png
5. assert(斷言)
- 很多
編程語言都有斷言機制,不符合指定條件就拋出運行時錯誤,常用于調(diào)試(Debug)階段的條件判斷

- 默認情況下,
Swift斷言只會在debug模式下生效,release模式下忽略:
image.png
本節(jié)內(nèi)容較多,涉及到Mirror的深層探索,放到下一節(jié):Mirror源碼探索












