使用Mirror自動比較Swift Class, Struct的嘗試

遇到的問題

  1. 上線了兩年的項目,沒有一丁點測試代碼,突然說要開始補單元測試
  2. 上百個Model,有struct,有class,全都沒有遵循Equatable協(xié)議...要比較兩個Model是否相同,需要逐條比較Model的property(屬性)

想做的事情

能否以最小的成本讓上百個Model在測試中可以自動比較

使用Mirror獲取property list

動態(tài)地獲取property list

Objective-C強大的運行時系統(tǒng)使得開發(fā)者可以在運行時獲取class的property list。
https://stackoverflow.com/questions/754824/get-an-object-properties-list-in-objective-c
因此碰到這個問題時,我首先想的是,是否Swift也可以用類似class_copyPropertyList這樣的API獲取對象的property list。
但是發(fā)現(xiàn)想多了...Swift是一門靜態(tài)語言,在Swift官方主頁的介紹中就強調(diào)了

Swift code is safe by design

這樣的表述。在運行時動態(tài)地獲取和篡改對象的property顯然不是Swift所推崇的。

不過Swift還是很體貼地提供了一個工具:Mirror,來滿足開發(fā)者的一些運行時需求。

Mirror的定義

A mirror describes the parts that make up a particular instance, such as the instance’s stored properties, collection or tuple elements, or its active enumeration case. Mirrors also provide a “display style” property that suggests how this mirror might be rendered.

Mirror描述了一個特定實例的結(jié)構(gòu),

  • 實例為class或struct,則描述該實例的儲存屬性 (stored properties)
  • 實例為collection或tuple (元組) ,則描述其elements (元素)
  • 實例為enumeration,則描述其當(dāng)前case

Playgrounds and the debugger use the Mirror type to display representations of values of any type. For example, when you pass an instance to the dump(::::) function, a mirror is used to render that instance’s runtime contents.

關(guān)于Mirror的用途,官方文檔提到Mirror主要是在Playgrounds或debug時使用,Swift的調(diào)試用函數(shù)dump(_:name:indent:maxDepth:maxItems:)即是使用Mirror來反射實例。
https://developer.apple.com/documentation/swift/1539127-dump
雖然沒有明確禁止在生產(chǎn)環(huán)境中使用Mirror,但是Mirror強大的運行時特性注定了它不是一個高效的選擇(參考自喵神這篇文章https://onevcat.com/2018/03/swift-meta/)。
嘛,單元測試?yán)锬脕碛靡幌聭?yīng)該問題不大吧哈哈哈哈h...

Mirror的基礎(chǔ)用法

光說不練假把式,我們來實際使用Mirror試試。
Mirror的使用方法非常簡單,使用Mirror的init(reflecting:)初始化方法,傳入想要反射的對象,就能生成反射對象的Mirror實例。

class AClass {
    let storedProperty: String
    var computedProperty: String {
        return "hi"
    }
    init(title: String) {
        storedProperty = title
    }
}

let aObject = AClass(title: "a")
let aObjectMirror = Mirror(reflecting: aObject)

調(diào)用Mirror實例的children屬性可以得到一個Mirror.Child的collection,而Mirror.Child則是一個包含labelvalue兩個元素的元組。
label在反射對象為class或struct時為對象的屬性名,value則是屬性值
注意此處label為Optional,在反射對象為collection(array, set, dictionary)時,labelnil,value為collection的元素,下文會再次提及。

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

遍歷aObjectMirrorchildren并打印,可以看到AClass的儲存屬性都被反射,而計算屬性沒有被反射。

aObjectMirror.children.forEach { print($0) }
/* output:
 (label: Optional("storedPropertyStr"), value: "a")
 (label: Optional("storedPropertyInt"), value: 1)
 */

使用Mirror反射class實例的基本用法如上所示,按官方文檔的說法,Mirror還可以作用于collection, tuple, enumeration。那具體反射后得到的Mirror是什么樣的,我們一個個把玩下
https://github.com/itsuhi-shu/PropertyEquatable/blob/master/MirrorPlayground.playground/Contents.swift

因篇幅限制,挑一些比較在意的內(nèi)容總結(jié):

  • class, struct的計算屬性不會被反射
  • collection(array, set, dictionary)反射后其child的labelnil
    -) array, set的value為其各個元素
    -) dictionary的value為其單個鍵值對構(gòu)成的元組(key: Hashable, value: Any)
let aDictionary = ["key1": "a", "key2": "b", "key3": "c"]
let aDictionaryMirror = Mirror(reflecting: aDictionary)
print(aDictionaryMirror.displayStyle!) // dictionary
aDictionaryMirror.children.forEach { print($0) }
/*
 (label: nil, value: (key: "key2", value: "b"))
 (label: nil, value: (key: "key1", value: "a"))
 (label: nil, value: (key: "key3", value: "c"))
 */
  • tuple的label若有定義標(biāo)簽名則為標(biāo)簽名,若沒有則為.n(n為該元素的序列)
let aTuple = ("a", labeled: 2, 9)
let aTupleMirror = Mirror(reflecting: aTuple)
print(aTupleMirror.displayStyle!) // tuple
aTupleMirror.children.forEach { print($0) }
/*
 (label: Optional(".0"), value: "a")
 (label: Optional("labeled"), value: 2)
 (label: Optional(".2"), value: 9)
 */
  • enum只能反射associated中儲存的屬性
enum AEnum {
    case first
    case seconde
}
enum AEnumWithAssociatedValues {
    case first
    case second(with: String)
    case third(title: String, Value: Int, complete: (() -> Void)?)
}

let aEnum = AEnum.seconde
let aEnumMirror = Mirror(reflecting: aEnum)
print(aEnumMirror.displayStyle!) // enum
aEnumMirror.children.forEach { print($0) }
/*
 */
print(aEnumMirror.children.count) // 0

let aEnumWithAssociatedValues = AEnumWithAssociatedValues.third(title: "a",
                                                                Value: 1,
                                                                complete: nil)
let aEnumWithAssociatedValuesMirror = Mirror(reflecting: aEnumWithAssociatedValues)
print(aEnumWithAssociatedValuesMirror.displayStyle!) // enum
aEnumWithAssociatedValuesMirror.children.forEach { print($0) }
/*
 (label: Optional("third"), value: (title: "a", Value: 1, complete: nil))
 */

利用Mirror反射來自動比較Model

了解了Mirror的基本用法,現(xiàn)在該思考如何將Mirror運用在開發(fā)中。
在上面提到,筆者想要寫最少的代碼來實現(xiàn)Model的比較,在有了Mirror之后,接下來很自然而言地就想到要定義一個protocol(協(xié)議),通過protocol的extension(擴展協(xié)議)來為Model添加一個比較方法。
一開始曾試過用protocol extension來讓Model遵循Equatable協(xié)議,并添加對==的實現(xiàn)。但后來突然發(fā)現(xiàn)NSObject都遵循了Equatable協(xié)議,且其==實現(xiàn)為單純的地址比較(一部分子類override了)。而protocol extension不能override類已實現(xiàn)的方法。所以只能另外提供一個比較函數(shù)。

最終的實現(xiàn)如下:

protocol PropertyEquatable {}
extension PropertyEquatable {
    // Do not use `==` because extension can NOT override methods,
    // and most NSObjects conforms to `Equatable` by implementing `==` simply compare their addresses.
    static func ~= (lhs: Self, rhs: Self) -> Bool {
        func _recursiveCompareElements(lhs: Any, rhs: Any) -> Bool {
            guard type(of: lhs) == type(of: rhs) else { return false }

            let lMir = Mirror(reflecting: lhs)
            let rMir = Mirror(reflecting: rhs)

            return lMir.children.elementsEqual(rMir.children) { (lElm, rElm) -> Bool in
                guard let lKey = lElm.label,
                    let rKey = rElm.label,
                    lKey == rKey else {
                        return false
                }

                // MARK: Collection
                // Arrays, Sets, Dictionaries are all Hashable, and can easily fall down to AnyHashable.
                // But if the Elements are MirrorEquatable, we need to compare the Elements using our methods.

                // Arrays
                if let lArr = lElm.value as? Array<PropertyEquatable>,
                    let rArr = rElm.value as? Array<PropertyEquatable> {
                    return lArr.elementsEqual(rArr) { _recursiveCompareElements(lhs: $0, rhs: $1) }
                }

                // Sets Elements must be Hashable, we don't need to consider about this case.

                // Dictionaries
                if let lDic = lElm.value as? [AnyHashable: PropertyEquatable],
                    let rDic = rElm.value as? [AnyHashable: PropertyEquatable] {
                    guard lDic.count == rDic.count else { return false }
                    for key in lDic.keys {
                        guard let lVal = lDic[key], let rVal = rDic[key] else { return false }
                        if !_recursiveCompareElements(lhs: lVal, rhs: rVal) {
                            return false
                        }
                    }
                    return true
                }

                // MARK: AnyHashable
                if let lVal = lElm.value as? AnyHashable,
                    let rVal = rElm.value as? AnyHashable {
                    return lVal == rVal
                }

                // MARK: Classes, Structs and others that do not match the conditions above
                return _recursiveCompareElements(lhs: lElm.value, rhs: rElm.value)
            }
        }

        return _recursiveCompareElements(lhs: lhs, rhs: rhs)
    }
}

主要想法如下:

  1. 定義一個協(xié)議,并擴展該協(xié)議實現(xiàn)對比方法。
  2. 若該類的屬性的類也遵循該協(xié)議,則遞歸地往下挖掘并對比。
  3. 直到挖到屬性不遵循該協(xié)議且為Hashable之后,調(diào)用==方法比對屬性。
  4. 考慮屬性為collection的情況。

完成!!
https://github.com/itsuhi-shu/PropertyEquatable

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容