[譯]Swift反射API及其用法

2018-06-19 15:01 更新

猛戳查看最終版@SwiftGG

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

API 概述

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

Mirror

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

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

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

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

所以每個 Child 都包含一個可選的 labelAny 類型的 value。為什么 labelOptional 的?如果你仔細考慮下,其實這是非常有意義的,并不是所有支持反射的數(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 是另外一個 mirrorsuperclassMirror() 時,這個類型和對象的動態(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 都可以利用這種方式來做序列化。

那么我們該怎么做呢?

協(xié)議

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

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

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

協(xié)議擴展

當(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)建一個對象,我們需要如下的步驟:

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

實現(xiàn)反射

下一步,我們想使用反射 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——也就是說 valueAny 類型,但是我們需要 AnyObject 類型的。為了解決這個問題,我們要測試 valueAnyObject 協(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)在,只有在 childAnyObject 類型的時候我們才會調(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 有效
  • 我們可以設(shè)定被反射的對象以及對象的 childrenCollection 的內(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 ,且偶爾想對它們中的一部分進行存儲時使用。

例子如下:

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

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

  • 遍歷 tuple
  • 對類做分析
  • 運行時分析對象的一致性
  • 自動生成詳細日志 / 調(diào)試信息(即外部生成對象)

討論

反射 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、我對注釋稍微做了簡化。

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號