盡管 Swift 一直在強(qiáng)調(diào)強(qiáng)類型、編譯時(shí)安全和靜態(tài)調(diào)度,但它的標(biāo)準(zhǔn)庫仍然提供了反射機(jī)制??赡苣阋呀?jīng)在很多博客文章或者類似Tuples、Midi Packets 和 Core Data 的項(xiàng)目中見過它。也許你剛好對(duì)在項(xiàng)目中使用反射機(jī)制感興趣,或者你想更好滴了解反射可以應(yīng)用的領(lǐng)域,那這篇文章就正是你需要的。文章的內(nèi)容是基于我在德國法蘭克福 Macoun會(huì)議上的一次演講,它對(duì) Swift 的反射 API 做了一個(gè)概述。
理解這個(gè)主題最好的方式就是看API,看它都提供了什么功能。
Mirror
Swift 的反射機(jī)制是基于一個(gè)叫 Mirror 的 struct
來實(shí)現(xiàn)的。你為具體的 subject
創(chuàng)建一個(gè) Mirror
,然后就可以通過它查詢這個(gè)對(duì)象 subject
。
在我們創(chuàng)建 Mirror
之前,我們先創(chuàng)建一個(gè)可以讓我們當(dāng)做對(duì)象來使用的簡單數(shù)據(jù)結(jié)構(gòu)。
import Foundation.NSURL // [譯者注]此處應(yīng)該為import Foundation
public class Store {
let storesToDisk: Bool = true
}
public class BookmarkStore: Store {
let itemCount: Int = 10
}
public struct Bookmark {
enum Group {
case Tech
case News
}
private let store = {
return BookmarkStore()
}()
let title: String?
let url: NSURL
let keywords: [String]
let group: Group
}
let aBookmark = Bookmark(title: "Appventure", url: NSURL(string: "appventure.me")!, keywords: ["Swift", "iOS", "OSX"], group: .Tech)
Mirror
創(chuàng)建 Mirror
最簡單的方式就是使用 reflecting
構(gòu)造器:
public init(reflecting subject: Any)
然后在 aBookmark
struct
上使用它:
let aMirror = Mirror(reflecting: aBookmark)
print(aMirror)
// 輸出 : Mirror for Bookmark
這段代碼創(chuàng)建了 Bookmark 的 Mirror
。正如你所見,對(duì)象的類型是 Any
。這是 Swift 中最通用的類型。Swift 中的任何東西至少都是 Any
類型的1。這樣一來 mirror
就可以兼容 struct
, class
, enum
, Tuple
, Array
, Dictionary
, set
等。
Mirror
結(jié)構(gòu)體還有另外三個(gè)構(gòu)造器,但是這三個(gè)都是在你需要自定義 mirror
這種情況下使用的。我們會(huì)在接下來討論自定義 mirror
時(shí)詳細(xì)講解這些額外的構(gòu)造器。
Mirror
中都有什么?
Mirror struct
中包含幾個(gè) types
來幫助確定你想查詢的信息。
第一個(gè)是 DisplayStyle
enum
,它會(huì)告訴你對(duì)象的類型:
public enum DisplayStyle {
case Struct
case Class
case Enum
case Tuple
case Optional
case Collection
case Dictionary
case Set
}
這些都是反射 API 的輔助類型。之前我們知道,反射只要求對(duì)象是 Any
類型,而且Swift 標(biāo)準(zhǔn)庫中還有很多類型為 Any
的東西沒有被列舉在上面的 DisplayStyle
enum
中。如果試圖反射它們中間的某一個(gè)又會(huì)發(fā)生什么呢?比如 closure
。
let closure = { (a: Int) -> Int in return a * 2 }
let aMirror = Mirror(reflecting: closure)
這里你會(huì)得到一個(gè) mirror
,但是 DisplayStyle
為 nil
2
也有提供給 Mirror
的子節(jié)點(diǎn)使用的 typealias
:
public typealias Child = (label: String?, value: Any)
所以每個(gè) Child
都包含一個(gè)可選的 label
和 Any
類型的 value
。為什么 label
是 Optional
的?如果你仔細(xì)考慮下,其實(shí)這是非常有意義的,并不是所有支持反射的數(shù)據(jù)結(jié)構(gòu)都包含有名字的子節(jié)點(diǎn)。 struct
會(huì)以屬性的名字做為 label
,但是 Collection
只有下標(biāo),沒有名字。Tuple
同樣也可能沒有給它們的條目指定名字。
接下來是 AncestorRepresentation
enum
3:
public enum AncestorRepresentation {
/// 為所有 ancestor class 生成默認(rèn) mirror。
case Generated
/// 使用最近的 ancestor 的 customMirror() 實(shí)現(xiàn)來給它創(chuàng)建一個(gè) mirror。
case Customized(() -> Mirror)
/// 禁用所有 ancestor class 的行為。Mirror 的 superclassMirror() 返回值為 nil。
case Suppressed
}
這個(gè) enum
用來定義被反射的對(duì)象的父類應(yīng)該如何被反射。也就是說,這只應(yīng)用于 class
類型的對(duì)象。默認(rèn)情況(正如你所見)下 Swift 會(huì)為每個(gè)父類生成額外的 mirror
。然而,如果你需要做更復(fù)雜的操作,你可以使用 AncestorRepresentation enum
來定義父類被反射的細(xì)節(jié)。我們會(huì)在下面的內(nèi)容中進(jìn)一步研究這個(gè)。
Mirror
現(xiàn)在我們有了給 Bookmark
類型的對(duì)象aBookmark
做反射的實(shí)例變量 aMirror
??梢杂盟鼇碜鍪裁茨??
下面列舉了 Mirror
可用的屬性 / 方法:
let children: Children
:對(duì)象的子節(jié)點(diǎn)。displayStyle: Mirror.DisplayStyle?
:對(duì)象的展示風(fēng)格let subjectType: Any.Type
:對(duì)象的類型func superclassMirror() -> Mirror?
:對(duì)象父類的 mirror
下面我們會(huì)分別對(duì)它們進(jìn)行解析。
displayStyle
很簡單,它會(huì)返回 DisplayStyle
enum
的其中一種情況。如果你想要對(duì)某種不支持的類型進(jìn)行反射,你會(huì)得到一個(gè)空的 Optional
值(這個(gè)之前解釋過)。
print (aMirror.displayStyle)
// 輸出: Optional(Swift.Mirror.DisplayStyle.Struct)
// [譯者注]此處輸出:Optional(Struct)
children
這會(huì)返回一個(gè)包含了對(duì)象所有的子節(jié)點(diǎn)的 AnyForwardCollection<Child>
。這些子節(jié)點(diǎn)不單單限于 Array
或者 Dictionary
中的條目。諸如 struct
或者 class
中所有的屬性也是由 AnyForwardCollection<Child>
這個(gè)屬性返回的子節(jié)點(diǎn)。AnyForwardCollection
協(xié)議意味著這是一個(gè)支持遍歷的 Collection
類型。
for case let (label?, value) in aMirror.children {
print (label, value)
}
//輸出:
//: store main.BookmarkStore
//: title Optional("Appventure")
//: url appventure.me
//: keywords ["Swift", "iOS", "OSX"]
//: group Tech
SubjectType
這是對(duì)象的類型:
print(aMirror.subjectType)
//輸出 : Bookmark
print(Mirror(reflecting: 5).subjectType)
//輸出 : Int
print(Mirror(reflecting: "test").subjectType)
//輸出 : String
print(Mirror(reflecting: NSNull()).subjectType)
//輸出 : NSNull
然而,Swift 的文檔中有下面一句話:
“當(dāng)
self
是另外一個(gè)mirror
的superclassMirror()
時(shí),這個(gè)類型和對(duì)象的動(dòng)態(tài)類型可能會(huì)不一樣“
SuperclassMirror
這是我們對(duì)象父類的 mirror
。如果這個(gè)對(duì)象不是一個(gè)類,它會(huì)是一個(gè)空的 Optional
值。如果對(duì)象的類型是基于類的,你會(huì)得到一個(gè)新的 Mirror
:
// 試試 struct
print(Mirror(reflecting: aBookmark).superclassMirror())
// 輸出: nil
// 試試 class
print(Mirror(reflecting: aBookmark.store).superclassMirror())
// 輸出: Optional(Mirror for Store)
Struct
轉(zhuǎn) Core Data
假設(shè)我們?cè)谝粋€(gè)叫 Books Bunny
的新興高科技公司工作,我們以瀏覽器插件的方式提供了一個(gè)人工智能,它可以自動(dòng)分析用戶訪問的所有網(wǎng)站,然后把相關(guān)頁面自動(dòng)保存到書簽中。
現(xiàn)在是 2016 年,Swift 已經(jīng)開源,所以我們的后臺(tái)服務(wù)端肯定是用 Swift 編寫。因?yàn)樵谖覀兊南到y(tǒng)中同時(shí)有數(shù)以百萬計(jì)的網(wǎng)站訪問活動(dòng),我們想用 struct
來存儲(chǔ)用戶訪問網(wǎng)站的分析數(shù)據(jù)。不過,如果我們 AI 認(rèn)定某個(gè)頁面的數(shù)據(jù)是需要保存到書簽中的話,我們需要使用 CoreData
來把這個(gè)類型的對(duì)象保存到數(shù)據(jù)庫中。
現(xiàn)在我們不想為每個(gè)新建的 struct
單獨(dú)寫自定義的 Core Data
序列化代碼。而是想以一種更優(yōu)雅的方式來開發(fā),從而可以讓將來的所有 struct
都可以利用這種方式來做序列化。
那么我們?cè)撛趺醋瞿兀?/p>
記住,我們有一個(gè) struct
,它需要自動(dòng)轉(zhuǎn)換為 NSManagedObject
(Core Data)。
如果我們想要支持不同的 struct
甚至類型,我們可以用協(xié)議來實(shí)現(xiàn),然后確保我們需要的類型符合這個(gè)協(xié)議。所以我們假想的協(xié)議應(yīng)該有哪些功能呢?
NSManagedObject
。
我們的 protocol
看起來是下面這個(gè)樣子的:
protocol StructDecoder {
// 我們 Core Data 實(shí)體的名字
static var EntityName: String { get }
// 返回包含我們屬性集的 NSManagedObject
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject //[譯者注]使用 NSManagedObjectContext 需要 import CoreData
}
toCoreData
方法使用了 Swift 2.0 新的異常處理來拋出錯(cuò)誤,如果轉(zhuǎn)換失敗,會(huì)有幾種錯(cuò)誤情況,這些情況都在下面的 ErrorType
enum
進(jìn)行了列舉:
enum SerializationError: ErrorType {
// 我們只支持 struct
case StructRequired
// 實(shí)體在 Core Data 模型中不存在
case UnknownEntity(name: String)
// 給定的類型不能保存在 core data 中
case UnsupportedSubType(label: String?)
}
上面列舉了三種轉(zhuǎn)換時(shí)需要注意的錯(cuò)誤情況。第一種情況是我們?cè)噲D把它應(yīng)用到非 struct
的對(duì)象上。第二種情況是我們想要?jiǎng)?chuàng)建的 entity
在 Core Data 模型中不存在。第三種情況是我們想要把一些不能存儲(chǔ)在 Core Data 中的東西保存到 Core Data 中(即 enum
)。
讓我們創(chuàng)建一個(gè) struct
然后為其增加協(xié)議一致性:
Bookmark
struct
struct Bookmark {
let title: String
let url: NSURL
let pagerank: Int
let created: NSDate
}
下一步,我們要實(shí)現(xiàn) toCoreData
方法。
當(dāng)然我們可以為每個(gè) struct
都寫新的 toCoreData
方法,但是工作量很大,因?yàn)?struct
不支持繼承,所以我們不能使用基類的方式。不過我們可以使用 protocol extension
來擴(kuò)展這個(gè)方法到所有相符合的 struct
:
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
}
}
因?yàn)閿U(kuò)展已經(jīng)被應(yīng)用到相符合的 struct
,這個(gè)方法就可以在 struct
的上下文中被調(diào)用。因此,在協(xié)議中,self
指的是我們想分析的 struct
。
所以,我們需要做的第一步就是創(chuàng)建一個(gè)可以寫入我們 Bookmark struct
值的NSManagedObject
。我們?cè)撛趺醋瞿兀?/p>
Core Data
Core Data
有點(diǎn)啰嗦,所以如果需要?jiǎng)?chuàng)建一個(gè)對(duì)象,我們需要如下的步驟:
NSManagedObjectContext
,然后為我們的實(shí)體創(chuàng)建 NSEntityDescription
NSManagedObject
。實(shí)現(xiàn)代碼如下:
// 獲取 Core Data 實(shí)體的名字
let entityName = self.dynamicType.EntityName
// 創(chuàng)建實(shí)體描述
// 實(shí)體可能不存在, 所以我們使用 'guard let' 來判斷,如果實(shí)體
// 在我們的 core data 模型中不存在的話,我們就拋出錯(cuò)誤
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw UnknownEntity(name: entityName) } // [譯者注] UnknownEntity 為 SerializationError.UnknownEntity
// 創(chuàng)建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
下一步,我們想使用反射 API 來讀取 bookmark
對(duì)象的屬性然后把它寫入到 NSManagedObject
實(shí)例中。
// 創(chuàng)建 Mirror
let mirror = Mirror(reflecting: self)
// 確保我們是在分析一個(gè) struct
guard mirror.displayStyle == .Struct else { throw SerializationError.StructRequired }
我們通過測試 displayStyle
屬性的方式來確保這是一個(gè) struct
。
所以現(xiàn)在我們有了一個(gè)可以讓我們讀取屬性的 Mirror
,也有了一個(gè)可以用來設(shè)置屬性的 NSManagedObject
。因?yàn)?mirror
提供了讀取所有 children
的方式,所以我們可以遍歷它們并保存它們的值。方式如下:
for case let (label?, value) in mirror.children {
managedObject.setValue(value, forKey: label)
}
太棒了!但是,如果我們?cè)噲D編譯它,它會(huì)失敗。原因是 setValueForKey
需要一個(gè) AnyObject?
類型的對(duì)象,而我們的 children
屬性只返回一個(gè) (String?, Any)
類型的 tuple
——也就是說 value
是 Any
類型,但是我們需要 AnyObject
類型的。為了解決這個(gè)問題,我們要測試 value
的 AnyObject
協(xié)議一致性。這也意味著如果得到的屬性的類型不符合 AnyObject
協(xié)議(比如 enum
),我們就可以拋出一個(gè)錯(cuò)誤。
let mirror = Mirror(reflecting: self)
guard mirror.displayStyle == .Struct
else { throw SerializationError.StructRequired }
for case let (label?, anyValue) in mirror.children {
if let value = anyValue as? AnyObject {
managedObject.setValue(child, forKey: label) // [譯者注] 正確代碼為:managedObject.setValue(value, forKey: label)
} else {
throw SerializationError.UnsupportedSubType(label: label)
}
}
現(xiàn)在,只有在 child
是 AnyObject
類型的時(shí)候我們才會(huì)調(diào)用 setValueForKey
方法。
然后唯一剩下的事情就是返回 NSManagedObject
。完整的代碼如下:
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
let entityName = self.dynamicType.EntityName
// 創(chuàng)建實(shí)體描述
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw UnknownEntity(name: entityName) } // [譯者注] UnknownEntity 為 SerializationError.UnknownEntity
// 創(chuàng)建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
// 創(chuàng)建一個(gè) Mirror
let mirror = Mirror(reflecting: self)
// 確保我們是在分析一個(gè) struct
guard mirror.displayStyle == .Struct else { throw SerializationError.StructRequired }
for case let (label?, anyValue) in mirror.children {
if let value = anyValue as? AnyObject {
managedObject.setValue(child, forKey: label) // [譯者注] 正確代碼為:managedObject.setValue(value, forKey: label)
} else {
throw SerializationError.UnsupportedSubType(label: label)
}
}
return managedObject
}
}
搞定,我們現(xiàn)在已經(jīng)把 struct
轉(zhuǎn)換為 NSManagedObject
了。
那么,速度如何呢?這個(gè)方法可以在生產(chǎn)中應(yīng)用么?我做了一些測試:
創(chuàng)建 2000 個(gè) NSManagedObject
原生: 0.062 seconds
反射: 0.207 seconds
這里的原生是指創(chuàng)建一個(gè) NSManagedObject
,然后通過 setValueForKey
設(shè)置屬性值。如果你在 Core Data
內(nèi)創(chuàng)建一個(gè) NSManagedObject
子類然后把值直接設(shè)置到屬性上(沒有了動(dòng)態(tài) setValueForKey
的開銷),速度可能更快。
所以正如你所見,使用反射使創(chuàng)建 NSManagedObject
的性能下降了3.5倍。當(dāng)你在數(shù)量有限的項(xiàng)目上使用這個(gè)方法,或者你不關(guān)心處理速度時(shí),這是沒問題的。但是當(dāng)你需要反射大量的 struct
時(shí),這個(gè)方法可能會(huì)大大降低你 app 的性能。
<a name="custom_mirrors">
Mirror
我們之前已經(jīng)討論過,創(chuàng)建 Mirror
還有其他的選項(xiàng)。這些選項(xiàng)是非常有用的,比如,你想自己定義 mirror
中對(duì)象的哪些部分是可訪問的。對(duì)于這種情況 Mirror Struct
提供了其他的構(gòu)造器。
Collection
第一個(gè)特殊 init
是為 Collection
量身定做的:
public init<T, C : CollectionType where C.Generator.Element == Child>
(_ subject: T, children: C,
displayStyle: Mirror.DisplayStyle? = default,
ancestorRepresentation: Mirror.AncestorRepresentation = default)
與之前的 init(reflecting:)
相比,這個(gè)構(gòu)造器允許我們定義更多反射處理的細(xì)節(jié)。
Collection
有效children
(Collection
的內(nèi)容)class
或者 struct
第二個(gè)可以在 class
或者 struct
上使用。
public init<T>(_ subject: T,
children: DictionaryLiteral<String, Any>,
displayStyle: Mirror.DisplayStyle? = default,
ancestorRepresentation: Mirror.AncestorRepresentation = default)
有意思的是,這里是由你指定對(duì)象的 children
(即屬性),指定的方式是通過一個(gè) DictionaryLiteral
,它有點(diǎn)像字典,可以直接用作函數(shù)參數(shù)。如果我們?yōu)?Bookmark struct
實(shí)現(xiàn)這個(gè)構(gòu)造器,它看起來是這樣的:
extension Bookmark: CustomReflectable {
func customMirror() -> Mirror { // [譯者注] 此處應(yīng)該為 public func customMirror() -> Mirror {
let children = DictionaryLiteral<String, Any>(dictionaryLiteral:
("title", self.title), ("pagerank", self.pagerank),
("url", self.url), ("created", self.created),
("keywords", self.keywords), ("group", self.group))
return Mirror.init(Bookmark.self, children: children,
displayStyle: Mirror.DisplayStyle.Struct,
ancestorRepresentation:.Suppressed)
}
}
如果現(xiàn)在我們做另外一個(gè)性能測試,會(huì)發(fā)現(xiàn)性能甚至略微有所提升:
創(chuàng)建 2000 個(gè) NSManagedObject
原生: 0.062 seconds
反射: 0.207 seconds
反射: 0.203 seconds
但這個(gè)工作幾乎沒有任何價(jià)值,因?yàn)樗c我們之前反射 struct
成員變量的初衷是相違背的。
所以留下來讓我們思考的問題是什么呢?好的反射用例又是什么呢?很顯然,如果你在很多 NSManagedObject
上使用反射,它會(huì)大大降低你代碼的性能。同時(shí)如果只有一個(gè)或者兩個(gè) struct
,根據(jù)自己掌握的struct
領(lǐng)域的知識(shí)編寫一個(gè)序列化的方法會(huì)更容易,更高性能且更不容易讓人困惑。
而本文展示反射技巧可以當(dāng)你在有很多復(fù)雜的 struct
,且偶爾想對(duì)它們中的一部分進(jìn)行存儲(chǔ)時(shí)使用。
例子如下:
除了這些,反射當(dāng)然還有其他的使用場景:
tuple
反射 API 主要做為 Playground
的一個(gè)工具。符合反射 API 的對(duì)象可以很輕松滴就在 Playground
的側(cè)邊欄中以分層的方式展示出來。盡管它的性能不是最優(yōu)的,在 Playground
之外仍然有很多有趣的應(yīng)用場景,這些應(yīng)用場景我們?cè)?strong>用例章節(jié)中都講解過。
反射 API 的源文件注釋非常詳細(xì),我強(qiáng)烈建議每個(gè)人都去看看。
同時(shí),GitHub 上的 CoreValue 項(xiàng)目展示了關(guān)于這個(gè)技術(shù)更詳盡的實(shí)現(xiàn),它可以讓你很輕松滴把 struct
編碼成 CoreData
,或者把 CoreData
解碼成 struct
。
<a name="1">1、實(shí)際上,Any
是一個(gè)空的協(xié)議,所有的東西都隱式滴符合這個(gè)協(xié)議。
<a name="2">2、更確切地說,是一個(gè)空的可選類型。
<a name="3">3、我對(duì)注釋稍微做了簡化。
更多建議: