专注、坚持

Obj-C & Swift 的类型内省与反射

2019.04.08 by kingcos
Date Notes Swift Demo
2019-04-08 首次提交 4.2 Type Introspection and Reflection

Preface

许多编程语言中都有类型内省(又称自省,Introspection)和反射(Reflection)这两个概念,本文将探讨一下 Obj-C 和 Swift 中类型内省和反射的相关概念与具体使用。

What

维基百科中对于类型内省定义如下:

In computing, type introspection is the ability of a program to examine the type or properties of an object at runtime. Some programming languages possess this capability.

Introspection should not be confused with reflection, which goes a step further and is the ability for a program to manipulate the values, meta-data, properties and/or functions of an object at runtime. Some programming languages - e.g. Java, Python and Go - also possess that capability.

—— Type introspection, Wikipedia

译:

在计算机科学中,类型内省指程序在运行时检查类型或属性的能力。一些编程语言拥有该能力。

内省不应该和反射混淆,反射更深入一步,其是一种程序在运行时制造对象的值、元数据、属性、和/或方法的能力。一些编程语言——如 Java、Python 和 Go,同样拥有该能力。

—— 类型内省,维基百科

维基百科中对于反射定义如下:

In computer science, reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.

—— Reflection (computer programming), Wikipedia

译:

在计算机科学中,反射是程序在运行时检查、内省并修改其自身结构和行为的能力。

—— 反射(计算机编程),维基百科

从以上的定义中可以得知,类型内省和反射的共同点是程序在运行时的一种能力,不同点则是,内省只是一种检查机制,而反射广义上不仅包括检查,也拥有改变编译时刻原有结构的能力。对于具体的某种编程语言,可能根本不支持类型内省和反射,也可能仅支持内省,又或者同时支持内省和反射。

Obj-C

内省

Obj-C 中的 id 类型本质是指向 Obj-C 任意对象的指针。

// objc.h
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

Person 类中定义一个参数为 id 类型的方法,并在运行时检查该参数的实际类型,以执行不同的方法。

@implementation Person
- (void)eat:(id)fruit {
    // 检查是否是 Fruit 类或其子类
    if ([fruit isKindOfClass:Fruit.class]) {
        if ([fruit isMemberOfClass:Apple.class]) {
            // 检查是否是 Apple 类
            [(Apple *)fruit tasteApple];
        } else if ([fruit isMemberOfClass:Orange.class]) {
            // 检查是否是 Orange 类
            [(Orange *)fruit tasteOrange];
        } else {
            [fruit taste];
        }
    }
}

+ (void)introspectionDemo {
    Person *person = [[Person alloc] init];
    Apple *apple = [[Apple alloc] init];

    [person eat:apple];
}
@end

// OUTPUT:
// Apple - -[Apple tasteApple]

在 Obj-C 中进行运行时检查的方法主要是 isKindOfClass: 检查对象是否为本类或其子类的类型,以及 isMemberOfClass: 只检查对象是否为本类的类型。

反射

+ (void)reflectionDemo {
    id person = [[NSClassFromString(@"Person") alloc] init];
    id orange = [[NSClassFromString(@"Orange") alloc] init];
    [person performSelector:NSSelectorFromString(@"eat:") withObject:orange];
}

// OUTPUT:
// Orange - -[Fruit tasteOrange]

由于 Obj-C 本身的运行时已经非常强大,反射的概念倒很少提及,但其实这些运行时的能力就可以被认为是反射。我们可以直接在运行时通过字符串得到类对象,进而创建实例对象,并执行方法。但这种灵活性也会带来一些风险,正如在 performSelector: 方法处,编译器会警告「PerformSelector may cause a leak because its selector is unknown」,所以 Apple 在设计 Swift 时,其实有意削减了类似 Obj-C 的动态特性。

Swift

内省

由于 Swift 和 Obj-C 能够互相桥接,Swift 中也可以访问到 Obj-C 中的类和方法等信息。Swift 中的 AnyObject 类型即是桥接了 Obj-C 中的 id 类型,代表任何 class 类型的对象实例。

class Animal {
    func play(_ toy: AnyObject) {
        // 检查是否是 Toy 或其子类
        // Type.self 可以得到类型本身
        if toy.isKind(of: Toy.self) {
            if toy.isMember(of: Ball.self) {
                // 检查是否是 Ball 类
                (toy as! Ball).playWithBall()
            } else if toy.isMember(of: Doll.self) {
                // 检查是否是 Doll 类
                (toy as! Doll).playWithDoll()
            } else {
                (toy as! Toy).playWithToy()
            }
        }
    }

    class func introspectionDemo() {
        let ball = Ball()
        let dog = Animal()

        dog.play(ball)
    }
}

// OUTPUT:
// playWithBall()

仿照 Obj-C 中的方法,桥接过来的 isKind(of: )isMember(of: ) 也可以用于运行时检查 class 类型。但其实在 Swift 中使用更多的是结构体和枚举类型,所以 Any 类型应运而出。

struct ToyCar {
    func playWithToyCar() {
        print(#function)
    }
}

class Animal {
    func play2(_ toy: Any) {
        // 检查是否是 Toy 或其子类
        if toy is Toy {
            (toy as! Toy).playWithToy()
        } else if (toy is ToyCar) {
            // 检查是否是 ToyCar 结构体类型
            (toy as! ToyCar).playWithToyCar()
        }
    }

    class func introspectionDemo2() {
        let car = ToyCar()
        let dog = Animal()

        dog.play2(car)
    }
}

// OUTPUT:
// playWithToyCar()

Swift 中的 AnyAnyObject 更加通用,可以代表任何类型的对象实例。Swift 中的 is 关键字,可以判断是否为某种结构体或枚举等类型;对于 class 类型,is 等同于 isKind(of: ) 的作用,即判断是否为本类或子类的类型。

反射

/// A type that explicitly supplies its own mirror.
///
/// You can create a mirror for any type using the `Mirror(reflecting:)`
/// initializer, but if you are not satisfied with the mirror supplied for
/// your type by default, you can make it conform to `CustomReflectable` and
/// return a custom `Mirror` instance.
public protocol CustomReflectable {

    /// The custom mirror for this instance.
    ///
    /// If this type has value semantics, the mirror should be unaffected by
    /// subsequent mutations of the instance.
    public var customMirror: Mirror { get }
}

虽然 Swift 的运行时远不如 Obj-C 强大,但 Swift 中反射设计了专门的 API,默认情况下所有类型都支持创建镜像(Mirror),开发者也可以根据需要实现 CustomReflectable 协议来自定义镜像。

struct Computer {
    var system: String
    var memorySize: Int

    let run = {
        print(#function)
    }
}

let cpt = Computer(system: "macOS", memorySize: 16)
let mirror = Mirror(reflecting: cpt)

if let displayStyle = mirror.displayStyle {
    print("mirror's style: \(displayStyle).")
}

print("mirror's properties count: \(mirror.children.count)")

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

// DEBUG OUTPUT:
// mirror's style: struct.
// mirror's properties count: 2
// system - macOS
// memorySize - 16
// Computer
// param:

通过镜像类型,可以从任意实例(包括 classstructenum 的实例)中获取到属性等信息,Mirro 中的 children 属性是 Mirror.Child 集合类型,Child 是键值元组类型:(label: String?, value: Any)。在 Swift 中,函数也是一类公民(First class),完全可以当作一种属性,因此也可以作为 children 内的信息,尝试将 value 其转换为函数类型,即可以执行(如上 function())。需要注意的是,这里的函数其实是闭包(Closure),即匿名函数,此时 #function 并不能得到函数名称。另外,含有参数的函数类型属性暂时还不能支持,在 Debug 模式下,程序没有崩溃,但参数无法打印;切换到 Release 模式下,程序将发生参数野指针错误导致崩溃「Thread 1: EXC_BAD_ACCESS」。

由此可见,反射在 Swift 中目前还比较弱,在类型安全还是运行时强大的自由选择题中,Swift 显然更加倾向于前者。

Reference