MENU

Swift 中类型反射

February 2, 2020 • iOS,Swift

Swift 中类型反射

反射是一个很常见的编程语言特性,它使我们能够在运行时动态获取变量的相关属性。虽然官方在 Swift 语言在设计时就非常注重编译期的类型验证,但依旧提供了这一强大的语言特性。只不过 Swift 的反射支持没有 OC 那么强大,它仅能对实例的存储属性进行反射操作。下面我们通过一个常见场景来看看 Swift 中类型反射的应用。

假设应用使用 UserSession 这个类处理用户登录态相关的内容,包含用户凭证、本地缓存的用户内容、用户设置项:

class UserSession {
    let credentials: CredentialsStorage
    let favorites: FavoritesStorage
    let settings: SettingsStorage
}

在用户进行 logout 退出操作时,客户端大多数情况下会需要对这些信息进行清理,从而保证下次登陆操作时客户端处于一个 init 初始态。

extension UserSession {
    func logOut() {
        credentials.reset()
        favorites.reset()
        settings.reset()
    }
}

这种写法虽然没啥毛病,但是并不够优雅。而且如果后续 UserSession 用户相关的属性不断增加那么 logout 里面可能就会存在遗漏,导致 APP 的状态没有被完全彻底的重制。这个时候我们就可以使用反射特性来处理了。

首先这里我们只关注 reset 操作,所以可以抽象出一个协议并且让所有存储属性遵循。

protocol Resettable {
    func reset()
}

extension CredentialsStorage: Resettable {}
extension FavoritesStorage: Resettable {}
extension SettingsStorage: Resettable {}

这样我们就可以在反射遍历所有存储属性时统一调用 reset 操作了。

Swift 中反射特性都是通过 Mirror API 进行操作的,该类型实例包含一个 children 属性(对应类型所有的存储属性),而 children 中每一个 Child 类型实例都包含属性名 label 和属性值 value 两个信息。所以我们的 logout 代码可以改写为

extension UserSession {
    func logOut() {
        let mirror = Mirror(reflecting: self)

        for child in mirror.children {
            if let resettable = child.value as? Resettable {
                resettable.reset()
            }
        }
    }
}

如果后续 UserSession 在添加其他需要重制操作的属性时,我们只需要让该类型属性遵循 Resettable 协议就好,反射操作会自动在退出时将其重制。

按照这个样式我们可以对很多类似情形进行反射应用:

extension DataController {
    func preload() {
        let mirror = Mirror(reflecting: self)

        for child in mirror.children {
            if let resettable = child.value as? Preloadable {
                resettable.child.preload()
            }
        }
    }
}

extension CacheController {
    func warmUp() {
        let mirror = Mirror(reflecting: self)

        for child in mirror.children {
            if let resettable = child.value as? Cache {
                resettable.child.preload()
            }
        }
    }
}

上诉代码样式看起来非常类似,我们可以使用 Swift 的范性支持对代码进一步压缩。

extension Mirror {
    static func reflectProperties<T>(
        of target: Any,
        matchingType type: T.Type = T.self,
        using closure: (T) -> Void
    ) {
        let mirror = Mirror(reflecting: target)

        for child in mirror.children {
            (child.value as? T).map(closure)
        }
    }
}

通过 Mirror 拓展的类方法,我们就能支持任意类型对象的满足 matchingType 类型的存储属性进行 closure 操作。那么上诉代码就简化为了:

extension UserSession {
    func logOut() {
        // matchingType 为  Resettable
        Mirror.reflectProperties(of: self) {
            (child: Resettable) in
            child.reset()
        }
    }
}

extension DataController {
    func preload() {
        // matchingType 为  Preloadable
        Mirror.reflectProperties(of: self) {
            (child: Preloadable) in
            child.preload()
        }
    }
}

extension CacheController {
    func warmUp() {
        // matchingType 为  Cache
        Mirror.reflectProperties(of: self) {
            (child: Cache) in
            child.warmUp()
        }
    }
}

上面我们只对一级存储属性进行了反射操作,如果存储属性本身也需要支持反射该怎么办呢?也就是说如何让 Mirror 支持反射递归?例如,我们如何右眼的实现 SettingsStorage 中语言设置的重制呢?

class SettingsStorage {
    let preferredLanguages: PreferredLanguagesStorage
}

处理方式首先依旧是让 PreferredLanguagesStorage 遵循 Resettable

extension PreferredLanguagesStorage: Resettable {}

然后我们对上面拓展的 reflectProperties 进行改写:

extension Mirror {
    static func reflectProperties<T>(
        of target: Any,
        matchingType type: T.Type = T.self,
        recursively: Bool = false,
        using closure: (T) -> Void
    ) {
        let mirror = Mirror(reflecting: target)

        for child in mirror.children {
            (child.value as? T).map(closure)

            if recursively {
                Mirror.reflectProperties(
                    of: child.value,
                    recursively: true,
                    using: closure
                )
            }
        }
    }
}

通过递归参数 recursively 来实现对每一个 T 类型的 child 进行递归调用 reflectProperties 的控制。

总结

虽然反射在 Swift 中非常有用(尤其是配合 OPP 时),但其当前官方实现仍有一定的局限性。首先它只支持存储属性,其次这些属性还只能进行读操作无法修改属性的值。也就是说我们无法像 OC 里面那样,获取 property list 然后进行动态赋值操作。另外,就如其他动态语言一样反射用了太多会对代码的可读性和调试带来一定的困难,所以我们在使用 Swift 反射特性时需要考虑清楚当前场景是否合适,或者说我们在什么场景下应该避免使用反射。