Swift 反射 API 及用法

作者:Benedikt Terhechte,原文链接,原文日期:2015-10-24
译者:mmoaay;校对:千叶知风;定稿:千叶知风

尽管 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 的性能。

<a name="custom_mirrors">

自定义 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打开的项目
  • 在特殊处理时做临时存储

当然除此之外,反射当然还有其他的使用场景:

  • 遍历 tuples
  • 对类做分析
  • 运行时分析对象的一致性
  • 自动生成详细日志 / 调试信息(即外部生成对象)

讨论

反射 API 主要做为 Playground 的一个工具。符合反射 API 的对象可以很轻松滴就在 Playground 的侧边栏中以分层的方式展示出来。因此,尽管它的性能不是最优的,在 Playground 之外仍然有很多有趣的应用场景,这些应用场景我们在用例章节中都讲解过。

更多信息

反射 API 的源文件注释非常详细,我强烈建议每个人都去看看。

同时,GitHub 上的 CoreValue 项目展示了关于这个技术更详尽的实现,它可以让你很轻松滴把 struct 编码成 CoreData,或者把 CoreData 解码成 struct

<a name="1">1、实际上,Any 是一个空的协议,所有的东西都隐式滴符合这个协议。
<a name="2">2、更确切地说,是一个空的可选类型。
<a name="3">3、我对注释稍微做了简化。

本文对应代码地址:下载

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容