Swift反射API及其用法

盡管 Swift 一直在強(qiáng)調(diào)強(qiáng)類型、編譯時(shí)安全和靜態(tài)調(diào)度,但它的標(biāo)準(zhǔn)庫(kù)仍然提供了反射機(jī)制??赡苣阋呀?jīng)在很多博客文章或者類似Tuples、Midi PacketsCore Data的項(xiàng)目中見過(guò)它。也許你剛好對(duì)在項(xiàng)目中使用反射機(jī)制感興趣,或者你想更好的了解反射可以應(yīng)用的領(lǐng)域,那這篇文章就正是你需要的。文章的內(nèi)容是基于我在德國(guó)法蘭克福 Macoun會(huì)議上的一次演講,它對(duì) Swift 的反射 API 做了一個(gè)概述。

API 概述

理解這個(gè)主題最好的方式就是看 API,看它都提供了什么功能。

Mirror

Swift 的反射機(jī)制是基于一個(gè)叫Mirror的struct來(lái)實(shí)現(xiàn)的。你為具體的subject創(chuàng)建一個(gè)Mirror,然后就可以通過(guò)它查詢這個(gè)對(duì)象subject。

在我們創(chuàng)建Mirror之前,我們先創(chuàng)建一個(gè)可以讓我們當(dāng)做對(duì)象來(lái)使用的簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)。

importFoundation.NSURL// [譯者注]此處應(yīng)該為import Foundation

publicclassStore{

letstoresToDisk:Bool=true

}

publicclassBookmarkStore:Store{

letitemCount:Int=10

}

publicstructBookmark{

enumGroup{

caseTech

caseNews

}

privateletstore = {

returnBookmarkStore()

}()

lettitle:String?

leturl:NSURL

letkeywords: [String]

letgroup:Group

}

letaBookmark =Bookmark(title:"Appventure", url:NSURL(string:"appventure.me")!, keywords: ["Swift","iOS","OSX"], group: .Tech)

創(chuàng)建一個(gè)Mirror

創(chuàng)建Mirror最簡(jiǎn)單的方式就是使用reflecting構(gòu)造器:

publicinit(reflecting subject:Any)

然后在aBookmarkstruct上使用它:

letaMirror =Mirror(reflecting: aBookmark)

print(aMirror)

// 輸出 : Mirror for Bookmark

這段代碼創(chuàng)建了Bookmark 的 Mirror。正如你所見,對(duì)象的類型是Any。這是 Swift 中最通用的類型。Swift 中的任何東西至少都是Any類型的1。這樣一來(lái)mirror就可以兼容struct,class,enum,Tuple,Array,Dictionary,set等。

Mirror結(jié)構(gòu)體還有另外三個(gè)構(gòu)造器,然而這三個(gè)都是在你需要自定義mirror這種情況下使用的。我們會(huì)在接下來(lái)討論自定義mirror時(shí)詳細(xì)講解這些額外的構(gòu)造器。

Mirror中都有什么?

Mirror struct中包含幾個(gè)types來(lái)幫助確定你想查詢的信息。

第一個(gè)是DisplayStyleenum,它會(huì)告訴你對(duì)象的類型:

publicenumDisplayStyle{

caseStruct

caseClass

caseEnum

caseTuple

caseOptional

caseCollection

caseDictionary

caseSet

}

這些都是反射 API 的輔助類型。正如之前我們知道的,反射只要求對(duì)象是Any類型,而且Swift 標(biāo)準(zhǔn)庫(kù)中還有很多類型為Any的東西沒(méi)有被列舉在上面的DisplayStyleenum中。如果試圖反射它們中間的某一個(gè)又會(huì)發(fā)生什么呢?比如closure。

letclosure = { (a:Int) ->Intinreturna *2}

letaMirror =Mirror(reflecting: closure)

在這種情況下,這里你會(huì)得到一個(gè)mirror,但是DisplayStyle為nil2

也有提供給Mirror的子節(jié)點(diǎn)使用的typealias:

publictypealiasChild= (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),沒(méi)有名字。Tuple同樣也可能沒(méi)有給它們的條目指定名字。

接下來(lái)是AncestorRepresentationenum3

publicenumAncestorRepresentation{

/// 為所有 ancestor class 生成默認(rèn) mirror。

caseGenerated

/// 使用最近的 ancestor 的 customMirror() 實(shí)現(xiàn)來(lái)給它創(chuàng)建一個(gè) mirror。

caseCustomized(() ->Mirror)

/// 禁用所有 ancestor class 的行為。Mirror 的 superclassMirror() 返回值為 nil。

caseSuppressed

}

這個(gè)enum用來(lái)定義被反射的對(duì)象的父類應(yīng)該如何被反射。也就是說(shuō),這只應(yīng)用于class類型的對(duì)象。默認(rèn)情況(正如你所見)下 Swift 會(huì)為每個(gè)父類生成額外的mirror。然而,如果你需要做更復(fù)雜的操作,你可以使用AncestorRepresentation enum來(lái)定義父類被反射的細(xì)節(jié)。我們會(huì)在下面的內(nèi)容中進(jìn)一步研究這個(gè)

如何使用一個(gè)Mirror

現(xiàn)在我們有了給Bookmark類型的對(duì)象aBookmark做反射的實(shí)例變量aMirror??梢杂盟鼇?lái)做什么呢?

下面列舉了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

很簡(jiǎn)單,它會(huì)返回DisplayStyleenum的其中一種情況。如果你想要對(duì)某種不支持的類型進(jìn)行反射,你會(huì)得到一個(gè)空的Optional值(這個(gè)之前解釋過(guò))。

print(aMirror.displayStyle)

// 輸出: Optional(Swift.Mirror.DisplayStyle.Struct)

// [譯者注]此處輸出:Optional(Struct)

children

這會(huì)返回一個(gè)包含了對(duì)象所有的子節(jié)點(diǎn)的AnyForwardCollection。這些子節(jié)點(diǎn)不單單限于Array或者Dictionary中的條目。諸如struct或者class中所有的屬性也是由AnyForwardCollection這個(gè)屬性返回的子節(jié)點(diǎn)。AnyForwardCollection協(xié)議意味著這是一個(gè)支持遍歷的Collection類型。

forcaselet(label?, value)inaMirror.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)

實(shí)例

Struct轉(zhuǎn)Core Data

假設(shè)我們?cè)谝粋€(gè)叫Books Bunny的新興高科技公司工作,我們以瀏覽器插件的方式提供了一個(gè)人工智能,它可以自動(dòng)分析用戶訪問(wèn)的所有網(wǎng)站,然后把相關(guān)頁(yè)面自動(dòng)保存到書簽中。

現(xiàn)在是 2016 年,Swift 已經(jīng)開源,所以我們的后臺(tái)服務(wù)端肯定是用 Swift 編寫。因?yàn)樵谖覀兊南到y(tǒng)中同時(shí)有數(shù)以百萬(wàn)計(jì)的網(wǎng)站訪問(wèn)活動(dòng),我們想用struct來(lái)存儲(chǔ)用戶訪問(wèn)網(wǎng)站的分析數(shù)據(jù)。不過(guò),如果我們 AI 認(rèn)定某個(gè)頁(yè)面的數(shù)據(jù)是需要保存到書簽中的話,我們需要使用CoreData來(lái)把這個(gè)類型的對(duì)象保存到數(shù)據(jù)庫(kù)中。

現(xiàn)在我們不想為每個(gè)新建的struct單獨(dú)寫自定義的Core Data序列化代碼。而是想以一種更優(yōu)雅的方式來(lái)開發(fā),從而可以讓將來(lái)的所有struct都可以利用這種方式來(lái)做序列化。

那么我們?cè)撛趺醋瞿兀?/p>

一個(gè)協(xié)議

記住,我們有一個(gè)struct,它需要自動(dòng)轉(zhuǎn)換為NSManagedObject(Core Data)。

如果我們想要支持不同的struct甚至類型,我們可以用協(xié)議來(lái)實(shí)現(xiàn),然后確保我們需要的類型符合這個(gè)協(xié)議。所以我們假想的協(xié)議應(yīng)該有哪些功能呢?

第一,協(xié)議應(yīng)該允許自定義我們想要?jiǎng)?chuàng)建的Core Data 實(shí)體的名字

第二,協(xié)議需要提供一種方式來(lái)告訴它如何轉(zhuǎn)換為NSManagedObject。

我們的protocol(協(xié)議) 看起來(lái)是下面這個(gè)樣子的:

protocolStructDecoder{

// 我們 Core Data 實(shí)體的名字

staticvarEntityName:String{get}

// 返回包含我們屬性集的 NSManagedObject

functoCoreData(context: NSManagedObjectContext)throws->NSManagedObject//[譯者注]使用 NSManagedObjectContext 需要 import CoreData

}

toCoreData方法使用了 Swift 2.0 新的異常處理來(lái)拋出錯(cuò)誤,如果轉(zhuǎn)換失敗,會(huì)有幾種錯(cuò)誤情況,這些情況都在下面的ErrorTypeenum進(jìn)行了列舉:

enumSerializationError:ErrorType{

// 我們只支持 struct

caseStructRequired

// 實(shí)體在 Core Data 模型中不存在

caseUnknownEntity(name:String)

// 給定的類型不能保存在 core data 中

caseUnsupportedSubType(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

structBookmark{

lettitle:String

leturl:NSURL

letpagerank:Int

letcreated:NSDate

}

接下來(lái),我們要實(shí)現(xiàn)toCoreData方法。

協(xié)議擴(kuò)展

當(dāng)然我們可以為每個(gè)struct都寫新的toCoreData方法,但是工作量很大,因?yàn)閟truct不支持繼承,所以我們不能使用基類的方式。不過(guò)我們可以使用protocol extension來(lái)擴(kuò)展這個(gè)方法到所有相符合的struct:

extensionStructDecoder{

functoCoreData(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>

一點(diǎn)Core Data

Core Data有點(diǎn)啰嗦,所以如果需要?jiǎng)?chuàng)建一個(gè)對(duì)象,我們需要如下的步驟:

獲得我們需要?jiǎng)?chuàng)建的實(shí)體的名字(字符串)

獲取NSManagedObjectContext,然后為我們的實(shí)體創(chuàng)建NSEntityDescription

利用這些信息創(chuàng)建NSManagedObject。

實(shí)現(xiàn)代碼如下:

// 獲取 Core Data 實(shí)體的名字

letentityName =self.dynamicType.EntityName

// 創(chuàng)建實(shí)體描述

// 實(shí)體可能不存在, 所以我們使用 'guard let' 來(lái)判斷,如果實(shí)體

// 在我們的 core data 模型中不存在的話,我們就拋出錯(cuò)誤

guardletdesc =NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)

else{throwUnknownEntity(name: entityName) }// [譯者注] UnknownEntity 為 SerializationError.UnknownEntity

// 創(chuàng)建 NSManagedObject

letmanagedObject =NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

實(shí)現(xiàn)反射

下一步,我們想使用反射 API 來(lái)讀取bookmark對(duì)象的屬性然后把它寫入到NSManagedObject實(shí)例中。

// 創(chuàng)建 Mirror

letmirror =Mirror(reflecting:self)

// 確保我們是在分析一個(gè) struct

guardmirror.displayStyle == .Structelse{throwSerializationError.StructRequired}

我們通過(guò)測(cè)試displayStyle屬性的方式來(lái)確保這是一個(gè)struct。

所以現(xiàn)在我們有了一個(gè)可以讓我們讀取屬性的Mirror,也有了一個(gè)可以用來(lái)設(shè)置屬性的NSManagedObject。因?yàn)閙irror提供了讀取所有children的方式,所以我們可以遍歷它們并保存它們的值。方式如下:

forcaselet(label?, value)inmirror.children {

managedObject.setValue(value, forKey: label)

}

太棒了!但是,如果我們?cè)噲D編譯它,它會(huì)失敗。原因是setValueForKey需要一個(gè)AnyObject?類型的對(duì)象,而我們的children屬性只返回一個(gè)(String?, Any)類型的tuple。也就是說(shuō)value是Any類型但是我們需要AnyObject類型的。為了解決這個(gè)問(wèn)題,我們要測(cè)試value的AnyObject協(xié)議一致性。這也意味著如果得到的屬性的類型不符合AnyObject協(xié)議(比如enum),我們就可以拋出一個(gè)錯(cuò)誤。

letmirror =Mirror(reflecting:self)

guardmirror.displayStyle == .Struct

else{throwSerializationError.StructRequired}

forcaselet(label?, anyValue)inmirror.children {

ifletvalue = anyValueas?AnyObject{

managedObject.setValue(child, forKey: label)// [譯者注] 正確代碼為:managedObject.setValue(value, forKey: label)

}else{

throwSerializationError.UnsupportedSubType(label: label)

}

}

現(xiàn)在,只有在child是AnyObject類型的時(shí)候我們才會(huì)調(diào)用setValueForKey方法。

然后唯一剩下的事情就是返回NSManagedObject。完整的代碼如下:

extensionStructDecoder{

functoCoreData(context: NSManagedObjectContext)throws->NSManagedObject{

letentityName =self.dynamicType.EntityName

// 創(chuàng)建實(shí)體描述

guardletdesc =NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)

else{throwUnknownEntity(name: entityName) }// [譯者注] UnknownEntity 為 SerializationError.UnknownEntity

// 創(chuàng)建 NSManagedObject

letmanagedObject =NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

// 創(chuàng)建一個(gè) Mirror

letmirror =Mirror(reflecting:self)

// 確保我們是在分析一個(gè) struct

guardmirror.displayStyle == .Structelse{throwSerializationError.StructRequired}

forcaselet(label?, anyValue)inmirror.children {

ifletvalue = anyValueas?AnyObject{

managedObject.setValue(child, forKey: label)// [譯者注] 正確代碼為:managedObject.setValue(value, forKey: label)

}else{

throwSerializationError.UnsupportedSubType(label: label)

}

}

returnmanagedObject

}

}

搞定,我們現(xiàn)在已經(jīng)把struct轉(zhuǎn)換為NSManagedObject了。

性能

那么,速度如何呢?這個(gè)方法可以在生產(chǎn)中應(yīng)用么?我做了一些測(cè)試:

創(chuàng)建2000個(gè)NSManagedObject

原生:0.062seconds

反射:0.207seconds

這里的原生是指創(chuàng)建一個(gè)NSManagedObject,然后通過(guò)setValueForKey設(shè)置屬性值。如果你在Core Data內(nèi)創(chuàng)建一個(gè)NSManagedObject子類然后把值直接設(shè)置到屬性上(沒(méi)有了動(dòng)態(tài)setValueForKey的開銷),速度可能更快。

所以正如你所見,使用反射使創(chuàng)建NSManagedObject的性能下降了3.5倍。當(dāng)你在數(shù)量有限的項(xiàng)目上使用這個(gè)方法,或者你不關(guān)心處理速度時(shí),這是沒(méi)問(wèn)題的。但是當(dāng)你需要反射大量的struct時(shí),這個(gè)方法可能會(huì)大大降低你 app 的性能。

自定義Mirror

我們之前已經(jīng)討論過(guò),創(chuàng)建Mirror還有其他的選項(xiàng)。這些選項(xiàng)是非常有用的,比如,你想自己定義mirror中對(duì)象的哪些部分是可訪問(wèn)的。對(duì)于這種情況Mirror Struct提供了其他的構(gòu)造器。

Collection

第一個(gè)特殊init是為Collection量身定做的:

publicinit

(_subject:T, children:C,

displayStyle:Mirror.DisplayStyle? =default,

ancestorRepresentation:Mirror.AncestorRepresentation=default)

與之前的init(reflecting:)相比,這個(gè)構(gòu)造器允許我們定義更多反射處理的細(xì)節(jié)。

它只對(duì)Collection有效

我們可以設(shè)定被反射的對(duì)象以及對(duì)象的children(Collection的內(nèi)容)

class或者struct

第二個(gè)可以在class或者struct上使用。

publicinit(_subject:T,

children:DictionaryLiteral,

displayStyle:Mirror.DisplayStyle? =default,

ancestorRepresentation:Mirror.AncestorRepresentation=default)

有意思的是,這里是由你指定對(duì)象的children(即屬性),指定的方式是通過(guò)一個(gè)DictionaryLiteral,它有點(diǎn)像字典,可以直接用作函數(shù)參數(shù)。如果我們?yōu)锽ookmark struct實(shí)現(xiàn)這個(gè)構(gòu)造器,它看起來(lái)是這樣的:

extensionBookmark:CustomReflectable{

funccustomMirror()->Mirror{// [譯者注] 此處應(yīng)該為 public func customMirror() -> Mirror {

letchildren =DictionaryLiteral(dictionaryLiteral:

("title",self.title), ("pagerank",self.pagerank),

("url",self.url), ("created",self.created),

("keywords",self.keywords), ("group",self.group))

returnMirror.init(Bookmark.self, children: children,

displayStyle:Mirror.DisplayStyle.Struct,

ancestorRepresentation:.Suppressed)

}

}

如果現(xiàn)在我們做另外一個(gè)性能測(cè)試,會(huì)發(fā)現(xiàn)性能甚至略微有所提升:

創(chuàng)建2000個(gè)NSManagedObject

原生:0.062seconds

反射:0.207seconds

反射:0.203seconds

但這個(gè)工作幾乎沒(méi)有任何價(jià)值,因?yàn)樗c我們之前反射struct成員變量的初衷是相違背的。

用例

所以留下來(lái)讓我們思考的問(wèn)題是什么呢?好的反射用例又是什么呢?很顯然,如果你在很多NSManagedObject上使用反射,它會(huì)大大降低你代碼的性能。同時(shí)如果只有一個(gè)或者兩個(gè)struct,根據(jù)自己掌握的struct領(lǐng)域的知識(shí)編寫一個(gè)序列化的方法會(huì)更容易,更高性能且更不容易讓人困惑。

而本文展示反射技巧可以當(dāng)你在有很多復(fù)雜的struct,且偶爾想對(duì)它們中的一部分進(jìn)行存儲(chǔ)時(shí)使用。

例子如下:

設(shè)置收藏夾

收藏書簽

加星

記住上一次選擇

在重新啟動(dòng)時(shí)存儲(chǔ)AST打開的項(xiàng)目

在特殊處理時(shí)做臨時(shí)存儲(chǔ)

當(dāng)然除此之外,反射當(dāng)然還有其他的使用場(chǎng)景:

遍歷tuples

對(duì)類做分析

運(yùn)行時(shí)分析對(duì)象的一致性

自動(dòng)生成詳細(xì)日志 / 調(diào)試信息(即外部生成對(duì)象)

討論

反射 API 主要做為Playground的一個(gè)工具。符合反射 API 的對(duì)象可以很輕松滴就在Playground的側(cè)邊欄中以分層的方式展示出來(lái)。因此,盡管它的性能不是最優(yōu)的,在Playground之外仍然有很多有趣的應(yīng)用場(chǎng)景,這些應(yīng)用場(chǎng)景我們?cè)?b>用例章節(jié)中都講解過(guò)。

更多信息

反射 API 的源文件注釋非常詳細(xì),我強(qiáng)烈建議每個(gè)人都去看看。

同時(shí),GitHub 上的CoreValue項(xiàng)目展示了關(guān)于這個(gè)技術(shù)更詳盡的實(shí)現(xiàn),它可以讓你很輕松滴把struct編碼成CoreData,或者把CoreData解碼成struct。

1、實(shí)際上,Any是一個(gè)空的協(xié)議,所有的東西都隱式滴符合這個(gè)協(xié)議。

2、更確切地說(shuō),是一個(gè)空的可選類型。

3、我對(duì)注釋稍微做了簡(jiǎn)化。

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

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

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