Swift Mirror & Error

前言

上篇文章Swift 內(nèi)存管理 & Runtime講解了Runtime的一個應(yīng)用場景:Swift類可繼承NSObject,配合使用@objc修飾符击困,讓OC端的Runtime API可調(diào)用Swift類的方法與屬性涎劈,同時使用Dynamic修飾符广凸,也可實現(xiàn)方法交換,只不過這個交換是在編譯期就確定的脸哀。

那么問題來了撞蜂,既然Swift是一門靜態(tài)語言蝌诡,那到底它有沒有類似OC這樣的Runtime運行時機制呢浦旱?當然有,就是本篇文章即將介紹的Mirror反射甥捺,雖然沒有OC的Runtime強大镰禾,但是也能在運行時獲取對象的類型成員變量羡微,其中第三方庫HandyJSON就是基于Mirror的機制來實現(xiàn)的。

一盯蝴、Mirror反射

什么是反射捧挺??? 可以動態(tài)獲取類型翅睛、成員信息捕发,在運行時可以調(diào)用方法扎酷、屬性等行為的特性。

老規(guī)矩凡纳,還是先上示例代碼??

class LGTeacher {
    var age: Int = 18
    var name: String = "Luoji"
}
let mirror = Mirror(reflecting: LGTeacher().self)
for pro in mirror.children{
    print("\(pro.label ?? ""): \(pro.value)")
}


上述代碼是Mirror的一個簡單的應(yīng)用場景 ?? 通過reflecting初始化,接著通過.children讀取屬性名與值狞尔。

1.1 Mirror定義

接下來我們看看Mirror相關(guān)文檔的API的定義??

  • init(reflecting: Any)

傳入的類型是Any偏序。

  • .children

    接著查看代碼??
    /// A collection of `Child` elements describing the structure of the
    /// reflected subject.
    public let children: Mirror.Children

繼續(xù)查看Children??

    /// The type used to represent substructure.
    ///
    /// When working with a mirror that reflects a bidirectional or random access
    /// collection, you may find it useful to "upgrade" instances of this type
    /// to `AnyBidirectionalCollection` or `AnyRandomAccessCollection`. For
    /// example, to display the last twenty children of a mirror if they can be
    /// accessed efficiently, you write the following code:
    ///
    ///     if let b = AnyBidirectionalCollection(someMirror.children) {
    ///         for element in b.suffix(20) {
    ///             print(element)
    ///         }
    ///     }
    public typealias Children = AnyCollection<Mirror.Child>

最后看Child??

    /// An element of the reflected instance's structure.
    ///
    /// When the `label` component in not `nil`, it may represent the name of a
    /// stored property or an active `enum` case. If you pass strings to the
    /// `descendant(_:_:)` method, labels are used for lookup.
    public typealias Child = (label: String?, value: Any)

所以端朵,回到示例代碼冲呢,通過print("\(pro.label ?? ""): \(pro.value)")打印的就是keyvalue敬拓。

1.2 JSON解析

既然Mirror可以解析出類的屬性keyvalue厕诡,那它能做什么灵嫌? ?? 第一時間聯(lián)想到的就是JSON解析寿羞。還是先看以下解析的實例代碼??

class LGPerson {
    var name = "Luoji"
    var age = 18
    var student = LGStudent() // 持有對象
}

class LGStudent {
    var score = 100.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)  // 遞歸(model嵌套的場景)
        }else{
            print("children.label ")
        }
    }
    return keyValue
}
    
let t = LGPerson()
print(test(t))

代碼不難形病,也是通過reflecting初始化mirror對象量瓜,然后讀取其children屬性值绍傲,存儲到keyValue字典并返回烫饼。其中做了判空,以及遞歸處理(處理屬性也是model的場景)比藻。

優(yōu)化一

上述的代碼银亲,是在一個swift命名空間中聲明的解析方法务蝠,我們想讓其通用化赠尾,基于封裝的思想气嫁,自然的想到寸宵,將其抽離成一個協(xié)議,這樣讓每個model遵循該協(xié)議甲棍,那么model就具備了JSON解析的功能感猛。廢話不多說陪白,直接上代碼??

//1、定義一個JSON解析協(xié)議
protocol LGJSONMap {
    func jsonMap() -> Any
}
//2序厉、在extension中實現(xiàn)jsonMap()解析方法
extension LGJSONMap {
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //遞歸終止條件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典弛房,用于存儲json數(shù)據(jù)
        var keyValue: [String: Any] = [:]
        //遍歷
        for children in mirror.children {
            if let value = children.value as? LGJSONMap {
                if let keyName = children.label {
                    //遞歸
                    keyValue[keyName] = value.jsonMap()
                }else{
                    print("key是nil")
                }
            }else{
                print("當前-\(children.value)-沒有遵守協(xié)議")
            }
        }
        return keyValue
    }
}

//3、讓類遵守協(xié)議(注意:類中屬性的類型也需要遵守協(xié)議拄轻,否則無法解析)
class LGPerson : LGJSONMap {
    var name = "Luoji"
    var age = 18
    var student = LGStudent() // 持有對象
}

class LGStudent : LGJSONMap {
    var score = 100.0
}

//調(diào)用
var t = LGPerson()
print(t.jsonMap())

運行??

上圖可知恨搓,沒有成功常拓,提示屬性并沒有遵循協(xié)議,那么根據(jù)錯誤提示宪郊,我們修改代碼??

extension Int: LGJSONMap{}
extension String: LGJSONMap{}
extension Double: LGJSONMap{}

完美解決懊亡!這里將解析方法抽離成協(xié)議的方法店枣,是一種封裝的思想鸯两。既然提到協(xié)議甩卓,那么我們再順便講解下Swift中的Protocol協(xié)議的用法吧。

Protocol

Swift的協(xié)議OC的協(xié)議功能多多了宅此,非常強大父腕,不僅可以聲明屬性璧亮、函數(shù)枝嘶,支持可選實現(xiàn)群扶,還可以在extension中完成屬性函數(shù)默認實現(xiàn)(上述JSON解析就是這樣的場景)缴饭。

所以颗搂,以后大家在開發(fā)過程中峭火,如果碰到了類似的這樣的場景,也可以封裝成協(xié)議的方法稍浆,很好的隔離代碼衅枫,而且讓代碼看起來非常簡潔弦撩。??

遇到一些通用功能的函數(shù),我們可以使用協(xié)議封裝起來点晴,只要遵循這個協(xié)議陪竿,就可以直接調(diào)用相應(yīng)的屬性和函數(shù)族跛。

優(yōu)化二

上述解析方法中礁哄,對于錯誤的處理方式姐仅,只是簡單的print一下掏膏,那有沒有一種更好的方式馒疹,將這些錯誤封裝成一個對象生均,并拋出來給調(diào)用方马胧,讓調(diào)用方自行處理呢?當然有??

可以通過系統(tǒng)Error協(xié)議搭配throw關(guān)鍵字威彰,優(yōu)雅的拋出錯誤或返回常規(guī)結(jié)果歇盼,讓開發(fā)者自己選擇處理方式。

二盈咳、Error處理

在Swift中,系統(tǒng)提供Error協(xié)議來表示當前應(yīng)用程序發(fā)生錯誤的情況丈积,并支持使用throw關(guān)鍵字,優(yōu)雅的拋出錯誤厌均。

2.1 Error協(xié)議

public protocol Error {
}

上面源碼可知 ?? Error是一個協(xié)議晶密,其中沒有任何實現(xiàn)懂牧,這也就意味著你可以遵守該協(xié)議尊勿,然后自定義錯誤類型躯保。所以不管是Struct吻氧、Class盯孙、enum,我們都可以遵循這個Error協(xié)議來表示一個錯誤骑晶。我們就先用枚舉enum來標識錯誤(枚舉的功能也十分強大草慧,后續(xù)會有專門的一篇文章來講解)桶蛔。

  1. 先定義錯誤類型
enum LGJSONMapError: Error{
    case emptyKey
    case notConformProtocol
}
  1. 然后調(diào)用
extension LGJSONMap{
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //遞歸終止條件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存儲json數(shù)據(jù)
        var keyValue: [String: Any] = [:]
        //遍歷
        for children in mirror.children {
            if let value = children.value as? LGJSONMap {
                if let keyName = children.label {
                    //遞歸
                    keyValue[keyName] = value.jsonMap()
                }else{
                    return LGJSONMapError.emptyKey
                }
            }else{
                return LGJSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

但是有個問題漫谷,現(xiàn)在return的數(shù)據(jù)仔雷,無法區(qū)分是正常的dic還是錯誤枚舉,如何處理呢舔示??? 可以利用throw關(guān)鍵字拋出錯誤??

extension LGJSONMap{
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //遞歸終止條件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典碟婆,用于存儲json數(shù)據(jù)
        var keyValue: [String: Any] = [:]
        //遍歷
        for children in mirror.children {
            if let value = children.value as? LGJSONMap {
                if let keyName = children.label {
                    //遞歸
                    keyValue[keyName] = value.jsonMap()
                }else{
                    throw LGJSONMapError.emptyKey
                }
            }else{
                throw LGJSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

return改成throw后,編譯器會報錯??

根據(jù)錯誤的提示信息公给,是因為我們聲明的函數(shù)沒有聲明throw匣沼,那么我們修改函數(shù)聲明??

protocol LGJSONMap {
    func jsonMap() throws ->  Any
}

仍舊會報錯它匕,用了throw沒有用try烧给,那么補齊??

我們把屬性遵循協(xié)議的注釋掉,運行看看是否會拋出錯誤??

果然拋出了LGJSONMapError.notConformProtocol錯誤平项。

Error處理的方式

Swift中錯誤處理的方式主要有以下兩種:

  1. 使用try - throw
    最簡便的园担,即甩鍋,將這個拋出給別人(向上拋出鸽嫂,拋給上層調(diào)用方函數(shù))癣籽。
    但是需要注意以下兩點:
    • try? ?? 返回一個可選類型塑顺,只有兩種結(jié)果:要么成功,返回具體的格二;要么錯誤滔吠,但并不關(guān)心是哪種錯誤,統(tǒng)一返回nil
print(try? t.jsonMap())
  • try! ?? 表示你對這段代碼有絕對的自信属愤,這行代碼絕對不會發(fā)生錯誤
print(try! t.jsonMap())

從上面可以知道驳阎,錯誤是向上拋出的饵隙,即拋給了調(diào)用函數(shù),如果調(diào)用的上層函數(shù)也不處理购披,則直接拋給mainmain沒有辦法處理,則直接報錯垢揩。

  1. 使用do - catch
    其中do處理正確結(jié)果悬包,catch處理error订咸,catch有個隱藏參數(shù)践宴,就是error(Error類型)
do {
    // 處理正常結(jié)果
    try t.jsonMap()   // 這里try不會報錯了,因為錯誤都分給了catch
} catch {
    // 處理錯誤類型(其中error為Error類型)
    print(error)
}
LocalizedError

上述的錯誤處理中,只知道了錯誤類型敷扫,如果還想知道錯誤相關(guān)的描述其它信息,則需要使用LocalizedError膀值,定義??

/// Describes an error that provides localized messages describing why
/// an error occurred and provides more information about the error.
public protocol LocalizedError : Error {

    /// A localized message describing what error occurred.
    var errorDescription: String? { get }

    /// A localized message describing the reason for the failure.
    var failureReason: String? { get }

    /// A localized message describing how one might recover from the failure.
    var recoverySuggestion: String? { get }

    /// A localized message providing "help" text if the user requests help.
    var helpAnchor: String? { get }
}

接下來,我們來使用一下這個協(xié)議茬缩,修改上面的解析代碼??

//定義錯誤類型
enum LGJSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

extension LGJSONMapError : LocalizedError {
    var errorDescription: String?{
            switch self {
            case .emptyKey:
                return "key為空"
            case .notConformProtocol:
                return "沒有遵守協(xié)議"
            }
        }
}

//調(diào)用
var t = LGPerson()
do {
    // 處理正常結(jié)果
    try t.jsonMap()   // 這里try不會報錯了,因為錯誤都分給了catch
} catch {
    // 處理錯誤類型(其中error為Error類型)
    print(error.localizedDescription)
}

完美,錯誤描述被打印了出來殴边!

CustomNSError協(xié)議

還有一個CustomNSError協(xié)議栗恩,相當于OC中的NSError,其定義如下洪燥,有三個默認屬性??

/// Describes an error type that specifically provides a domain, code,
/// and user-info dictionary.
public protocol CustomNSError : Error {

    /// The domain of the error.
    static var errorDomain: String { get }

    /// The error code within the given domain.
    var errorCode: Int { get }

    /// The user-info dictionary.
    var errorUserInfo: [String : Any] { get }
}

接著我們繼續(xù)修改JSON解析中的LGJSONMapError磕秤,讓其遵守CustomNSError協(xié)議??

extension LGJSONMapError: CustomNSError{
    var errorCode: Int{
        switch self {
        case .emptyKey:
            return 404
        case .notConformProtocol:
            return 504
        }
    }
}

//調(diào)用
var t = LGPerson()
do {
    // 處理正常結(jié)果
    try t.jsonMap()   // 這里try不會報錯了,因為錯誤都分給了catch
} catch {
    // 處理錯誤類型(其中error為Error類型)
//    print(error.localizedDescription)
    print((error as? LGJSONMapError)?.errorCode)
}
rethorws

rethrows是處理這種場景的錯誤 ?? 函數(shù)1函數(shù)2入?yún)?/code>捧韵,其中函數(shù)1中有throws錯誤市咆。
上代碼

func add(_ a: Int, _ b: Int) throws -> Int {
    return a + b
}

func exec(_ f:(Int, Int) throws -> Int, _ a: Int, _ b: Int) rethrows {
    print(try f(a, b) )
}

do {
    try exec(add, 1, 2)
}catch {
    print(error.localizedDescription)
}

rethrows將函數(shù)參數(shù)add的錯誤再次拋出,統(tǒng)一交由外部do - catch處理再来。

defer(延后處理)
func functionDefer()  {
    print("begin")
    defer {
        print("defer")
    }
    print("end")
}

函數(shù)執(zhí)行完成之前才會執(zhí)行defer代碼塊內(nèi)部的邏輯蒙兰。如果有多個defer代碼塊磷瘤,執(zhí)行順序怎么樣?

func functionDefer()  {
    print("begin")
    defer {
        print("defer1")
    }
    defer {
        print("defer2")
    }
    print("end")
}

defer代碼塊的執(zhí)行順序是逆序的搜变。

assert(斷言)

很多編程語言都有斷言機制采缚,不符合指定條件拋出運行時錯誤,常用于調(diào)試(Debug)階段條件判斷挠他。例如??

func devide(_ a: Int, _ b: Int) -> Int {
    assert(b != 0, "除數(shù)不能為0")
    return a / b
}

默認情況下扳抽,Swift斷言只會在debug模式下生效,release模式下忽略??


總結(jié)

本篇文章主要利用Swift類中的Mirror反射機制殖侵,封裝了一套JSON解析協(xié)議贸呢,同時將解析時錯誤也進行了封裝處理,順便講解了Error的幾個常用的協(xié)議拢军。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楞陷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子朴沿,更是在濱河造成了極大的恐慌猜谚,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赌渣,死亡現(xiàn)場離奇詭異,居然都是意外死亡昌犹,警方通過查閱死者的電腦和手機坚芜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斜姥,“玉大人鸿竖,你說我怎么就攤上這事≈簦” “怎么了缚忧?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長杈笔。 經(jīng)常有香客問我闪水,道長,這世上最難降的妖魔是什么蒙具? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任球榆,我火速辦了婚禮,結(jié)果婚禮上禁筏,老公的妹妹穿的比我還像新娘持钉。我一直安慰自己,他們只是感情好篱昔,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布每强。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪空执。 梳的紋絲不亂的頭發(fā)上浪箭,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機與錄音脆烟,去河邊找鬼山林。 笑死,一個胖子當著我的面吹牛邢羔,可吹牛的內(nèi)容都是我干的驼抹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼拜鹤,長吁一口氣:“原來是場噩夢啊……” “哼框冀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起敏簿,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤明也,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后惯裕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體温数,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年蜻势,在試婚紗的時候發(fā)現(xiàn)自己被綠了撑刺。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡握玛,死狀恐怖够傍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情挠铲,我是刑警寧澤冕屯,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拂苹,受9級特大地震影響安聘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜醋寝,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一搞挣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧音羞,春花似錦囱桨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搀继。三九已至,卻和暖如春翠语,著一層夾襖步出監(jiān)牢的瞬間叽躯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工肌括, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留点骑,地道東北人。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓谍夭,卻偏偏與公主長得像黑滴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子紧索,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容