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