Swift-进阶:反射Mirror & 错误处理

本文主要介绍Mirror的使用以及使用Mirror进行JSON解析的错误处理

反射Mirror

反射:是指可以动态获取类型、成员信息,在运行时可以调用方法、属性等行为的特性,

  • 在上面的分析中,我们已经知道,对于一个纯swift类来说,并不支持直接像OC runtime那样的操作

  • 但是swift标准库依旧提供了反射机制,用来访问成员信息,即Mirror

一般使用

class CJLTeacher: NSObject {
    var age: Int = 18
}
let mirror = Mirror(reflecting: CJLTeacher.self)
for pro in mirror.children{
    print("\(pro.label): \(pro.value)")
}

  • 运行上面代码,发现没有任何打印,为什么?是因为Mirror中传入的参数不对,应该是传入实例对象,修改如下
class CJLTeacher: NSObject {
    var age: Int = 18
}
var t = CJLTeacher()
//传入t也可以
let mirror = Mirror(reflecting: t.self)
for pro in mirror.children{
    print("\(pro.label): \(pro.value)")
}

查看Mirror定义

  • 进入Mirror初始化方法,发现传入的类型是Any,则可以直接传t
public init(reflecting subject: Any)

  • 进入children
public let children: Mirror.Children
👇
//进入Children,发现是一个AnyCollection,接收一个泛型
public typealias Children = AnyCollection<Mirror.Child>
👇
//进入Child,发现是一个元组类型,由可选的标签和值构成,
public typealias Child = (label: String?, value: Any)

这也是为什么能够通过labelvalue打印的原因。即可以在编译时期且不用知道任何类型信息情况下,在Child的值上用Mirror遍历整个对象的层级视图

JSON解析

根据Mirror的这个特性,我们思考下,可以通过Mirror做什么?首先想到的是JSON解析,如下所示,我们定义了一个CJLTeacher类,然后通过一个test方法来解析t

class CJLTeacher {
    var age = 18
    var name = "CJL"
}

<!--JSON解析-->
func test(_ obj: Any) -> Any{
    let mirror = Mirror(reflecting: obj)
    //判断条件 - 递归终止条件
    guard !mirror.children.isEmpty else {
        return obj
    }
    //字典
    var keyValue: [String: Any] = [:]
    //遍历
    for children in mirror.children {
        if let keyName = children.label {
            //递归调用
            keyValue[keyName] = test(children.value)
        }else{
            print("children.label 为空")
        }
    }
    return keyValue
}

<!--使用-->
var t = CJLTeacher()
print(test(t))

代码的打印结果如下,打印出了key-value

JSON解析封装
如果我们想大规模的使用上述的JSON解析,上面只是针对CJLTeacher的JSON解析,所以,为了方便其他类使用,可以将JSON解析抽取成一个协议,然后提供一个默认实现,让类遵守协议

//1、定义一个JSON解析协议
protocol CustomJSONMap {
    func jsonMap() -> Any
}
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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{
                    print("key是nil")
                }
            }else{
                print("当前-\(children.value)-没有遵守协议")
            }
        }
        return keyValue
    }
}

//3、让类遵守协议(注意:类中属性的类型也需要遵守协议,否则无法解析)
class CJLTeacher: CustomJSONMap {
    var age = 18
    var name = "CJL"
}

//使用
var t = CJLTeacher()
print(t.jsonMap())

【问题】:运行代码发现,并不是我们想要的结果,原因是因为CJLTeacher中属性的类型也需要遵守CustomJSONMap协议

【修改】:所以在原有基础上增加以下代码

//Int、String遵守协议
extension Int: CustomJSONMap{}
extension String: CustomJSONMap{}

修改后的运行结果如下

错误处理

为了让我们自己封装的JSON解析更好用,除了对正常返回的处理,还需要对其中的错误进行处理,在上面的封装中,我们目前采用的是print打印的,这样并不规范,也不好维护及管理。那么如何在swift中正确的表达错误呢?

首先,Swift中,提供了Error协议标识当前应用程序发生错误的情况,其中Error的定义如下

public protocol Error {
}

Error是一个空协议,其中没有任何实现,这也就意味着你可以遵守该协议,然后自定义错误类型。所以不管是我们的struct、Class、enum,我们都可以遵循这个Error来表示一个错误

所以接下来,对我们上面封装的JSON解析修改其中的错误处理

  • 定义一个JSONMapError错误枚举,将默认实现的print替换成枚举类型
//定义错误类型
enum JSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

//1、定义一个JSON解析协议
protocol CustomJSONMap {
    func jsonMap() -> Any
}
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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.emptyKey
                }
            }else{
                return JSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}
  • 但是这里有一个问题,jsonMap方法的返回值是Any,我们无法区分返回的是错误还是json数据,那么该如何区分呢?即如何抛出错误呢?在这里可以通过throw关键字(即将Demo中原本return的错误,改成throw抛出)
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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{
                    throw JSONMapError.emptyKey
                }
            }else{
                throw JSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

  • 发现改成throw抛出错误后,编译器提示有错,其原因是因为方法并没有声明成throws

  • 所以还需要在方法的返回值箭头前增加throws(表示将方法中错误抛出),修改后的默认实现代码如下所示

//1、定义一个JSON解析协议
protocol CustomJSONMap {
    func jsonMap() throws-> Any
}
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() throws -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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{
                    throw JSONMapError.emptyKey
                }
            }else{
                throw JSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

  • 由于我们在jsonMap方法中递归调用了自己,所以还需要在递归调用前增加 try 关键字
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() throws -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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()
                }else{
                    throw JSONMapError.emptyKey
                }
            }else{
                throw JSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

<!--使用时需要加上try-->
var t = CJLTeacher()
print(try t.jsonMap())

到此,一个完整的错误表达方式就完成了

swift中错误处理的方式

swift中错误处理的方式主要有以下两种:

  • 【方式一】:使用try关键字,是最简便的,即甩锅,将这个抛出给别人(向上抛出,抛给上层函数)。但是在使用时,需要注意以下两点:
    • try? 返回一个可选类型,只有两种结果:

      • 要么成功,返回具体的字典值

      • 要么错误,但并不关心是哪种错误,统一返回nil

    • try! 表示你对这段代码有绝对的自信,这行代码绝对不会发生错误

  • 【方式二】:使用do...catch

【方式一】try

通过try来处理JSON解析的错误

//CJLTeacher中定义一个height属性,并未遵守协议
class CJLTeacher: CustomJSONMap {
    var age = 18
    var name = "CJL"
    var height = 1.85
}

/*****1、try? 示例*****/
var t = CJLTeacher()
print(try? t.jsonMap())

/*****打印结果*****/
nil

/*****2、try! 示例*****/
var t = CJLTeacher()
print(try! t.jsonMap())

/*****打印结果*****/
Fatal error: 'try!' expression unexpectedly raised an error: _5_MirrorAndError.JSONMapError.notConformProtocol: file _5_MirrorAndError/main.swift, line 90
2020-12-20 18:27:28.305112+0800 05-MirrorAndError[18642:1408258] Fatal error: 'try!' expression unexpectedly raised an error: _5_MirrorAndError.JSONMapError.notConformProtocol: file _5_MirrorAndError/main.swift, line 90

<!--3、如果直接使用try,是向上抛出-->
var t = CJLTeacher()
try t.jsonMap()

/*****打印结果*****/
Fatal error: Error raised at top level: _5_MirrorAndError.JSONMapError.notConformProtocol: file Swift/ErrorType.swift, line 200
2020-12-20 18:31:24.837476+0800 05-MirrorAndError[18662:1410970] Fatal error: Error raised at top level: _5_MirrorAndError.JSONMapError.notConformProtocol: file Swift/ErrorType.swift, line 200

从上面可以知道,错误是向上抛出的,即抛给了上层函数,如果上层函数也不处理,则直接抛给main,main没有办法处理,则直接报错,例如下面的例子

//使用
var t = CJLTeacher()

func test() throws -> Any{
    try t.jsonMap()
}
try test()

其运行结果如下

方式二:do-catch

通过do-catch来处理JSON解析的错误

var t = CJLTeacher()
do{
    try t.jsonMap()
}catch{
    print(error)
}

运行结果如下

LocalError协议

如果只是用Error还不足以表达更详尽的错误信息,可以使用LocalizedError协议,其定义如下

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

  • 继续修改上面的JSON解析,对JSONMapError枚举增加一个扩展,遵守LocalizedError协议,可以打印更详细的错误信息
//定义更详细的错误信息
extension JSONMapError: LocalizedError{
    var errorDescription: String?{
        switch self {
        case .emptyKey:
            return "key为空"
        case .notConformProtocol:
            return "没有遵守协议"
        }
    }
}

<!--使用-->
var t = CJLTeacher()
do{
    try t.jsonMap()
}catch{
    print(error.localizedDescription)
}

运行结果如下

CustomNSError协议

CustomNSError相当于OC中的NSError,其定义如下,有三个默认属性

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

  • 继续修改JSON解析中的JSONMapError,让其遵守CustomNSError协议,如下所示
//CustomNSError相当于NSError
extension JSONMapError: CustomNSError{
    var errorCode: Int{
        switch self {
        case .emptyKey:
            return 404
        case .notConformProtocol:
            return 504
        }
    }
}

<!--使用-->
var t = CJLTeacher()
do{
    try t.jsonMap()
}catch{
    print((error as? JSONMapError)?.errorCode)
}

运行结果如下

总结

  • Error是swift中错误类型的基本协议,其中LocalizedErrorCustomNSError都遵守Error

  • 如果在方法中,想要同时返回正常值错误,需要通过throw返回错误,并且在方法返回值的箭头前面加throws关键字,再调用方法时,还需要加上try关键字,或者使用do-catch,使用try时,有以下两点需要注意:

    • try? 返回的是一个可选类型,要么成功,返回正常值,要么失败,返回nil

    • try! 表示你对自己的代码非常自信,绝对不会发生错误,一旦发生错误,就会崩溃

    • 使用建议:建议使用try?,而不是try!

最后附上完整的自定义JSON解析代码

//定义错误类型
enum JSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

//定义更详细的错误信息
extension JSONMapError: LocalizedError{
    var errorDescription: String?{
        switch self {
        case .emptyKey:
            return "key为空"
        case .notConformProtocol:
            return "没有遵守协议"
        }
    }
}

//CustomNSError相当于NSError
extension JSONMapError: CustomNSError{
    var errorCode: Int{
        switch self {
        case .emptyKey:
            return 404
        case .notConformProtocol:
            return 504
        }
    }
}

//1、定义一个JSON解析协议
protocol CustomJSONMap {
    func jsonMap() throws-> Any
}
//2、提供一个默认实现
extension CustomJSONMap{
    func jsonMap() throws -> Any{
        let mirror = Mirror(reflecting: self)
        //递归终止条件
        guard !mirror.children.isEmpty else {
            return self
        }
        //字典,用于存储json数据
        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()
                }else{
                    throw JSONMapError.emptyKey
                }
            }else{
                throw JSONMapError.notConformProtocol
            }
        }
        return keyValue
    }
}

extension Int: CustomJSONMap{}
extension String: CustomJSONMap{}

//3、让类遵守协议(注意:类中属性的类型也需要遵守协议,否则无法解析)
class CJLTeacher: CustomJSONMap {
    var age = 18
    var name = "CJL"
    var height = 1.85
}

//使用
var t = CJLTeacher()

do{
    try t.jsonMap()
}catch{
    print((error as? JSONMapError)?.errorCode)
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容