Swift反射API及其用法

Swift反射API及其用法

尽管 Swift 一直在强调强类型、编译时安全和静态调度,但它的标准库仍然提供了反射机制。可能你已经在很多博客文章或者类似Tuples、Midi Packets和Core Data的项目中见过它。也许你刚好对在项目中使用反射机制感兴趣,或者你想更好的了解反射可以应用的领域,那这篇文章就正是你需要的。文章的内容是基于我在德国法兰克福 Macoun会议上的一次演讲,它对 Swift 的反射 API 做了一个概述。

API 概述

理解这个主题最好的方式就是看 API,看它都提供了什么功能。

Mirror

Swift 的反射机制是基于一个叫Mirror的struct来实现的。你为具体的subject创建一个Mirror,然后就可以通过它查询这个对象subject。

在我们创建Mirror之前,我们先创建一个可以让我们当做对象来使用的简单数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Foundation.NSURL// [译者注]此处应该为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)

创建一个Mirror

创建Mirror最简单的方式就是使用reflecting构造器:

1
public init(reflectingsubject:Any)

然后在aBookmarkstruct上使用它:

1
2
3
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来帮助确定你想查询的信息。

第一个是DisplayStyleenum,它会告诉你对象的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
publicenumDisplayStyle{
caseStruct
caseClass
caseEnum
caseTuple
caseOptional
caseCollection
caseDictionary
caseSet
}

这些都是反射 API 的辅助类型。正如之前我们知道的,反射只要求对象是Any类型,而且Swift 标准库中还有很多类型为Any的东西没有被列举在上面的DisplayStyleenum中。如果试图反射它们中间的某一个又会发生什么呢?比如closure。

1
2
3
let closure = { (a:Int) ->Intinreturna *2}
let aMirror = Mirror(reflecting: closure)

在这种情况下,这里你会得到一个mirror,但是DisplayStyle为nil2

也有提供给Mirror的子节点使用的typealias:

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

所以每个Child都包含一个可选的label和Any类型的value。为什么label是Optional的?如果你仔细考虑下,其实这是非常有意义的,并不是所有支持反射的数据结构都包含有名字的子节点。struct会以属性的名字做为label,但是Collection只有下标,没有名字。Tuple同样也可能没有给它们的条目指定名字。

接下来是AncestorRepresentationenum3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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可用的属性 / 方法:

1
2
3
4
5
6
7
let children: Children:对象的子节点。
displayStyle: Mirror.DisplayStyle?:对象的展示风格
let subjectType: Any.Type:对象的类型
func superclassMirror() -> Mirror?:对象父类的mirror

下面我们会分别对它们进行解析。

displayStyle

很简单,它会返回DisplayStyleenum的其中一种情况。如果你想要对某种不支持的类型进行反射,你会得到一个空的Optional值(这个之前解释过)。

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

// [译者注]此处输出:Optional(Struct)

children

这会返回一个包含了对象所有的子节点的AnyForwardCollection。这些子节点不单单限于Array或者Dictionary中的条目。诸如struct或者class中所有的属性也是由AnyForwardCollection这个属性返回的子节点。AnyForwardCollection协议意味着这是一个支持遍历的Collection类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for case let(label?, value)inaMirror.children {
print(label, value)
}
//输出:
//: store main.BookmarkStore
//: title Optional("Appventure")
//: url appventure.me
//: keywords ["Swift", "iOS", "OSX"]

//: group Tech

SubjectType

这是对象的类型:

//输出 : Bookmark

1
2
3
4
5
6
7
8
9
print(Mirror(reflecting:5).subjectType)
//输出 : Int
print(Mirror(reflecting:"test").subjectType)
//输出 : String
print(Mirror(reflecting:NSNull()).subjectType)

//输出 : NSNull

然而,Swift 的文档中有下面一句话:“当self是另外一个mirror的superclassMirror()时,这个类型和对象的动态类型可能会不一样。”

SuperclassMirror

这是我们对象父类的mirror。如果这个对象不是一个类,它会是一个空的Optional值。如果对象的类型是基于类的,你会得到一个新的Mirror:

// 试试 struct

1
2
3
4
5
6
7
print(Mirror(reflecting: aBookmark).superclassMirror())
// 输出: nil
// 试试 class
print(Mirror(reflecting: aBookmark.store).superclassMirror())

// 输出: Optional(Mirror for Store)

实例

Struct转Core Data

假设我们在一个叫Books Bunny的新兴高科技公司工作,我们以浏览器插件的方式提供了一个人工智能,它可以自动分析用户访问的所有网站,然后把相关页面自动保存到书签中。

现在是 2016 年,Swift 已经开源,所以我们的后台服务端肯定是用 Swift 编写。因为在我们的系统中同时有数以百万计的网站访问活动,我们想用struct来存储用户访问网站的分析数据。不过,如果我们 AI 认定某个页面的数据是需要保存到书签中的话,我们需要使用CoreData来把这个类型的对象保存到数据库中。

现在我们不想为每个新建的struct单独写自定义的Core Data序列化代码。而是想以一种更优雅的方式来开发,从而可以让将来的所有struct都可以利用这种方式来做序列化。

那么我们该怎么做呢?

一个协议

记住,我们有一个struct,它需要自动转换为NSManagedObject(Core Data)。

如果我们想要支持不同的struct甚至类型,我们可以用协议来实现,然后确保我们需要的类型符合这个协议。所以我们假想的协议应该有哪些功能呢?

第一,协议应该允许自定义我们想要创建的Core Data 实体的名字

第二,协议需要提供一种方式来告诉它如何转换为NSManagedObject。

我们的protocol(协议) 看起来是下面这个样子的:

1
2
3
4
5
6
7
8
9
10
11
protocol StructDecoder{
// 我们 Core Data 实体的名字
static var EntityName:String{get}
// 返回包含我们属性集的 NSManagedObject
func toCoreData(context: NSManagedObjectContext)throws->NSManagedObject//[译者注]使用 NSManagedObjectContext 需要 import CoreData
}

toCoreData方法使用了 Swift 2.0 新的异常处理来抛出错误,如果转换失败,会有几种错误情况,这些情况都在下面的ErrorTypeenum进行了列举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enumSerializationError:ErrorType{
// 我们只支持 struct
caseStructRequired
// 实体在 Core Data 模型中不存在
caseUnknownEntity(name:String)
// 给定的类型不能保存在 core data 中
caseUnsupportedSubType(label:String?)
}

上面列举了三种转换时需要注意的错误情况。第一种情况是我们试图把它应用到非struct的对象上。第二种情况是我们想要创建的entity在 Core Data 模型中不存在。第三种情况是我们想要把一些不能存储在 Core Data 中的东西保存到 Core Data 中(即enum)。

让我们创建一个struct然后为其增加协议一致性:

1
2
3
4
5
6
7
8
9
10
11
12
13
Bookmark struct
struct Bookmark{
let title:String
let url:NSURL
let pagerank:Int
let created:NSDate
}

接下来,我们要实现toCoreData方法。

协议扩展

当然我们可以为每个struct都写新的toCoreData方法,但是工作量很大,因为struct不支持继承,所以我们不能使用基类的方式。不过我们可以使用protocol extension来扩展这个方法到所有相符合的struct:

1
2
3
4
5
6
7
extension StructDecoder{
func toCoreData(context: NSManagedObjectContext)throws->NSManagedObject{
}
}

因为扩展已经被应用到相符合的struct,这个方法就可以在struct的上下文中被调用。因此,在协议中,self指的是我们想分析的struct。

所以,我们需要做的第一步就是创建一个可以写入我们Bookmark struct值的NSManagedObject。我们该怎么做呢?

一点Core Data

Core Data有点啰嗦,所以如果需要创建一个对象,我们需要如下的步骤:

获得我们需要创建的实体的名字(字符串)

获取NSManagedObjectContext,然后为我们的实体创建NSEntityDescription

利用这些信息创建NSManagedObject。

实现代码如下:

// 获取 Core Data 实体的名字

1
let entityName = self.dynamicType.EntityName

// 创建实体描述

// 实体可能不存在, 所以我们使用 ‘guard let’ 来判断,如果实体

// 在我们的 core data 模型中不存在的话,我们就抛出错误

1
2
3
4
5
6
7
8
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else {
throwUnknownEntity(name: entityName) }// [译者注] UnknownEntity 为 SerializationError.UnknownEntity
// 创建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

实现反射

下一步,我们想使用反射 API 来读取bookmark对象的属性然后把它写入到NSManagedObject实例中。

// 创建 Mirror

1
let mirror = Mirror(reflecting:self)

// 确保我们是在分析一个 struct

1
guard mirror.displayStyle == .Structelse{throwSerializationError.StructRequired}

我们通过测试displayStyle属性的方式来确保这是一个struct。

所以现在我们有了一个可以让我们读取属性的Mirror,也有了一个可以用来设置属性的NSManagedObject。因为mirror提供了读取所有children的方式,所以我们可以遍历它们并保存它们的值。方式如下:

1
2
3
4
5
for case let(label?, value)inmirror.children {
managedObject.setValue(value, forKey: label)
}

太棒了!但是,如果我们试图编译它,它会失败。原因是setValueForKey需要一个AnyObject?类型的对象,而我们的children属性只返回一个(String?, Any)类型的tuple。也就是说value是Any类型但是我们需要AnyObject类型的。为了解决这个问题,我们要测试value的AnyObject协议一致性。这也意味着如果得到的属性的类型不符合AnyObject协议(比如enum),我们就可以抛出一个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let mirror = Mirror(reflecting:self)
guard mirror.displayStyle == .Struct
else {
throwSerializationError.StructRequired}
for case let(label?, anyValue)inmirror.children {
if let value = anyValueas?AnyObject{
managedObject.setValue(child, forKey: label)// [译者注] 正确代码为:managedObject.setValue(value, forKey: label)
}else{
throwSerializationError.UnsupportedSubType(label: label)
}
}

现在,只有在child是AnyObject类型的时候我们才会调用setValueForKey方法。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
extensionStructDecoder{
functoCoreData(context: NSManagedObjectContext)throws->NSManagedObject{
let entityName =self.dynamicType.EntityName
// 创建实体描述
guard let desc =NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else{throwUnknownEntity(name: entityName) }// [译者注] UnknownEntity 为 SerializationError.UnknownEntity
// 创建 NSManagedObject
let managedObject =NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
// 创建一个 Mirror
let mirror =Mirror(reflecting:self)
// 确保我们是在分析一个 struct
guard mirror.displayStyle == .Structelse{throwSerializationError.StructRequired}
for case let(label?, anyValue)inmirror.children {
if let value = anyValueas?AnyObject{
managedObject.setValue(child, forKey: label)// [译者注] 正确代码为:managedObject.setValue(value, forKey: label)
}else{
throwSerializationError.UnsupportedSubType(label: label)
}
}
returnmanagedObject
}
}

搞定,我们现在已经把struct转换为NSManagedObject了。

性能

那么,速度如何呢?这个方法可以在生产中应用么?我做了一些测试:

创建2000个NSManagedObject

原生:0.062seconds

反射:0.207seconds

这里的原生是指创建一个NSManagedObject,然后通过setValueForKey设置属性值。如果你在Core Data内创建一个NSManagedObject子类然后把值直接设置到属性上(没有了动态setValueForKey的开销),速度可能更快。

所以正如你所见,使用反射使创建NSManagedObject的性能下降了3.5倍。当你在数量有限的项目上使用这个方法,或者你不关心处理速度时,这是没问题的。但是当你需要反射大量的struct时,这个方法可能会大大降低你 app 的性能。

自定义Mirror

我们之前已经讨论过,创建Mirror还有其他的选项。这些选项是非常有用的,比如,你想自己定义mirror中对象的哪些部分是可访问的。对于这种情况Mirror Struct提供了其他的构造器。

Collection

第一个特殊init是为Collection量身定做的:

1
public init (_subject:T, children:C,displayStyle:Mirror.DisplayStyle? = default,ancestorRepresentation:Mirror.AncestorRepresentation=default)

与之前的init(reflecting:)相比,这个构造器允许我们定义更多反射处理的细节。

它只对Collection有效

我们可以设定被反射的对象以及对象的children(Collection的内容)

class或者struct

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

1
2
3
4
5
6
7
public init(_subject:T,
children:DictionaryLiteral,
displayStyle:Mirror.DisplayStyle? =default,
ancestorRepresentation:Mirror.AncestorRepresentation=default)

有意思的是,这里是由你指定对象的children(即属性),指定的方式是通过一个DictionaryLiteral,它有点像字典,可以直接用作函数参数。如果我们为Bookmark struct实现这个构造器,它看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Bookmark:CustomReflectable{
func customMirror()->Mirror{// [译者注] 此处应该为 public func customMirror() -> Mirror {
let children = DictionaryLiteral(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.062seconds

反射:0.207seconds

反射:0.203seconds

但这个工作几乎没有任何价值,因为它与我们之前反射struct成员变量的初衷是相违背的。

用例

所以留下来让我们思考的问题是什么呢?好的反射用例又是什么呢?很显然,如果你在很多NSManagedObject上使用反射,它会大大降低你代码的性能。同时如果只有一个或者两个struct,根据自己掌握的struct领域的知识编写一个序列化的方法会更容易,更高性能且更不容易让人困惑。

而本文展示反射技巧可以当你在有很多复杂的struct,且偶尔想对它们中的一部分进行存储时使用。

例子如下:

记住上一次选择

在重新启动时存储AST打开的项目

在特殊处理时做临时存储

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

遍历tuples

对类做分析

运行时分析对象的一致性

自动生成详细日志 / 调试信息(即外部生成对象)

讨论

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

更多信息

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

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

1、实际上,Any是一个空的协议,所有的东西都隐式滴符合这个协议。

2、更确切地说,是一个空的可选类型。

3、我对注释稍微做了简化。

------本文结束😘感谢阅读------