38. Groovy 类型检查扩展,第二篇 使用扩展

2023-02-23 17:54:29 浏览数 (1)

1. 介绍

在上一篇介绍了基本的Groovy的类型检查扩展,以及该扩展的意义和部分的API说明。

本篇接着上篇没有讲完的内容,继续介绍类型检查扩展的相关知识点。

2. 使用类型检查扩展

我们讲解了如何创建类型检查扩展,这里开始讲解各种的使用方式。

2.1 支持类-Support classes

DSL依赖于一个名为org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport的支持类。这个类本身扩展了org.codehaus.groovy.transform.stc.TypeCheckingExtension。这两个类定义了许多帮助器方法,使使用AST变得更容易,特别是在类型检查方面。要知道的一件有趣的事情是,我们可以访问类型检查器。这意味着我们可以以编程方式调用类型检查器的方法,包括那些允许抛出编译错误的方法。

扩展脚本委托给org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport类,这意味着我们可以直接访问以下变量:

  • context:类型检查器上下文,类型为org.codehaus.groovy.transform.stc.TypeCheckingContext
  • typeCheckingVisitor:类型检查器本身,一个org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor实例
  • generatedMethods:一个“生成方法”的列表,这实际上是一个“dummy”方法的列表,你可以使用newMethod调用在一个类型检查扩展中创建。

类型检查上下文包含大量在上下文中对类型检查器有用的信息。例如,当前的封闭方法调用堆栈、二进制表达式、闭包等等,如果我们必须知道错误发生时我们在哪里以及我们想要处理它,那么这些信息就特别重要。

除了GroovyTypeCheckingExtensionSupportStaticTypeCheckingVisitor提供的功能外,类型检查DSL脚本还从org.codehaus.groovy.ast.ClassHelperorg.codehaus.groovy.transform.stc.StaticTypeCheckingSupport导入静态成员,允许通过OBJECT_TYPESTRING_TYPETHROWABLE_TYPE等对公共类型进行访问,并检查诸如missesgenericsttypes(ClassNode)isClassClassNodeWrappingConcreteType(ClassNode)等。

2.2 类节点-Class nodes

在使用类型检查扩展时,需要特别注意处理类节点。编译使用抽象语法树(AST),当您检查类的类型时,该树可能不完整。这也意味着在引用类型时,不能使用StringHashSet等类字面量,而是使用表示这些类型的类节点。这需要一定程度的抽象和理解Groovy如何处理类节点。为了简化工作,Groovy提供了几个辅助方法来处理类节点。例如,如果你想说“String的类型”,你可以这样写:

代码语言:javascript复制
assert classNodeFor(String) instanceof ClassNode

还会注意到,classNodeFor有一个变体,它以String而不是Class作为参数。一般来说,我们不应该使用该方法,因为它将创建一个名称为String的类节点,但没有在其上定义任何方法、任何属性等。第一个版本返回已解析的类节点,而第二个版本返回未解析的类节点。所以后者应该留给非常特殊的情况。

可能遇到的第二个问题是引用一个尚未编译的类型。这种情况发生的频率可能比你想象的要高。例如,当一起编译一组文件时。在这种情况下,如果你想说“那个变量是Foo类型的”,但Foo还没有编译,仍然可以使用lookupClassNodeFor引用Foo类节点:

代码语言:javascript复制
assert lookupClassNodeFor('Foo') instanceof ClassNode

2.3 类型检查器的帮助

假设你知道变量foo的类型是Foo,你想告诉类型检查器。然后可以使用storeType方法,该方法接受两个参数:第一个参数是希望存储类型的节点,第二个参数是节点的类型。如果查看storeType的实现,我们将看到它委托给类型检查器等效方法,该方法本身做了大量工作来存储节点元数据。还将看到,存储类型并不局限于变量:可以设置任何表达式的类型。

同样,获取AST节点的类型只需调用该节点上的getType即可。这通常是你想要的,但有一些注意事项:

  1. getType返回一个表达式的推断类型。这意味着对于声明为Object类型的变量,它不会返回Object的类节点,而是在代码的这一点返回该变量的推断类型(流类型)。
  2. 如果想访问变量(或字段/参数)的原始类型,那么必须在AST节点上调用适当的方法。

2.4 抛出错误

要抛出一个类型检查错误,你只需要调用addStaticTypeError方法,它有两个参数:

  • 将显示给最终用户的字符串形式的消息
  • 负责错误的AST节点。最好提供最合适的AST节点,因为它将用于检索行号和列号

2.5 isXXXExpression表达式

通常需要知道AST节点的类型。为了可读性DSL提供了一个特殊的isXXXExpression方法,该方法将委托给XXXExpression的x实例。例如:

不建议的写法:

代码语言:javascript复制
if (node instanceof BinaryExpression) {
   ...
}

正确,推荐的写法:

代码语言:javascript复制
if (isBinaryExpression(node)) {   ... }

2.6 虚拟方法-Virtual methods

当我们执行动态代码的类型检查时,可能经常会遇到这样的情况:知道一个方法调用是有效的,但它背后并没有“真正的”方法。

Grails动态查找器为例。可以有一个由名为findByName(…)的方法组成的方法调用。

由于bean中没有定义findByName方法,类型检查器会报错。

但是,我们知道这个方法在运行时不会失败,甚至可以知道这个方法的返回类型是什么。

对于这种情况,DSL支持由虚拟方法组成的两个特殊构造。这意味着将返回一个实际上并不存在但在类型检查上下文中定义的方法节点。有三种方法:

  • newMethod(String name, Class returnType)
  • newMethod(String name, ClassNode returnType)
  • newMethod(String name, Callable<ClassNode> return Type)

所有这三个方法都做同样的事情:它们创建一个新的方法节点,其名称是提供的名称,并定义该方法的返回类型。

此外,类型检查器会将这些方法添加到generatedMethods列表中。我们只设置名称和返回类型的原因是,这是在90%的情况下所需要的。

例如,在上面的findByName示例中,只需要知道findByName不会在运行时失败,并且它返回一个域类。

返回类型的Callable版本很有趣,因为当类型检查器实际需要返回类型时,它推迟了返回类型的计算。

在某些情况下,当类型检查器要求返回类型时,可能不知道实际的返回类型,因此可以使用闭包,每当类型检查器在此方法节点上调用getReturnType时,都会调用闭包。

如果将此与延迟检查结合起来,就可以实现相当复杂的类型检查,包括前向引用的处理。

代码语言:javascript复制
newMethod(name) {
    //每次调用这个方法节点上的getReturnType时,这个闭包就会被调用!
    println 'Type checker called me!'
    lookupClassNodeFor(Foo) //返回类型
}

如果你需要的不仅仅是名称和返回类型,你可以自己创建一个新的MethodNode

2.7 范围-Scoping

范围在DSL类型检查中非常重要,这也是为什么我们不能使用基于切入点的方法来进行DSL类型检查的原因之一。

基本上,必须能够非常精确地定义何时应用扩展,何时不应用扩展。此外,必须能够处理常规类型检查器无法处理的情况,例如前向引用:

代码语言:javascript复制
point a(1,1)
line a,b // b是事后引用的!
point b(5,2)

例如,你想处理一个构建器:

代码语言:javascript复制
builder.foo {
   bar
   baz(bar)
}

因此,我们的扩展应该只在输入foo方法时是活动的,并且在此范围之外是不活动的。

但是,可能会遇到复杂的情况,比如同一个文件中有多个构建程序或嵌入式构建程序(构建程序中的构建程序)。

虽然不应该尝试从一开始就修复所有这些问题(必须接受类型检查的限制),但类型检查器确实提供了一种很好的机制来处理这个问题:使用newScopescopeExit方法的作用域堆栈。

  • newScope :创建一个新的作用域并将其放在堆栈顶部
  • scopeExits :从堆栈中弹出作用域

范围包括:

  • 父作用域
  • 自定义数据的Map

如果想看一下实现,它只是一个LinkedHashMap (org.codehaus.groovytypecheckingextensionsupport.typecheckingscope),但是它非常强大。例如,可以使用这样的作用域来存储退出作用域时要执行的闭包列表。这是处理前向引用的方式:

代码语言:javascript复制
def scope = newScope()
scope.secondPassChecks = []
//...
scope.secondPassChecks << { println 'executed later' }
// ...
scopeExit {
    secondPassChecks*.run() // 执行延迟检查
}

也就是说,如果在某个时候无法确定表达式的类型,或者此时无法检查赋值是否有效,仍然可以稍后进行检查……这是一个非常强大的功能。

现在,newScopescopeExit提供了一些有趣的语法糖:

代码语言:javascript复制
newScope {
    secondPassChecks = []
}

DSL中的任何时候,都可以使用getCurrentScope()或更简单的currentScope访问当前作用域:

代码语言:javascript复制
//...
currentScope.secondPassChecks << { println 'executed later' }
// ...

一般的模式是:

  • 确定将新作用域推入堆栈的切入点,并在此作用域中初始化自定义变量
  • 使用各种事件,可以使用存储在自定义范围中的信息来执行检查、延迟检查……
  • 确定退出范围的切入点,调用scopeExit并最终执行额外的检查

2.8 其他有用的方法

要获得helper方法的完整列表,请参考类org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport和类org.codehaus.groovy.transform.stc.TypeCheckingExtension。但是,要特别注意以下方法:

  • isDynamic: 接受VariableExpression作为参数,如果变量是DynamicExpression则返回true,这意味着在脚本中,它不是使用类型或def定义的。
  • isGenerated:接受MethodNode作为参数,并告知该方法是否是由类型检查器扩展使用newMethod 方法生成的方法
  • isAnnotatedBy: 接受一个AST节点和一个类(或ClassNode),并告知该节点是否用这个类标注。例如: isAnnotatedBy(node, NotNull)
  • getTargetMethod: 接受一个方法调用作为参数,并返回类型检查器为其确定的 MethodNode
  • delegatesTo: 模拟@DelegatesTo注解的行为。它允许我们判断参数将委托给特定类型(也可以指定委托策略)

3. 小结

关于类型检测扩展的使用相关知识要点,就介绍到这里了。以上相关内容可以参考Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#Typecheckingextensions-Workingwithextensions进行查询。

下一篇将会继续介绍类型检查扩展的知识点,高级类型检查扩展。

0 人点赞