遇到的問題
- 上線了兩年的項目,沒有一丁點測試代碼,突然說要開始補單元測試
- 上百個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則是一個包含label和value兩個元素的元組。
label在反射對象為class或struct時為對象的屬性名,value則是屬性值
注意此處label為Optional,在反射對象為collection(array, set, dictionary)時,label為nil,value為collection的元素,下文會再次提及。
typealias Mirror.Children = AnyCollection<Mirror.Child>
typealias Mirror.Child = (label: String?, value: Any)
遍歷aObjectMirror的children并打印,可以看到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的
label為nil
-) 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)
}
}
主要想法如下:
- 定義一個協(xié)議,并擴展該協(xié)議實現(xiàn)對比方法。
- 若該類的屬性的類也遵循該協(xié)議,則遞歸地往下挖掘并對比。
- 直到挖到屬性不遵循該協(xié)議且為Hashable之后,調(diào)用
==方法比對屬性。 - 考慮屬性為collection的情況。