[譯]Swift反射API及其用法

2018-06-19 15:01 更新

猛戳查看最終版@SwiftGG

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

API 概述

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

Mirror

Swift 的反射機(jī)制是基于一個(gè)叫 Mirrorstruct 來實(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)

創(chuàng)建一個(gè) 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,但是 DisplayStylenil 2

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

public typealias Child = (label: String?, value: Any)

所以每個(gè) Child 都包含一個(gè)可選的 labelAny 類型的 value。為什么 labelOptional 的?如果你仔細(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è)。

如何使用一個(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è) mirrorsuperclassMirror() 時(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ǎ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>

協(xié)議

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

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

  • 第一,協(xié)議應(yīng)該允許自定義我們想要?jiǎng)?chuàng)建的Core Data 實(shí)體的名字
  • 第二,協(xié)議需要提供一種方式來告訴它如何轉(zhuǎn)換為 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 方法。

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

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

一點(diǎn) Core Data

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

  1. 獲得我們需要?jiǎng)?chuàng)建的實(shí)體的名字(字符串)
  2. 獲取 NSManagedObjectContext,然后為我們的實(shí)體創(chuàng)建 NSEntityDescription
  3. 利用這些信息創(chuàng)建 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)

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

下一步,我們想使用反射 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——也就是說 valueAny 類型,但是我們需要 AnyObject 類型的。為了解決這個(gè)問題,我們要測試 valueAnyObject 協(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)在,只有在 childAnyObject 類型的時(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é)。

  • 它只對(duì) Collection 有效
  • 我們可以設(shè)定被反射的對(duì)象以及對(duì)象的 childrenCollection 的內(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í)使用。

例子如下:

  • 設(shè)置收藏夾
  • 收藏書簽
  • 加星
  • 記住上一次選擇
  • 在重新啟動(dòng)時(shí)存儲(chǔ)AST打開的項(xiàng)目
  • 在特殊處理時(shí)做臨時(shí)存儲(chǔ)

除了這些,反射當(dāng)然還有其他的使用場景:

  • 遍歷 tuple
  • 對(duì)類做分析
  • 運(yùn)行時(shí)分析對(duì)象的一致性
  • 自動(dòng)生成詳細(xì)日志 / 調(diào)試信息(即外部生成對(duì)象)

討論

反射 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ì)注釋稍微做了簡化。

附: 文章可執(zhí)行代碼工程地址

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)