Dart学习笔记(41):Reflection 反射

发表于2018-07-04 01:29 阅读(71)

在Dart中,Reflection 是基于 Mirror 的概念,它仅是反映其它对象的对象。在基于 Mirror 的API中,无论什么时候,在你想反射一个实体时,你必须得到一个被称为 Mirror 的单独的对象。

在安全、分发和部署方面,Mirror-based 的反射API有很大的优势。但另一方面,有时候使用反射比旧的方法更冗长。

原文地址:http://www.cndartlang.com/987.html

关于基于Mirror的反射的基本原理更深入的介绍,参见本文末尾的引用。当然,你也完全不需要深入研究。本文涵盖了你真正需要了解的、关于Dart中 Mirror API 的内容。

告诫1:Dart的 Mirror API 正在不断发展中。虽然大多内省API是Stable稳定版,但将来仍会有一些补充和调整。

提示:内省(Introspection),通过分析类的设计模式以揭示类的属性、方法的自动化过程。简单的说,也就是通过反射的方式访问类的技术。

此时此刻,原计划的API仅有部分被实现。但存在的部分仍可以处理内省,程序拥有可以发现和使用它自己的结构的能力。很大程度上,Dart VM 中内省API已经被实现。在 dart2js 中,类似的实现就是在此之上发展而来,但仍然是不完整的。

内省API被定义在 dart:mirrors 中,如果你想使用内省,可以直接引入:

import 'dart:mirrors';

为方便说明起见,假设你已经定义了如下类:

class MyClass {
  int i, j;
  int sum() => i + j;

  MyClass(this.i, this.j);

  static noise() => 42;

  static var s;
}

最简单的获取Mirror的方式是调用顶层函数 reflect() 。

告诫2:目前,仅在反射代码和被反射的对象运行在同一Isolate中时,才能运行反射。预计将来能扩展API以支持跨Isolate进行反射。

reflect() 方法接受一个对象,并返回 InstanceMirror :

InstanceMirror myClassInstanceMirror = reflect(new MyClass(3, 4));

InstanceMirror 是 Mirror 的子类,而 Mirror 则是反射层次结构的根(root)。InstanceMirror 允许动态选择对象上动态代码,并调用。

InstanceMirror f = myClassInstanceMirror.invoke(#sum, []);
// 返回关于7的InstanceMirror

invoke() 方法接受一个用来表示方法名称的Symbol符号(在上面的例子中,是#sum),位置参数的List,以及(可选)表示命名参数的Map。

为什么 invoke() 不使用字符串表示方法名称?因为缩小(Minification,不同于 Compression 压缩)代码。缩小是一个处理过程,目的是重命名Web程序中的名称,以减少下载大小。

因为缩小的原因,为了帮助 Reflection 运行,Symbol 被引入Dart。Symbol 最大的优势是当Dart程序被缩小时,Symbol 也被缩小。你可以在 Symbol 和 String 之间进行转换。

假设你想打印类中的所有声明,需要使用 ClassMirror,它是你希望反射的类。获取一个类的Mirror的方法之一是从实例的 Mirror 获取。

ClassMirror MyClassMirror = myClassInstanceMirror.type; // 反射MyClass

另一个方法是使用顶层函数 reflectClass() 。

ClassMirror cm = reflectClass(MyClass); // 反射MyClass

无论什么方式,如果我们已经获得了一个类的反射 cm ,我们可以打印 cm 反射的类所有声明的名称。

for (var m in cm.declarations.values) 
  print(MirrorSystem.getName(m.simpleName));

ClassMirror 有一个 getter 方法 declarations ,将返回被反射类的声明的名称(Symbol)映射到声明的 Mirror(DeclarationMirror)的 Map 。Map 包含所有显式地在类的源码中列出的声明:字段(Field)和方法(Method,包括getter、setter 和常规方法),无论它们是静态或不是,以及各类构造函数。Map 并不会包含继承的成员,以及隐式成员,如字段自动生成的 getter 和 setter 方法。

我们从 Map 中提取值的时候,每个值都是关于 MyClass 的声明的 Mirror 之一,并且支持 getter 方法 simpleName ,它将返回声明的名称。但是返回的名称是 Symbol ,因此我们必须转换成 String 以打印名称。静态方法 MirrorSystem.getName 已经为我们做了这些工作。

显然,在这种情况下,我们知道 MyClass 中的所有声明。因此,我们可以使用它打印任何类的声明。

printAllDeclarationsOf(ClassMirror cm) {
  for (var m in cm.declarations.values) 
    print(MirrorSystem.getName(m.simpleName));
}

在 Mirror API 中,许多方法以类似的方式返回 Map 。Map 允许你通过名称查找成员,迭代所有的名称,或者迭代所有的成员。事实上,对于刚才所写的代码,有更简单的方法来达到目的。

printAllDeclarationsOf(ClassMirror cm) {
  for (var k in cm.declarations.keys) 
    print(MirrorSystem.getName(k));
}

如果想调用静态代码,也可以调用 ClassMirror 的 invoke() 方法。

cm.invoke(#noise, []); // 返回关于42的InstanceMirror

事实上,invoke() 被定义在 ObjectMirror 类中,它实现了 Mirror 接口,是所有具有状态和可执行代码(如常规实例、Class、库等等)反射类的共有父类。

下面是一个完整的例子,包含截止目前所有的代码:

import 'dart:mirrors';

class MyClass {
  int i, j;
  void my_method() {  }

  int sum() => i + j;

  MyClass(this.i, this.j);

  static noise() => 42;

  static var s;
}

main() {
  MyClass myClass = new MyClass(3, 4);
  InstanceMirror myClassInstanceMirror = reflect(myClass);

  ClassMirror MyClassMirror = myClassInstanceMirror.type;

  InstanceMirror res = myClassInstanceMirror.invoke(#sum, []);
  print('sum = ${res.reflectee}');

  var f = MyClassMirror.invoke(#noise, []);
  print('noise = $f');

  print('\nMethods:');
  Iterable<DeclarationMirror> decls =
      MyClassMirror.declarations.values.where(
        (dm) => dm is MethodMirror && dm.isRegularMethod);
  decls.forEach((MethodMirror mm) {
    print(MirrorSystem.getName(mm.simpleName));
  });

  print('\nAll declarations:');
  for (var k in MyClassMirror.declarations.keys) {
    print(MirrorSystem.getName(k));
  }

  MyClassMirror.setField(#s, 91);
  print(MyClass.s);
}

输出结果:

sum = 7
noise = InstanceMirror on 42

Methods:
my_method

sum

noise

All declarations:
i
j
s
my_method

sum
noise
MyClass
91

此时此刻,所说明的内容已经足够你入门了。但是下面更多的内容,你应该要注意到。

告诫3:对于Web应用,你部署的东西往往比你写的东西少。这可能与反射相互影响,很烦人。

因为需要控制Web应用的大小,部署的Dart应用可能被缩小(Minification)和 Tree shaking 优化。我们在上面讨论了缩小,而 Tree shaking 是指消除未调用的源代码。这两步通常并不能检测到代码中是否使用了反射。

在Dart中,这样的优化无可争辩,因为绝大多数情况需要部署为JavaScript。对于每个以Dart编写的Web页面,我们需要避免下载全部的、完整的Dart平台,因为部分库、类或代码可能并未使用。而 Tree shaking 通过检测源码中方法名称是否确实被调用,来达到这样的目的。然而,对于基于动态计算 Symbol 而被调用的代码,并不能被检测,因此会被消除。

上述意味着,运行时存在的实际代码,可能与开发过程中的代码不同。你使用反射的代码可能不会被部署。这可能会导致意外发生。例如,可能你试图反射调用一个源码中存在的方法,但因为没有非反射的调用存在,已经被优化消除。这样的调用将导致 noSuchMethod() 被调用。Tree shaking 对于结构上的内省同样如此,库或类型的成员在运行时可能与源码不相符。

在使用 Mirror 的时候,可以选择更加保守的做法。遗憾的是,应用中的任何对象都可能包含 Mirror ,所有应用中的代码不得不被保存,包括 Dart 平台本身。否则,在处理这样的的调用时,就会出现“似乎方法并未存在于源码中”。

官方正在针对程序员试验相关的机制:指定某些代码不会被 Tree shaking 消除。目前,对该目的你可以使用 MirrorsUsed 注解,但是预计详细的内容会随时间发生明显变化。

告诫4:有一件事可以保证,dart:mirrors 库还在开发中,MirrorsUsed 也将会发生变化。如果你使用该注解,需对破坏性的变化有所准备。

提示:由此可知,MirrorsUsed 的目的并不是解决 dart2js 生成的 JavaScript 文件过大的问题,部分中文文档对此存在误解。

内省API还有很多内容,你可以研究一下API。在未来,Dart将支持更强大的反射功能,包括 mirror builder,被设计为允许程序扩展和修改它自身,以及基于 Mirror 的 Debugging API。