Jacinth
首发于Jacinth

Swift 中的 Reflection(反射)

前言

文章中一些例子的想法来源于我最近在做的一个小项目。

TintPoint/BeforeSetupgithub.com图标

BeforeSetup 可以让你使用「配置文件」来管理 GitHub 仓库的设置,我之后会单独写一篇文章来介绍它。

正文

虽然 Swift 4 对 reflection(反射)的支持还不是很成熟,但是通过一些小的技巧,我们现在就能用 Mirror API 来做很多事情了。

如果你还不了解什么是 reflection,推荐先阅读这个知乎问题。

如何理解计算机编程领域的反射?www.zhihu.com图标

在 Swift 4 中,reflection 是通过 Mirror Class 实现的。

假设我们有这样的一个 struct。

struct Foo {
    var x: Int
    var y: String
    func doSomething() {
        print("Hello, world!")
    }
}

使用 Mirror,我们可以做到在 runtime(运行时)取到 Foo 所有 property(实例变量)的相关信息。

let foo = Foo(x: 1, y: "Hello")
let mirror = Mirror(reflecting: foo)
for property in mirror.children {
    print("\(property.label!): \(property.value)")
}
// x: 1
// y: Hello

通过上面的代码我们就可以在 runtime 枚举所有 property 了。

但是问题是,Mirror 的 children 并不会返回任何关于 doSomething() 的信息。那有没有办法可以绕过这个限制呢?

答案当然是肯定的,在 Swift 中 function(方法)也是 first-class citizen(第一类对象),我们可以将 function 当成 property 来用。

struct Foo {
    var x: Int
    var y: String
    let doSomething = {
        print("Hello, world!")
    }
}

let foo = Foo(x: 1, y: "Hello")
let mirror = Mirror(reflecting: foo)
for property in mirror.children {
    print("\(property.label!): \(property.value)")
}

// x: 1
// y: Hello
// doSomething: (Function)

这样我们就可以取到 doSomething 这个 property 了。

但是还有一个问题,在 runtime 该如何调用它呢?我们可以使用 pattern matching(模式匹配)来判断并转换这个 property 的类型,然后就可以安全的调用它了。

for property in mirror.children {
    switch property.value {
    case let function as () -> Void:
        function()
    default:
        print("\(property.label!): \(property.value)")
    }
}

// x: 1
// y: Hello
// Hello, world!

当你这样做的时候要小心,因为 Swift 目前实现上的限制,doSomething 并不能接受任何参数,不然调用它的时候就会导致程序崩溃。

struct Foo {
    var x: Int
    var y: String
    let doSomething: (String) -> Void = { name in
        print("Hello, \(name)!")
    }
}

let foo = Foo(x: 1, y: "Hello")
let mirror = Mirror(reflecting: foo)
for property in mirror.children {
    switch property.value {
    case let function as (String) -> Void:
        function("World")
    default:
        print("\(property.label!): \(property.value)")
    }
}

// x: 1
// y: Hello
// segmentation fault

不过,我们可以使用 global variable(全局变量)或者 singleton(单例变量)来变相给 function 传递参数。下面让我们稍微提高一点难度,让我们使用 reflection 实现一个超级基础的 Command Line Arguments Parser(命令行参数解析器)。

不需要考虑各种边界情况,假设输入的数据类似于 "--token mytoken --repo TintPoint/BeforeSetup" 这样。我们希望把 "mytoken","TintPoint" 和 "BeforeSetup" 分别提取并保存到 ParsedArguments 这个 class 中。

class ParsedArguments {
    var token: String!
    var repositoryOwner: String!
    var repositoryName: String!
}

最终的代码类似这样。

class SupportedArguments {
    static var parsedArguments: ParsedArguments!
    static var nextArgument: String!
    
    private let token = {
        parsedArguments.token = nextArgument
    }
    
    private let repo = {
        let tokens = nextArgument.split(separator: "/").map(String.init)
        parsedArguments.repositoryOwner = tokens.first
        parsedArguments.repositoryName = tokens.last
    }
}

class ArgumentsParser {
    let inputArguments: [String]
    let supportedArguments = SupportedArguments()
    let parsedArguments = ParsedArguments()
    
    init(_ string: String) {
        inputArguments = string.split(separator: " ").map(String.init)
    }
    
    func processArguments() {
        SupportedArguments.parsedArguments = parsedArguments
        for case (let label?, let function as () -> Void) in Mirror(reflecting: SupportedArguments()).children {
            if let index = inputArguments.index(of: "--\(label.lowercased())"), inputArguments.indices.contains(index + 1) {
                SupportedArguments.nextArgument = inputArguments[index + 1]
                function()
                SupportedArguments.nextArgument = nil
            }
        }
        SupportedArguments.parsedArguments = nil
    }
}

let argumentParser = ArgumentsParser("--token mytoken --repo TintPoint/BeforeSetup")
argumentParser.processArguments()
print(argumentParser.parsedArguments.token) // mytoken
print(argumentParser.parsedArguments.repositoryOwner) // TintPoint
print(argumentParser.parsedArguments.repositoryName) // BeforeSetup

使用 reflection 的好处是,以后如果我们想要支持更多的参数(比如 "--config"),只需要修改 ParsedArguments 和 SupportedArguments 这两个 class 就够了。

class ParsedArguments {
    // ... same as above
    var configURL: URL!
}

class SupportedArguments {
    // ... same as above
    private let config = {
        parsedArguments.configURL = URL(fileURLWithPath: nextArgument)
    }
}

是不是很方便?

编辑于 2018-02-27

文章被以下专栏收录