阅读 71

[Dart翻译]可反射能力的设计

原文地址:github.com/google/refl…

原文作者:

发布时间:

本文旨在从概念上介绍我们对ReflectCapability类及其子类型的设计选择,该类是在package reflectable中的package:reflectable/capability.dart中声明的一组类。如果想了解更多的编程观点,请查阅库的文档。我们用能力这个词来指定ReflectCapability类的子类型的实例。这个类和它的子类型是在指定包reflectable的客户端在给定的上下文中对反射操作的支持程度时使用的,例如,对特定类的实例。当我们指的是导入并使用包 reflectable 的库或包含这样一个库的包时,我们使用客户端这个词。在客户端代码中使用一种或另一种能力作为类C的元数据,可以决定是否可以通过InstanceMirror在C的实例上反射式地调用一个方法。鉴于首先让包具有可反射性的一个主要原因是为了节省由不太节俭的反射种类所消耗的空间,将反射支持限制在实际需求上的能力是包设计中的一个核心点。

应该注意的是,本文中的能力概念以及与一般的可反射包有关的能力概念与操作系统研究中已知的能力概念不同,后者是关于不可伪造的授权令牌(即大而密的数字)。在这里,能力只关注做某事的能力,而不关注安全性。

背景和设计思想

为了理解本文所涉及的主题,我们需要简要地概述一下如何理解作为一个整体的可反射包。然后我们继续解释我们是如何划分支持反思的可能种类的宇宙的,这样我们就有一组反思的种类可以选择。最后我们解释如何使用能力在这些选择中进行选择,以及如何将它们应用于客户程序的特定部分。

可反射包

包reflectable是在一般的面向对象语言中支持基于镜像的内省反射的一个例子,它应该可以被理解为如此[1]。更具体地说,由包reflectable提供的反射API是从包dart:mirrors提供的API中逐字复制过来的,然后以一些方式进行修改。因此,使用dart:mirrors的代码应该与使用reflectable包的相应代码非常相似。确实存在的差异是出于两个原因。

  • 根据设计,一些在dart:mirrors中被声明为顶级函数的操作被声明为包reflectable中Reflectable类的方法,因为其子类的实例被称为reflectors,旨在扮演镜像系统的角色[1,或者搜索下面的'镜像系统'],而这些操作是镜像系统特有的。例如,dart:mirrors中的顶级函数reflect对应于两个不同的镜像系统的两个不同的方法(语义不同,所以不能合并)。

  • 有人提出了一些修改dart:mirrors API的建议。我们利用这个机会,通过对某些方法的签名进行修改,来尝试更新API。例如,InstanceMirror.invoke将返回方法调用的结果,而不是包裹它的InstanceMirror。一般来说,在镜像经常被立即丢弃的情况下,镜像操作会返回基础级别的值,而不是其镜像,而且如果需要的话,很容易创建镜像。镜像类的方法签名也以一种方式进行了修改。在dart:mirrors方法接受参数或返回涉及Symbol的结果时,package reflectable使用String。这有助于避免与minification相关的困难(minification是一种自动的、普遍的重命名程序,主要为了节省空间而应用于程序),因为String值在整个编译过程中保持不变。

  • 某些镜像增加了新的方法,这样一来,与dart:mirrors相比,包reflectable也提供了一个扩展的API。特别是,变量镜像和参数镜像支持reflectedType方法,方法镜像支持reflectedReturnType。这些方法是方法调用链的短手(也就是说,它们的工作方式分别是.type.reflectedType.reflectedType)。拥有它们的原因是中间的类镜像不需要存在,这意味着在大量的类镜像会存在的情况下,可以减少空间消耗,因为它们会在执行那些特定的方法调用链时作为中间结果出现。反射器上增加的一个方法是getInstance,它可以返回给定反射器类的典型反射器;需要这个方法是为了实现元级反射,即以编程方式浏览当前程序,以便找到合适的镜像系统。

总之,由包reflectable提供的绝大部分API与dart:mirrors提供的API相同,有关该API或一般反射的设计文档[2,3]将用于记录基本的想法和设计选择。

反射能力的设计

包reflectable中明显的新元素是它允许客户端通过使用元数据中的能力,以一种新的方式指定对反射的支持程度。这一节概述了反射能力的语义,也就是说,它们应该能够表达哪种标准。

一般来说,我们保持这样的属性:具有一个反射器(即在一个镜像系统内)的反射支持规范是单调的,也就是说,如果在给定的反射器上添加额外的规范,任何具有一定量的反射支持的程序将至少支持同样多的反射操作。换句话说,反射支持规范可以请求额外的功能,它们永远不能阻止任何反射功能被支持。因此,我们得到了一个模块化定律:程序员在浏览源代码时,如果在某个地方遇到了反射支持规范S,他总是可以相信程序中会有相应种类的反射支持。程序的其他部分仍然可以添加更多的反射支持,但它们不能撤回S所要求的功能。同样,规范是等效的,也就是说,多个规范要求相同的功能或重叠的功能集是无害的,一个特定的东西被要求一次或多次是没有区别的。

基于镜像API的能力

原则上,对反射的支持程度可以用很多方式来指定:有大量的方式来使用反射,理想情况下,客户端应该能够准确地请求对所需的支持。为了极大地简化这种可能性,并保持有用的表达能力水平,我们决定使用下面的分层作为设计的总体框架。

  • 最基本的反射支持规范只是直接处理镜像类的API,也就是说,它关注的是 "打开 "对镜像类中单个方法或小群方法的使用支持。例如,可以使用一种能力打开对InstanceMirror.invoke的支持,而另一种能力将打开ClassMirror.invoke。如果一个支持的方法被调用,它的行为就像来自dart:mirrors的相应镜像类中的相应方法一样(除了上面提到的调整,比如返回一个基值而不是它的镜像)。如果一个不支持的方法被调用,就会抛出一个异常。

  • 作为对基于API的规范的细化,我们选择将重点放在对API中特定方法的允许参数值的规范上。例如,我们可以指定一个谓词,用来过滤现有的方法名称,这样InstanceMirror.invoke就会被支持,因为它的名称符合这个谓词。一个例子是在测试中,对所有名称以...Test结尾的方法进行反射式调用可能是一个很方便的功能,而不是纯粹的静态方法,即有人必须写一个所有此类方法的集中列表,然后可以用来调用它们。

有了这些机制,就有可能在镜像和它们提供的功能方面指定对反射的支持,而不依赖于客户程序中的实际源代码。

基于反射的能力

支持反射的另一个维度是选择客户程序的哪些部分可以被反射,包括当ClassMirror反射到其中一个类时,以及当InstanceMirror反射到其中一个实例时。简而言之,这个维度是关于可用的被反射者的选择。

涵盖这种类型的规范的一般特征是对源代码元素的量化,特别是对类和其他顶层声明的量化。在这一领域,我们着重于下面列出的机制。请注意,MyReflector被假定为Reflectable的一个子类的名字,而myReflector被假定为MyReflector的一个const实例,通过规范化是其唯一的const实例。这使得我们可以用myReflector这个例子来指代反射器的一般概念,以及它的类和类似的相关声明。

  • 反射支持是通过在myReflector上调用reflect或reflectType中的一个方法开始的。我们选择省略做反射的能力(在这个意义上,这总是可能的),因为如果不支持实例的镜像,就没有理由拥有反射。相反,我们选择了获得类镜像和类似的面向源代码的镜像的能力,这也控制了执行 reflectType 的能力;这是因为拥有这些镜像在程序大小上可能是昂贵的,而且在某些情况下可能不需要。最后,我们选择省略了reflectClass方法,因为它可能被reflectType所取代,当isOriginalDeclaration为假时,接着是originalDeclaration。

  • 为类C获得反射支持的基本机制是给它附加元数据,而这个元数据必须是一个反射器,比如myReflector。类Reflectable有一个构造函数,它是const的,并接受List<ReflectCapability>类型的单个参数,还有一个构造函数,它最多接受10个ReflectCapability类型的参数(从而避免了明确使其成为一个列表的模板)。MyReflector必须有一个单一的构造函数,它是constant,并且接受零参数。因此,强制要求MyReflector在其构造函数中通过一个超初始化器传递List<ReflectCapability>,这样MyReflector的每个实例都有相同的状态,"相同的能力"。总之,这个基本机制将请求对一个类的反射支持,其级别由存储在元数据中的能力指定。

  • 反射支持规范可以是非本地的,也就是说,它可以被放置在程序中的不同位置,而不是目标类本身。当需要为库中的一个不能被编辑的类请求反射支持时,就需要这样做(它可能是预定义的,也可能是由第三方提供的,这样在更新后的修改会引起重复的维护,等等)。这个功能从可反射包的开始就被称为侧标签。它们必须作为元数据附在库包:reflectable/reflectable.dart的导入指令中。

  • 量化概括了单类规范,允许单个规范指定作为其参数的能力应适用于一组类或其他程序元素。提供量化机制是很容易的,但我们不想用丰富得令人困惑的量化机制来污染这个包,所以我们的每一个量化机制都应该是可理解的、合理的、强大的,而且它们不应该重叠。到目前为止,我们主要关注以下几种变体。

    • 应该可以通过一些查询机制来请求对一组选定的类的反射支持。明显的候选量化机制可以量化所有的超类;所有的超类型;所有的子类;给定类的所有子类型;以及名称与给定模式匹配的所有类。
    • 前面提到的量化是集中式的,因为它是基于一个规范,然后用来 "查询 "整个程序中的匹配实体。用一个分散的机制来补充这一点是很常见和有用的,程序员明确地标记一个集合的每个成员,例如,将某个标记作为元数据附加到这些成员上。这使得精确和明确地维护集合成为可能,即使在成员没有明显的共同特征,不适合集中式 "查询 "方法的情况下。一个很好的例子是,一组方法可以通过给它们加上元数据的注释来获得反射性支持;例如,我们可能希望能够反射性地调用所有标有@businessRule的方法。

值得注意的是,支持镜像上的特定方法(API相关)和支持特定目标类(reflectee相关)的机制的分离所带来的灵活性。这种分离是由于API相关的支持是通过能力在反射器类中指定的,而反射器相关的支持是通过将反射器作为元数据添加到类等顶层声明中,以及通过全局量词来指定的。特别是,我们可以使用一个在某些第三方包中声明的反射器,然后在本地指定该反射器应该为哪些类提供反射支持,因为不需要编辑反射器类本身。

我们赞同这样一种观点,即反射操作分为(a)与实例的动态行为有关的操作,以及(b)与程序的结构有关的操作;让我们把前者称为行为操作,后者称为内省操作。举个例子,使用InstanceMirror.invoke来执行被反映者的方法是一种行为操作,而使用ClassMirror.declarations来调查被反映类的实例将拥有的成员集则是一种内省操作。

这种区别的一个重要结果是,行为操作关注的是对象的实际行为,这意味着继承的方法实现与在类中声明的方法实现具有相同的地位,而类是被反映者的运行时类型。相反,内省操作关注的是源代码实体,如声明,因此一个给定的类所报告的声明并不包括继承的声明,它们必须通过明确地迭代超类链来找到。同样地,内省观点包括抽象成员声明,但在使用行为观点时,它们被忽略了。

最后,我们需要稍微阐述一下镜像系统的概念,这个术语我们已经用过几次了。如前所述,Bracha和Ungar在2004年的OOPSLA论文中建立了镜像和镜像系统的概念基础[1]。镜像系统是一组功能,它在专门的环境中为基于镜像的反射提供支持,例如,只为特定执行中的某些类或方法,而不是所有的类和所有的方法,或者只为镜像所能提供的某些功能,例如,只为实例方法的反射性调用,而不是为静态方法。典型的例子也可以是为远程调试定制的镜像系统,或者为编译时反射定制的镜像系统,但这些例子在这里不太相关。

对于包reflectable,我们需要镜像系统的概念,因为在同一个程序中使用几个不同的镜像系统是很有用的,例如,当几个类需要广泛的反射支持,而其他大量的类只需要一点点。在这种情况下,在前者中使用强大的镜像系统,在后者中使用最小的镜像系统,由于在全球范围内提高了资源的经济性,可能是值得努力的。

一些额外的复杂性必须被预料到;例如,如果我们可以为同一个对象同时获得一个 "廉价 "和一个 "强大 "的镜像,这将通过类似myCheapReflectable.reflect(o)分别为myPowerfulReflectable.reflect(o)发生。这取决于程序员如何避免要求便宜的对象做强大的事情。作为回报,整个程序可能会节省大量的空间,相比之下,如果使用单一的镜像系统,每个需要反射的类都必须携带全套的数据,以便在程序的任何地方进行最苛刻的反射。

指定反射的能力

正如本文第一节所提到的,反射能力是使用根植于package:reflectable/capability.dart中的ReflectCapability类的子类型层次结构指定的。这些类的实例被用来构建一些东西,这些东西很可能被认为是特定领域语言的抽象句法树。本节描述了如何使用这种设置来指定使用该 "特定领域语言 "的反射支持。

ReflectCapability下的子类型层次是密封的,在这个意义上,该库中有一组ReflectCapability的子类型,并且永远不应该有该类的任何其它子类型,如下所述。

作为常量值,这些类的实例显然不能有可变的状态,但其中一些确实包含常量值,如字符串或类型。能力没有方法,除了它们从Object继承的那些。总的来说,这意味着这些类的实例不能 "做任何事情",但是它们可以被用来构建不可变的树,而且可能的树的范围是固定的,因为类的集合是固定的。这使得这些树类似于抽象的语法树,我们可以从外部给这些语法树赋予一种语义。这种语义可以由解释器或翻译器来实现。所涉及的类集的密封性是必需的,因为ReflectCapability的未知子类型不会有语义,解释者和翻译者将无法处理它们。

换句话说,我们通过在特定领域的语言中建立一个表达式的表示来指定反射能力;我们把这种语言称为可反射能力语言。该语言有一个翻译器,它是实现可反射包(即代码生成器)的一个集成部分。

显然,在这种语言中可以有多种表达方式,我们考虑为它引入一种传统的、文本的语法。我们可以有一个解析器,它接受一个 String,解析它,并产生一个由 ReflectCapability 的子类型的实例组成的抽象语法树,或者报告一个语法错误。可以提供一个接受String参数的Reflectable构造函数,并且在需要时可以解析String。这将是程序员指定反射支持的一种方便(但不太安全)的方式,可以作为目前必须直接指定抽象语法树的方法的一种替代。

然而,本文档中使用文本语法只是因为它简洁易读,它还没有(也可能永远不会)被实现。因此,使用可反射能力语言的实际代码将不得不使用更粗略的形式,即直接建立一个代表该表达式的抽象语法树的对象结构。显示如何做到这一点的示例代码可以在包 test_reflectable 中找到。

在本文中,我们将从语法结构的角度来讨论这种语言,以及每个结构的非正式语义。

指定基于镜像API的能力

图1显示了可反射能力语言语法的一部分元素的原始材料。图的左边包含代表集群的抽象概念的标记,右边包含代表整个镜像API中每个方法的标记。有几个标记代表了不止一个方法(例如,所有的VariableMirror、MethodMirror和TypeVariableMirror都有一个isStatic getter,元数据也定义在两个类中),但它们被合并成一个标记,因为这些方法在它们出现的所有语境中都扮演着相同的语义。在其他语义不同的情况下(invoke、invokeGetter、invokeSetter和declarations),每个方法名有多个标记,用以_结尾的前缀表示包围的镜像类。

StrongSpecialization
invocationinstance_invoke | class_invoke | library_invoke | instance_invokeGetter | class_invokeGetter | library_invokeGetter | instance_invokeSetter | class_invokeSetter | library_invokeSetter | delegate | apply | newInstance
namingsimpleName | qualifiedName | constructorName
classificationisPrivate | isTopLevel | isImport | isExport | isDeferred | isShow | isHide | isOriginalDeclaration | isAbstract | isStatic | isSynthetic | isRegularMethod | isOperator | isGetter | isSetter | isConstructor | isConstConstructor | isGenerativeConstructor | isRedirectingConstructor | isFactoryConstructor | isFinal | isConst | isOptional | isNamed | hasDefaultValue | hasReflectee | hasReflectedType
annotationmetadata
typinginstance_type | variable_type | parameter_type | typeVariables | typeArguments | originalDeclaration | isSubtypeOf | isAssignableTo | superclass | superinterfaces | mixin | isSubclassOf | returnType | upperBound | referent
concretizationreflectee | reflectedType
introspectionowner | function | uri | library_declarations | class_declarations | libraryDependencies | sourceLibrary | targetLibrary | prefix | combinators | instanceMembers | staticMembers | parameters | callMethod | defaultValue
textlocation | source

图1. 可反射能力语言API的原始材料。

图2显示了将这一原始材料简化为我们认为合理的一组能力。它不允许程序员以相同程度的细节来选择他们的能力,但我们希望复杂性的降低有足够的价值来证明不那么精细的控制。

我们添加了正则参数,指定这些能力中的每一个都可以有意义地应用模式匹配约束来选择包含的方法、getters等。具体来说,这个参数是一个字符串,作为正则表达式使用。空的正则表达式是默认值,这意味着当正则表达式被省略时,相关类别中的所有实体都被包括在内。

类似地,我们创建了一些变体,这些变体有一个MetadataClass参数,表示相关类别中的实体如果被注释了元数据,其类型是给定的MetadataClass的一个子类型(它可以是微不足道的子类型,即MetadataClass本身),则该实体被包含。该参数是对应于预定类的Type类型的实例。

总之,这提供了对使用正则表达式的集中和略微抽象的实体选择的支持,它提供了对使用元数据的分散的实体选择的支持,以明确地标记实体。

需要注意的是,MetadataClass可能与包reflectable不相关。我们有这样的用例:来自与reflectable无关的包P的某个类C恰好很适合,因为C的实例已经作为元数据附在相关的成员集合上。这又可能是因为其他一些包需要C的元数据,用于其他一些与反射的需求有某种联系的目的,比如说序列化。

Non-terminalExpansion
apiSelectioninvocation | annotation | typing | introspection
invocationinstanceInvoke([RegExp]) | instanceInvokeMeta(MetadataClass) | staticInvoke([RegExp]) | staticInvokeMeta(MetadataClass) | topLevelInvoke([RegExp]) | topLevelInvokeMeta(MetadataClass) | newInstance([RegExp]) | newInstanceMeta(MetadataClass)
delegationdelegate
annotationmetadata
typingtype | typeRelations
introspectionowner | declarations | uri | libraryDependencies

图2. 可反射能力语言API的语法标记。

在类别调用中,我们使用了前缀topLevel而不是library,因为这个术语在现有的镜像类的文档中很常见。取消了类别命名,始终提供对相应功能的支持,因为在实践中从未出现过禁用这些功能的需要,而且支持这些功能的成本很低;类别分类的处理方式相同,具体化也是如此。类别文本被删除了,因为我们目前不打算支持对源代码整体的反思性访问。

我们省略了apply和function,因为我们没有对ClosureMirror的支持,而且我们也不期望很快得到它。

类别委托从调用中分离出来,因为对委托的支持是相当昂贵的。

类别的打字在几个方面被简化了:instance_type被重新命名为type,因为它很突出。反射器上的reflectType方法只有在这种能力存在的情况下才被支持。variable_type、parameter_type和returnType的能力被统一为type,因为它们关注的是同类镜像的查找,但支持的类集是用类型注解量词控制的,下文将介绍。为了对类型相关镜像的详细程度进行一些控制,typeVariables、typeArguments、originalDeclaration、isSubtypeOf、isAssignableTo、superclass、superinterfaces、mixin、isSubclassOf、upperBound和referent被统一到typeRelations中;它们都涉及类型、类型变量和类型定义之间的关系,如果从不使用相关信息,可能会导致大量空间开销的保留。

类别自省也被简化了。我们将class_declarations、library_declarations、instanceMembers、staticMembers、callMethod、parameters和defaultValue统一为声明。最后,我们将导入和导出属性统一到libraryDependencies中,这样它就包含了sourceLibrary、targetLibrary、前缀和组合器。我们单独保留了所有者的能力,因为我们希望为一个给定的声明查找封闭的声明的能力,如果作为另一个能力的一部分隐含地包括在内,则代价太高。我们还单独保留了 uri 功能,因为在某些情况下,保存 JavaScript 翻译代码中的 URI 信息(为了在库镜像上实现 uri 方法而需要)被认为是一个安全问题。

请注意,某些反思性方法是非基本的,因为它们可以完全基于其他反思性方法,即基本的方法来实现。这影响了以下能力:isSubtypeOf、isAssignableTo、isSubclassOf、instanceMembers和staticMembers。这些方法可以以一般的方式实现,所以它们被作为包reflectable的一部分提供,而不是被生成。因此,当且仅当它们所依赖的方法被支持时,它们才被支持。这就是我们说instanceMembers已经被 "统一到 "声明中的意思。

简洁地涵盖多个基于API的能力

为了避免在需要相对广泛的反射支持的情况下使用过于冗长的语法,我们选择引入一些分组标记。它们没有任何新的贡献,它们只是为某些预计会经常出现的能力选择提供了一个更简洁的符号。图3显示了这些分组标记。为了帮助记忆这种附加语法的含义,我们使用了以 "ing "结尾的词,以提示将几种能力分组到一个结构中所涉及的微小的抽象量。

GroupMeaning
invoking([RegExp])instanceInvoke([RegExp]), staticInvoke([RegExp]), newInstance([RegExp])
invokingMeta(MetadataClass)instanceInvokeMeta(MetadataClass), staticInvokeMeta(MetadataClass), newInstanceMeta(MetadataClass>)
typingtype, name, classify, metadata, typeRelations, owner, declarations, uri, libraryDependencies

图3. 可反射能力语言的分组令牌。

包括能力 invoking(RegExp) 的语义,其中 RegExp 代表一个给定的参数,与包括图中右侧同一行的所有三个能力的语义相同,给所有这些能力以相同的 RegExp 作为参数。同样地,在没有参数的情况下调用()请求支持对所有实例方法、所有静态方法和所有构造函数的反思性调用。包括能力 invokingMeta(MetadataClass) 的语义与在同一行中包括所有三个能力的语义相同,参数相同。最后,包括打字的语义是请求支持右边的所有能力;也就是说,请求支持与程序结构信息相关的每个特征。

自动获得相关能力

我们选择在能力之间使用子类型结构,以确保其中一些能力之间有自动关系。例如,如果你指定了声明能力,那么类型能力也会自动包括在内。这样做的原因是,除非有一些类或库的镜像可以获得这些声明,否则声明能力是没有用的,也就是说,不存在任何人需要声明能力而不需要类型能力的情况。这个机制的细节可以通过检查能力类之间的实际子类型关系来检查。如果一个能力类C1是另一个能力类C0的子类型,那么包含C1就意味着包含C0。

指定基于反射者的能力

在上一节中,我们发现了一种将基于镜像API的能力指定为一种语法的方法。它非常简单,因为它只由终端组成,除了其中一些终端需要一个参数,用来限制支持的参数到匹配的名字。如图2所示,非终端apiSelection涵盖了所有这些终端。我们将一次使用它们几个,所以典型的用法是一个列表,写成apiSelection*。

在这一节中,我们讨论如何为一组特定的程序元素请求由一个给定的apiSelection*指定的反射支持。接受反射支持的程序元素被称为规范的目标,规范本身在Reflectable类的一个子类(称为MyReflector)中作为超初始化器给出,有一个唯一的实例(称为myReflector)。现在,myReflector被用作程序中某个地方的元数据,每种能力只在某些地方作为注解适用,这将在下面讨论。

图4显示了如何构造能力和注解,一般从apiSelection*开始。语法的这一部分中的非端点是以元数据的预定位置命名的,它是或包含相应种类的能力。

Non-terminalsExpansions
reflectorReflectable(targetMetadata)
targetMetadataapiSelection | subtypeQuantify | superclassQuantify(upperBound, excludeUpperBound) | typeAnnotationQuantify(transitive) | correspondingSetterQuantify | admitSubtype
globalMetadataglobalQuantify(RegExp, reflector) | globalQuantifyMeta(MetadataClass, reflector)

图 4. 可反映的能力语言目标选择。

在实践中,反射器是Reflectable类的一个子类的实例,它作为元数据直接附着在一个类上,或者传递给一个全局量词;在运行的例子术语中,它是myReflector这个对象。反射器有一个状态,我们用targetMetadata建模。在图 4 的语法中,我们用标识符 Reflectable 来代表所有的子类,并且我们通过让它接受相应的 targetMetadata 作为参数来模拟状态。用一个给定的反射器来注释一个类的语义取决于targetMetadata,如下所述。

targetMetadata能力可以是一个基本级别的能力集,也就是apiSelection*,它也可以是一个量词,可能采取一个参数来表达变体。将包含普通apiSelection的反射器附加到目标类C的语义是,为类C和其实例提供由给定的apiSelection指定的级别的反射支持。

将包含 subtypeQuantify 的反射器附加到类 C 的语义是,由给定到同一反射器的 apiSelection 元素所指定的反射支持被提供给作为类 C 的子类型的所有类,包括 C 本身以及其实例。

将一个包含 superclassQuantify(upperBound, excludeUpperBound) 的反射器附加到类 C 的语义是,由给同一个反射器的 apiSelection 元素指定的反射支持被提供给作为类 C 的超类的所有类(以及它们的实例),包括 C 本身,并且停止在给定的 upperBound,或者如果 excludeUpperBound 为真则停止在它下面。如果excludeUpperBound被省略,那么它就被认为是false,如果upperBound被省略,那么它就被认为是Object。

根据这些规则,由给定的反射器指定的接受反射支持的类集被计算为最小固定点。例如,subtypeQuantify会重复添加已经包含的类的直接子类型,直到达到不添加任何类的状态。固定点计算在一个阶段中首先添加子类型,然后在第二个阶段添加超类。请注意,如果我们使用相反的顺序,或者对两者一起运行定点迭代,那么在两个量词都存在的情况下,我们会琐碎地包括所有的类(在上限下,以及默认的上限:所有的类),所以选择的排序是唯一有意义的排序。

如果指定了声明能力,那么就有可能获得一个类的镜像,然后为其字段查找变量镜像,为其方法、getters和setters查找方法镜像(使用类镜像上的声明、instanceMembers或staticMembers方法)。有了这些镜像,就有可能进一步查找类的镜像,比如给定方法镜像的参数类型的镜像,而且这个过程可以重复多次。这意味着,如果天真地提供对所有可达类镜像的支持,将很容易导致程序中的所有类都被包括在内,尽管这可能不是一个好的选择。正因为如此,我们选择默认省略所有声明的类型注释的类镜像。如果这些类镜像确实应该被包含,那么必须明确地请求它们。这是用typeAnnotationQuantify能力完成的。

将包含 typeAnnotationQuantify(transient) 的反射器附加到类 C 的语义是,包含的类镜像集将被增强,所有的类在包含的成员中作为类型注释使用。也就是说,已经包含的类的集合被遍历,这些类的每个包含的成员都被检查(比如说,一个方法,如果它与给定的正则表达式相匹配,或者携带给定类型的元数据,如果相应的能力需要这样的参数,那么它就被包含了)。对于该方法的每个参数以及返回值,任何给定的类型注解都是一个类,都会被添加到所包含的类集中。如果transient为false或省略,这个过程只运行一次,如果transient为true,则运行到没有更多的类被添加。

基于类型注解的覆盖类集的扩展,无论是单步还是定点迭代,都是在第三阶段进行的,在子类型和超类定点迭代之后。

将包含 admitSubtype 的反射器附加到一个类 C 上的语义非常微妙,值得在下一节中进行稍微详细的讨论。其基本思想是,它允许目标类的子类型的实例被当作目标类的实例来对待。

最后,我们支持使用全局量词、globalQuantify(RegExp, reflector) 和 globalQuantifyMeta(MetadataClass, reflector) 的 "侧标签"。目前,我们决定它们必须作为元数据附在导入 package:reflectable/reflectable.dart 的导入语句中,但如果其他位置在实践中有帮助,我们可能会放松这一限制。由于能力的单调语义,如果一个给定的程序包含多个这样的globalMetadata并不是问题,所提供的反射支持将只是满足所有请求的最小的一个。

在程序中拥有 globalQuantify(RegExp, reflector) 的语义与将给定的反射器直接附加到程序中限定名称与给定的 RegExp 相匹配的每个类的语义是相同的。同样地,在程序中拥有 globalQuantifyMeta(MetadataClass, reflector) 的语义与将给定的反射器直接附加到元数据包括 MetadataClass 类型的实例或其子类型的每个类的语义相同。

包含的成员和没有这样的方法

一般来说,覆盖是基于自下而上的语义的。在给定的能力集合中,被覆盖的类的集合和它们内部被覆盖的成员的集合被计算为对给定程序的查询。这是一个自下而上的语义,因为它从空的覆盖开始,然后用程序中确实存在的具体元素来扩展覆盖。

考虑一下被覆盖类中的一个成员。如果它不符合覆盖标准(它的名字不符合给定的正则表达式,也没有任何要求的元数据类型),那么它的状态就和根本不存在任何声明的类名或成员名一样。当用给定的实际参数列表L调用方法时,如果其形式参数列表不允许使用L作为实际参数进行调用,那么即使有一个具有指定名称的方法的声明,该方法也被认为是不存在的。例如,即使void foo(int i)存在,如果我们遇到调用foo(0, bar: true),它就是一个不存在的方法事件。

这种语义和与本地Dart调用的no-such-method事件相关的语义的关键区别在于,有关的方法可能完全缺失,也可能被拒绝覆盖,因为给定的能力太严格。因此,为了使程序员能够正确处理可反射调用的无此方法的情况,我们对这些情况的处理方式与本地无此方法的处理方式不同。本机调用失败会导致noSuchMethod被调用到同一个接收方,其Invocation参数描述了选择器和参数,而可反射调用失败会导致ReflectableNoSuchMethodError被抛出。这种类型的异常包含一个StringInvocation,它指定了与Invocation相同的调用信息,只是它的成员名是一个字符串而不是一个符号。程序员可以捕捉到这个调用,并以任何适当的方式做出反应,例如,通过调用他们自己的noSuchMethod的变体。

完全或部分镜像的实例?

传统上,人们认为对一个实例、一个类或其他一些实体的反射性访问将提供该实体的完整而忠实的视图。例如,反思性代码应该可以访问被声明为私有的特征,即使该反思性代码位于一个非反思性访问相同特征不被允许的环境中。此外,当反射式查找被用来了解一个给定的对象是哪个类的实例时,我们希望响应描述的是该对象的实际运行时类型,而不是一些超类,比如该对象在某些上下文中的静态已知类型。

在包reflectable中,有违反这个完整性假设的原因,其中一些是首先拥有这个包的原因的内在后果。换句话说,这些限制不会完全消失。其他的限制可能会在将来被取消,因为它们是基于在实现包的过程中做出的某些权衡而引入的。

提供reflectable包的主要动机是,由dart:mirrors包提供的对反射的更普遍的支持在运行时往往在程序大小上花费太多,或者也许有dart:mirrors的资源影响,所以完全省略了对dart:mirrors的支持。因此,包reflectable的一个核心点是指定一个符合特定程序目的的限制性反射版本,这样就可以用一个明显较小的空间完成。因此,对于这样一个程序来说,对一个对象有反射支持而没有对其某些方法的反射访问是完全正常的。还有其他几种设计上不完整的覆盖,它们并不是一个问题:它们是首先使用包reflectable的部分原因。

下面的小节讨论了两种不同的情况,在这些情况下,有些限制在设计上是不存在的。我们首先讨论对私有特性的访问不完整的情况,然后我们讨论用 admitSubtype(apiSelection*) 指定的接纳子类型的后果。

与隐私相关的限制

本小节讨论的限制是由包reflectable的实现中的权衡所引起的,所以我们需要提到一些实现细节。包reflectable是为代码生成而设计的。代码生成器接收一个程序(该程序使用包reflectable)作为输入,并生成支持所要求的反射功能的代码,使用镜像创建表达式的 "数据库",并在运行时咨询该数据库,所有这些都使用普通的、非反射的代码实现。

普通代码不能违反隐私限制。因此,由包reflectable提供的反射操作不能,例如,读取或写入一个与包含相关生成代码的库不同的私有字段。但是目前的代码生成方法总是而且只生成一个新的库,其中包含所有生成的代码;这意味着程序中的任何私有声明都不能从生成的代码中到达,甚至是当前包中的私有声明。

原则上可以修改当前包中的所有库,但即使这可以用来访问本地的私有声明,它仍然会使进口包中的所有私有声明无法到达。然而,这只能在不是那么迫切需要解决方案的情况下有所帮助。本地包中的库通常可以被编辑,添加一个合适的公共声明,以便给成员提供某种访问权限,否则是无法访问的。

有几个例外的情况。私有类的镜像可以从包围库的镜像中获得,超类链中的私有类被保留下来,这样,如果超类量化被要求的话,在所有超类上进行迭代就可以了。但是这些私有类不支持静态方法的调用,也不支持在其实例上获得镜像。

围绕接纳子类型的考虑因素

当apiSelection*形式的targetMetadata被附加到一个给定的类C时,其效果是为类C和C的实例提供反射支持。然而,这种支持可以被扩展到为C的子类型的实例提供部分反射支持,其方式不会在程序大小方面产生进一步的成本。为C类实例生成的镜像可以有一个反射对象(被该镜像反射的对象),其类型是C的适当子类型。它使实例镜像能够持有一个被反射者,这个被反射者是该镜像所生成的类型的适当子类型的实例。

问题是,对于具有运行时类型D的给定对象O,当没有正好为D创建的镜像类时,应该使用哪个实例镜像,该对象被作为参数给反射器上的操作反射。一般来说,可能有多个候选镜像类对应于C1、C2、......Ck类,它们是 "D的最小超类型",即没有类型E是D的适当超类型和对任何i而言是Ci的适当子类型(这也意味着没有两个Ci和Cj类是彼此的子类型)。语言规范包括一个算法,它将找到一个唯一确定的C1 ... Ck的超类型,这被称为它们的最小上界。我们不能直接使用这个算法,因为我们在一个类型层次中拥有一个任意的类型子集,而不是所有的类型,然后我们需要对这个 "稀疏的 "子类型层次做出类似的决定,它只包括来自给定反射器的支持反射的类。尽管如此,我们希望有可能创建一个最小上界算法的变体,该算法将适用于这些稀疏的子类型层次结构。

应该注意的是,在各种语言中通常被假定为支持反射的一个非常基本的不变量被违反了。当然,并不是所有的镜像系统都有类似于为特定类型构建的镜像的概念,但相应的问题在任何地方都是相关的。镜像不会报告对象的原样属性,它将报告超类型的实例的属性。这是一种不完全性,它甚至导致镜像在某些情况下对对象给出明显不正确的描述。

特别是,假设给定了一个运行时类型为D的对象O,我们有一个实例镜像IM,它的反射对象是O。

让我们在IM上使用反射操作来获取O的类镜像。IM.type将返回C的类镜像的实例CM,而不是O的实际运行时类型D的类镜像。如果程序员使用这种方法来查询像O这样的对象的类的名称,答案将是简单的错误,它说的是 "C",而它应该说是 "D"。同样地,如果我们遍历超类,我们将永远看不到D类,也看不到D和C之间的中间类。一个真实的例子是序列化:如果我们为了序列化反射体而查找字段的声明,那么我们将默默地不能包括在被忽略的子类中声明的字段,直到D。一般来说,有许多不愉快的惊喜在等待这个功能的天真用户,所以应该认为它是一个专家才有的选择。

为什么不直接做 "正确的事情",为D返回一个类镜像?不可能简单地在方法类型的实现中检查reflectee的运行时类型,然后提供D的类镜像,因为通常情况下,没有D的类镜像。事实上,有 admitSubtype 量词的全部意义在于它节省了空间,因为一个给定类型的潜在的大量子类型可以被给予部分反射支持,而不需要生成相应数量的镜像类。

为了进一步澄清获得 "部分 "反射支持的含义,考虑一些情况。

反射地调用在C中声明的或继承到C中的O上的实例方法将像预期的那样工作,标准的面向对象的方法调用将确保被调用的是O的正确方法实现,这可能是C中可用的实现,也可能是C的适当子类型中的一个实现。

调用在C的适当子类型中声明的O的实例方法,包括来自D本身的方法,将不会起作用。这是因为IM的类是在不存在这种方法的假设下生成的,它只知道C的方法。如前所述,如果我们获取O的类,我们可能会得到O的实际类的适当超类型,因此所有的派生操作都会受到类似的影响。例如,来自CM的声明将是C中的声明,它们与D中的声明毫无关系。同样,如果我们遍历超类,那么我们将只看到O类的实际超类列表的严格后缀。

基于这些严重的问题,我们决定,当实例镜像与 admitSubtype 量词相关联时,为了获得一个类的镜像而执行类型方法是一个运行时错误,因为当该类事实上不是被反映者的类时,它很可能无法按照预期的方式工作。同样地,在这种情况下,声明也不被支持。在匹配恰好完美的情况下(C == D),允许它是可能的,但是这对程序员来说很难使用,如果他们想反映一个不是直接来自实例的类,不妨直接使用 reflectType(C) 。

总而言之,在整个子类型层次结构应该配备反射支持的情况下,需要做一个微妙的权衡。这种权衡是:要么在程序大小上付出代价,获得完全的支持(使用subtypeQuantify);要么积极地节省空间,作为回报容忍对反射的部分支持(使用 admitSubtype)。

总结

我们已经描述了在包 reflectable 中使用的能力的设计,以指定对反射的预期支持水平。其基本思想是,基础层的能力指定了从镜像类的 API 中选择的操作,以及对这些操作的允许参数的一些简单限制。在此基础上,基于API的能力可以与目标程序的特定部分相关联(尽管在这一点上只有类),这样,正是这些类将拥有基于API的能力所指定的反射支持。通过在每个目标类上添加一个反射器作为元数据,可以单独选择目标类。或者,可以通过量化来选择目标类。例如,可以对所有的子类型进行量化,在这种情况下,不仅持有元数据的类C会得到反射支持,而且C的所有子类型也会得到反射支持。最后,有可能接纳子类型的实例作为一小部分镜像的反射者,这样就可以为许多类实现部分反射支持,而不需要花费许多镜像类。

参考文献

  1. Gilad Bracha和David Ungar. "镜像:面向对象编程语言元级设施的设计原则"。ACM SIGPLAN通告。2004年10月24日。331-344.
  2. Brian Cantwell Smith. "编程语言中的程序性反射"。1982.
  3. Jonathan M. Sobel 和 Daniel P. Friedman. "面向反射的编程介绍"。反思论文集》。1996年4月。

www.deepl.com 翻译

文章分类
代码人生
文章标签