原文地址:www.dartcn.com/articles/se…
原文作者:
发布时间:2012年11月 (2013年11月更新)
注意:这篇文章只适用于1.x Dart SDK下的独立虚拟机。我们不建议在网络应用中使用镜像,而且Flutter SDK不支持dart:mirrors库。
Dart中的反射是基于镜像的概念,它是简单的反映其他对象的对象。在一个基于镜像的API中,每当人们想对一个实体进行反射时,必须获得一个单独的对象,称为镜像。
基于镜像的反射式API在安全、分发和部署方面有很大的优势。另一方面,使用它们有时会比旧的方法更加繁琐。
关于基于镜像的反射的原理的全面介绍,请参见本文末尾的参考文献。然而,如果你不想的话,你不需要深入研究这些,你真正需要知道的关于Dart的镜像API将在这里涵盖。
注意事项1:Dart的镜像API是不断发展的;虽然大部分自省API是稳定的,但在未来会有一些补充和调整,甚至在1.0之后。
目前,只有部分计划中的API已经实现。现有的部分是关于自省的,即程序发现和使用其自身结构的能力。自省API已经在Dart虚拟机上基本实现。
自省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;
}
复制代码
获得镜像的最简单方法是调用顶层函数reflect()。
注意事项2:目前,只有当反射代码和被反射的对象在同一个隔离区内运行时,反射才能发挥作用。在未来,我们希望扩展API以支持跨隔离区的反射。
reflect()方法接收一个对象并返回其上的InstanceMirror。
InstanceMirror myClassInstanceMirror = reflect(new MyClass(3, 4));
复制代码
InstanceMirror是Mirror的一个子类,是镜像层次结构的根。一个InstanceMirror允许人们在一个对象上调用动态选择的代码。
InstanceMirror f = myClassInstanceMirror.invoke(#sum, []);
// Returns an InstanceMirror on 7.
复制代码
invoke()方法接收一个代表方法名称的符号(在本例中是#sum),一个位置参数列表,以及(可选)一个描述命名参数的映射。
为什么invoke()不接受一个代表方法名的字符串?因为最小化。最小化是指为了减少下载量而对网络程序中的名称进行处理的过程。
符号被引入Dart,以帮助反射在最小化的情况下工作。符号的最大优势是,当Dart程序被最小化时,符号也会被最小化。由于这个原因,镜像API使用的是符号而不是字符串。你可以在符号和字符串之间进行转换;通常,你这样做是为了打印出声明的名称,我们将在下面看到。
假设你想打印出一个类中的所有声明。你将需要一个ClassMirror,正如你所期望的那样,它能反映一个类。获取类镜像的一种方法是从实例镜像中获取。
ClassMirror MyClassMirror = myClassInstanceMirror.type; // Reflects MyClass
复制代码
另一种方法是使用顶层函数reflectClass()。
ClassMirror cm = reflectClass(MyClass); // Reflects MyClass
复制代码
一旦我们通过各种方式获得了一个类镜像cm,我们就可以打印出cm所反映的类的所有声明的名称。
for (var m in cm.declarations.values) print(MirrorSystem.getName(m.simpleName));
复制代码
ClassMirror有一个getter declarations,返回一个从被反射类的声明名称到这些声明的镜像的地图。该地图包含了该类源代码中明确列出的所有声明:它的字段和方法(包括getters、setters和常规方法),不管它们是否是静态的,以及所有条形的构造函数。该地图不包含任何继承的成员,也不包含任何合成的成员,例如为字段自动生成的getters和setters。
我们从地图中提取值;每个值都将是MyClass的一个声明的镜像,并支持返回声明名称的getter simpleName。返回的名称是一个符号,所以我们必须将其转换为字符串,以便打印。静态方法MirrorSystem.getName为我们做到了这一点。
显然,在这种情况下,我们知道MyClass中的声明是什么;重点是,上面的for循环对任何类的镜像都有效,因此我们可以用它来打印任何类的声明。
printAllDeclarationsOf(ClassMirror cm) {
for (var m in cm.declarations.values) print(MirrorSystem.getName(m.simpleName));
}
复制代码
镜像API中的一些方法以类似方式返回地图。这些地图允许你按名称查找成员,遍历所有名称,或遍历所有成员。事实上,有一个更简单的方法来完成我们刚刚做的事情。
printAllDeclarationsOf(ClassMirror cm) {
for (var k in cm.declarations.keys) print(MirrorSystem.getName(k));
}
复制代码
如果我们想反射性地调用静态代码怎么办?我们也可以在ClassMirror上调用invoke()。
cm.invoke(#noise, []); // Returns an InstanceMirror on 42
复制代码
事实上,invoke()被定义在ObjectMirror类中,这是一个常见的镜像类的超类,它反映了具有状态和可执行代码的Dart实体,如常规的实例、类、库等。
下面是一个完整的例子,包含了我们到目前为止所做的事情。
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:你部署的东西往往比你写的东西少。这可能会以恼人的方式与反射发生交互。
由于网络应用程序的大小需要被控制,部署的Dart应用程序可能会被最小化和树状晃动。我们在上面讨论了最小化;摇树是指消除不被调用的源代码。这两个步骤一般都不能检测到代码的反射性使用。
这种优化在Dart中是一个事实,因为需要部署到JavaScript。我们需要避免在每个用Dart编写的网页上下载整个Dart平台。Tree shaking通过检测源代码中实际调用的方法名来做到这一点。然而,基于动态计算的符号而调用的代码无法通过这种方式被检测到,因此会被淘汰。
上述情况意味着,在运行时存在的实际代码可能与你在开发时的代码不同。你只用反射的代码可能不会被部署。运行时反射只知道运行时在运行程序中实际存在的东西。这可能会导致意外的发生。例如,人们可能试图以反射方式调用一个存在于源代码中的方法,但由于不存在非反射式的调用而被优化掉。这样的调用将导致对noSuchMethod()的调用。树的摇动对结构反省也有影响。同样,一个库或类型在运行时有哪些成员,可能与源代码中的内容不一致。
在有镜像的情况下,人们可以选择更加保守。不幸的是,由于人们可以为应用程序中的任何对象获得镜像,应用程序中的所有代码都必须被保留,包括Dart平台本身。相反,我们可以选择把这种调用当作方法在源代码中从未存在过。
我们正在试验一些机制,让程序员指定某些代码可能不会被树状摇动所消除。目前,你可以使用MirrorsUsed注解来达到这个目的,但我们预计随着时间的推移,细节会有很大的变化。
注意事项4:我们可以向你保证的是,MirrorsUsed将会改变。如果你使用它,请准备好应对突发变化。
上述内容应该足以让你开始使用镜像。自省API还有很多内容;你可以探索API,看看还有哪些内容。
我们希望在未来支持更强大的反射功能。这些功能包括镜像构建器,旨在允许程序扩展和修改自己,以及一个基于镜像的调试API。
参考文献
Gilad Bracha和David Ungar. 镜像。面向对象编程语言的元级设施的设计原则。在ACM面向对象编程、系统、语言和应用会议上,2004年10月。
Gilad Bracha. 通过镜子的语言学反思. 2010年1月在波茨坦HPI演讲的截屏。57分钟。
这些关于镜像的博文也可能被证明是有用的(而且消化起来比较不费时间)。
- Gilad Bracha. 透过黑暗的望远镜。
- Allen Wirfs-Brock. 实验JavaScript的镜像。
- Gilad Bracha. 在镜子中寻求关闭。
通过www.DeepL.com/Translator(免费版)翻译