Swift反射API及其用法

猛戳查看最終版@SwiftGG

儘管 Swift 一直在強調強類型、編譯時安全和靜態調度,但它的標準庫仍然提供了反射機制。可能你已經在很多博客文章或者類似TuplesMidi PacketsCore Data 的項目中見過它。也許你剛好對在項目中使用反射機制感興趣,或者你想更好滴了解反射可以應用的領域,那這篇文章就正是你需要的。文章的內容是基於我在德國法蘭克福 Macoun會議上的一次演講,它對 Swift 的反射 API 做了一個概述。

API 概述

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

Mirror

Swift 的反射機制是基於一個叫 Mirrorstruct 來實現的。你爲具體的 subject 創建一個 Mirror,然後就可以通過它查詢這個對象 subject

在我們創建 Mirror 之前,我們先創建一個可以讓我們當做對象來使用的簡單數據結構。

import Foundation.NSURL // [譯者注]此處應該爲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

創建 Mirror 最簡單的方式就是使用 reflecting 構造器:

public init(reflecting subject: Any)

然後在 aBookmark struct 上使用它:

let aMirror = Mirror(reflecting: aBookmark)
print(aMirror)
// 輸出 : Mirror for Bookmark

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

Mirror 結構體還有另外三個構造器,但是這三個都是在你需要自定義 mirror 這種情況下使用的。我們會在接下來討論自定義 mirror 時詳細講解這些額外的構造器

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 中。如果試圖反射它們中間的某一個又會發生什麼呢?比如 closure

let closure = { (a: Int) -> Int in return a * 2 }
let aMirror = Mirror(reflecting: closure)

這裏你會得到一個 mirror,但是 DisplayStylenil 2

也有提供給 Mirror 的子節點使用的 typealias

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

所以每個 Child 都包含一個可選的 labelAny 類型的 value。爲什麼 labelOptional 的?如果你仔細考慮下,其實這是非常有意義的,並不是所有支持反射的數據結構都包含有名字的子節點。 struct 會以屬性的名字做爲 label,但是 Collection 只有下標,沒有名字。Tuple 同樣也可能沒有給它們的條目指定名字。

接下來是 AncestorRepresentation enum 3

public enum AncestorRepresentation {
    /// 爲所有 ancestor class 生成默認 mirror。
    case Generated
    /// 使用最近的 ancestor 的 customMirror() 實現來給它創建一個 mirror。    
    case Customized(() -> Mirror)
    /// 禁用所有 ancestor class 的行爲。Mirror 的 superclassMirror() 返回值爲 nil。
    case Suppressed
}

這個 enum 用來定義被反射的對象的父類應該如何被反射。也就是說,這隻應用於 class 類型的對象。默認情況(正如你所見)下 Swift 會爲每個父類生成額外的 mirror。然而,如果你需要做更復雜的操作,你可以使用 AncestorRepresentation enum 來定義父類被反射的細節。我們會在下面的內容中進一步研究這個

如何使用一個 Mirror

現在我們有了給 Bookmark 類型的對象aBookmark 做反射的實例變量 aMirror。可以用它來做什麼呢?

下面列舉了 Mirror 可用的屬性 / 方法:

  • let children: Children:對象的子節點。
  • displayStyle: Mirror.DisplayStyle?:對象的展示風格
  • let subjectType: Any.Type:對象的類型
  • func superclassMirror() -> Mirror?:對象父類的 mirror

下面我們會分別對它們進行解析。

displayStyle

很簡單,它會返回 DisplayStyle enum 的其中一種情況。如果你想要對某種不支持的類型進行反射,你會得到一個空的 Optional 值(這個之前解釋過)。

print (aMirror.displayStyle)
// 輸出: Optional(Swift.Mirror.DisplayStyle.Struct)
// [譯者注]此處輸出:Optional(Struct)

children

這會返回一個包含了對象所有的子節點的 AnyForwardCollection<Child>。這些子節點不單單限於 Array 或者 Dictionary 中的條目。諸如 struct 或者 class 中所有的屬性也是由 AnyForwardCollection<Child> 這個屬性返回的子節點。AnyForwardCollection 協議意味着這是一個支持遍歷的 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 的文檔中有下面一句話:

“當 self 是另外一個 mirrorsuperclassMirror() 時,這個類型和對象的動態類型可能會不一樣“

SuperclassMirror

這是我們對象父類的 mirror。如果這個對象不是一個類,它會是一個空的 Optional 值。如果對象的類型是基於類的,你會得到一個新的 Mirror

// 試試 struct
print(Mirror(reflecting: aBookmark).superclassMirror())
// 輸出: nil
// 試試 class
print(Mirror(reflecting: aBookmark.store).superclassMirror())
// 輸出: Optional(Mirror for Store)

實例

StructCore Data

假設我們在一個叫 Books Bunny 的新興高科技公司工作,我們以瀏覽器插件的方式提供了一個人工智能,它可以自動分析用戶訪問的所有網站,然後把相關頁面自動保存到書籤中。

現在是 2016 年,Swift 已經開源,所以我們的後臺服務端肯定是用 Swift 編寫。因爲在我們的系統中同時有數以百萬計的網站訪問活動,我們想用 struct 來存儲用戶訪問網站的分析數據。不過,如果我們 AI 認定某個頁面的數據是需要保存到書籤中的話,我們需要使用 CoreData 來把這個類型的對象保存到數據庫中。

現在我們不想爲每個新建的 struct 單獨寫自定義的 Core Data 序列化代碼。而是想以一種更優雅的方式來開發,從而可以讓將來的所有 struct 都可以利用這種方式來做序列化。

那麼我們該怎麼做呢?

協議

記住,我們有一個 struct,它需要自動轉換爲 NSManagedObjectCore Data)。

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

  • 第一,協議應該允許自定義我們想要創建的Core Data 實體的名字
  • 第二,協議需要提供一種方式來告訴它如何轉換爲 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 新的異常處理來拋出錯誤,如果轉換失敗,會有幾種錯誤情況,這些情況都在下面的 ErrorType enum 進行了列舉:

enum SerializationError: ErrorType {
    // 我們只支持 struct
    case StructRequired
    // 實體在 Core Data 模型中不存在
    case UnknownEntity(name: String)
    // 給定的類型不能保存在 core data 中
    case UnsupportedSubType(label: String?)
}

上面列舉了三種轉換時需要注意的錯誤情況。第一種情況是我們試圖把它應用到非 struct 的對象上。第二種情況是我們想要創建的 entity 在 Core Data 模型中不存在。第三種情況是我們想要把一些不能存儲在 Core Data 中的東西保存到 Core Data 中(即 enum)。

讓我們創建一個 struct 然後爲其增加協議一致性:

Bookmark struct

struct Bookmark {
   let title: String
   let url: NSURL
   let pagerank: Int
   let created: NSDate
}

下一步,我們要實現 toCoreData 方法。

協議擴展

當然我們可以爲每個 struct 都寫新的 toCoreData 方法,但是工作量很大,因爲 struct 不支持繼承,所以我們不能使用基類的方式。不過我們可以使用 protocol extension 來擴展這個方法到所有相符合的 struct

extension StructDecoder {
    func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
    }
}

因爲擴展已經被應用到相符合的 struct,這個方法就可以在 struct 的上下文中被調用。因此,在協議中,self 指的是我們想分析的 struct

所以,我們需要做的第一步就是創建一個可以寫入我們 Bookmark struct 值的NSManagedObject。我們該怎麼做呢?

一點 Core Data

Core Data 有點囉嗦,所以如果需要創建一個對象,我們需要如下的步驟:

  1. 獲得我們需要創建的實體的名字(字符串)
  2. 獲取 NSManagedObjectContext,然後爲我們的實體創建 NSEntityDescription
  3. 利用這些信息創建 NSManagedObject

實現代碼如下:

// 獲取 Core Data 實體的名字
let entityName = self.dynamicType.EntityName

// 創建實體描述
// 實體可能不存在, 所以我們使用 'guard let' 來判斷,如果實體
// 在我們的 core data 模型中不存在的話,我們就拋出錯誤 
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
    else { throw UnknownEntity(name: entityName) } // [譯者注] UnknownEntity 爲 SerializationError.UnknownEntity

// 創建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

實現反射

下一步,我們想使用反射 API 來讀取 bookmark 對象的屬性然後把它寫入到 NSManagedObject 實例中。

// 創建 Mirror
let mirror = Mirror(reflecting: self)

// 確保我們是在分析一個 struct
guard mirror.displayStyle == .Struct else { throw SerializationError.StructRequired }

我們通過測試 displayStyle 屬性的方式來確保這是一個 struct

所以現在我們有了一個可以讓我們讀取屬性的 Mirror,也有了一個可以用來設置屬性的 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 協議一致性。這也意味着如果得到的屬性的類型不符合 AnyObject 協議(比如 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)
    }
}

現在,只有在 childAnyObject 類型的時候我們纔會調用 setValueForKey 方法。

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

extension StructDecoder {
    func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
    let entityName = self.dynamicType.EntityName

    // 創建實體描述
    guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
        else { throw UnknownEntity(name: entityName) } // [譯者注] UnknownEntity 爲 SerializationError.UnknownEntity

    // 創建 NSManagedObject
    let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

    // 創建一個 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
    }
}

搞定,我們現在已經把 struct 轉換爲 NSManagedObject 了。

性能

那麼,速度如何呢?這個方法可以在生產中應用麼?我做了一些測試:

創建 2000 個 NSManagedObject
原生: 0.062 seconds
反射: 0.207 seconds

這裏的原生是指創建一個 NSManagedObject,然後通過 setValueForKey 設置屬性值。如果你在 Core Data 內創建一個 NSManagedObject 子類然後把值直接設置到屬性上(沒有了動態 setValueForKey 的開銷),速度可能更快。

所以正如你所見,使用反射使創建 NSManagedObject 的性能下降了3.5倍。當你在數量有限的項目上使用這個方法,或者你不關心處理速度時,這是沒問題的。但是當你需要反射大量的 struct 時,這個方法可能會大大降低你 app 的性能。

自定義 Mirror

我們之前已經討論過,創建 Mirror 還有其他的選項。這些選項是非常有用的,比如,你想自己定義 mirror對象的哪些部分是可訪問的。對於這種情況 Mirror Struct 提供了其他的構造器。

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:) 相比,這個構造器允許我們定義更多反射處理的細節。

  • 它只對 Collection 有效
  • 我們可以設定被反射的對象以及對象的 childrenCollection 的內容)

class 或者 struct

第二個可以在 class 或者 struct 上使用。

public init<T>(_ subject: T, 
  children: DictionaryLiteral<String, Any>, 
  displayStyle: Mirror.DisplayStyle? = default, 
  ancestorRepresentation: Mirror.AncestorRepresentation = default)

有意思的是,這裏是由你指定對象的 children (即屬性),指定的方式是通過一個 DictionaryLiteral,它有點像字典,可以直接用作函數參數。如果我們爲 Bookmark struct 實現這個構造器,它看起來是這樣的:

extension Bookmark: CustomReflectable {
    func customMirror() -> Mirror { // [譯者注] 此處應該爲 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)
    }
}

如果現在我們做另外一個性能測試,會發現性能甚至略微有所提升:

創建 2000 個 NSManagedObject
原生: 0.062 seconds
反射: 0.207 seconds
反射: 0.203 seconds

但這個工作幾乎沒有任何價值,因爲它與我們之前反射 struct 成員變量的初衷是相違背的。

用例

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

而本文展示反射技巧可以當你在有很多複雜的 struct ,且偶爾想對它們中的一部分進行存儲時使用。

例子如下:

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

除了這些,反射當然還有其他的使用場景:

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

討論

反射 API 主要做爲 Playground 的一個工具。符合反射 API 的對象可以很輕鬆滴就在 Playground 的側邊欄中以分層的方式展示出來。儘管它的性能不是最優的,在 Playground 之外仍然有很多有趣的應用場景,這些應用場景我們在用例章節中都講解過。

更多信息

反射 API 的源文件註釋非常詳細,我強烈建議每個人都去看看。

同時,GitHub 上的 CoreValue 項目展示了關於這個技術更詳盡的實現,它可以讓你很輕鬆滴把 struct 編碼成 CoreData,或者把 CoreData 解碼成 struct

1、實際上,Any 是一個空的協議,所有的東西都隱式滴符合這個協議。
2、更確切地說,是一個空的可選類型。
3、我對註釋稍微做了簡化。

附:
文章可執行代碼工程地址

發佈了61 篇原創文章 · 獲贊 167 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章