本篇是Typing相关知识的最后一篇。介绍关于类型的闭包和类型推断关系,以及最终的类型静态编译相关知识点。,类型检查器对闭包执行特殊的推断,在一边执行额外的检查,在另一边提高流畅性。,类型检查器能够做的第一件事是推断闭包的返回类型。下面的例子简单地说明了这一点:,正如上面所看到的,与显式声明其返回类型的方法不同,不需要声明闭包的返回类型:它的类型是从闭包的主体推断出来的。,返回类型推断仅适用于闭包。虽然类型检查器可以对方法执行相同的操作,但实际上并不可取:通常情况下,方法可以被覆盖,并且静态地不可能确保所调用的方法不是被覆盖的版本。所以流类型实际上会认为一个方法返回一些东西,而在现实中,它可以返回其他东西,如下面的例子所示:,通过上面的示例可以知道,如果类型检查器依赖于方法的推断返回类型(使用流类型),则类型检查器可以确定是否可以调用toUpperCase。这实际上是一个错误,因为子类可以重写compute并返回不同的对象。这里,B.compute返回一个整型,因此在B的实例上调用computeFully将会看到一个运行时错误。编译器通过使用方法的声明返回类型而不是推断返回类型来防止这种情况发生。,为了保持一致性,这种行为对于每个方法都是相同的,即使它们是静态的或最终的。,除了返回类型外,闭包还可以从上下文推断其参数类型。编译器有两种方法来推断形参类型:,让我们从一个由于类型检查器无法推断形参类型而导致编译失败的示例开始:,在这个例子中,闭包体包含了it.age。对于动态的、非类型检查的代码,这是可行的,因为它的类型在运行时是Person。不幸的是,在编译时,没有办法知道它的类型,只能通过读取inviteIf的签名。,2.3.1 显式闭包参数,简而言之,类型检查器在inviteIf方法上没有足够的上下文信息来静态确定it的类型。这意味着方法调用需要像这样重写:,通过显式声明it变量的类型,可以解决这个问题,并使此代码进行静态检查。,2.3.2 从单一抽象方法类型推断出的参数,对于API或框架设计人员来说,有两种方法可以使其对用户来说更优雅,这样他们就不必为闭包参数声明显式类型。第一个方法,也是最简单的方法,是用SAM类型替换闭包:,通过使用这种技术,我们利用了Groovy将闭包自动强制转换为SAM类型的特性。,我们应该使用SAM类型还是Closure的问题实际上取决于需要做什么。,在很多情况下,使用SAM接口就足够了,特别是当考虑Java 8中的功能接口时。,但是,闭包提供了功能接口无法访问的特性。特别是,闭包可以有一个委托和所有者,并且可以在被调用之前作为对象进行操作(例如,克隆、序列化、curry等等)。它们还可以支持多个签名(多态性)。,因此,如果需要这种操作,最好切换到下面描述的最高级的类型推断注释。,当涉及到闭包参数类型推断时,最初需要解决的问题是,Groovy类型系统继承了Java类型系统,而Java类型系统不足以描述参数的类型,也就是说,静态地确定闭包的参数类型,而无需显式地声明它们。,2.3.3 使用@ClosureParams 注解,Groovy提供了一个注解@ClosureParams,用于完成类型信息。该注释主要针对那些希望通过提供类型推断元数据来扩展类型检查器功能的框架和API开发人员。如果我们的库使用闭包,并且也希望获得最大级别的工具支持,那么这一点非常重要。,让我们通过修改原始示例来说明这一点,引入@ClosureParams注释:,@ClosureParams注释最少接受一个参数,该参数被命名为类型提示。类型提示是一个类,它负责在闭包的编译时完成类型信息。在本例中,使用的类型提示是groovy.transform.stc.FirstParam,它向类型检查器指示闭包将接受一个类型为方法第一个参数类型的参数。在本例中,方法的第一个参数是Person,因此它向类型检查器指示闭包的第一个参数实际上是Person。,第二个可选参数名为options。它的语义取决于类型提示类。Groovy提供了各种捆绑的类型提示,如下表所示:,即使你使用FirstParam, SecondParam或ThirdParam作为类型提示,这并不严格意味着将传递给闭包的参数将是第一个(resp。方法调用的第二个,第三个)参数。这只意味着闭包的参数类型将与第一个(resp。方法调用的第二个,第三个)参数。,PS: 上面的表格,从Groovy中直接赋值的。所以表格阅读比较难看,简而言之,在接受Closure的方法上缺少@ClosureParams注释不会导致编译失败。如果存在(它可以出现在Java源代码中,也可以出现在Groovy源代码中),则类型检查器具有更多信息,并可以执行额外的类型推断。这使得框架开发人员对该特性特别感兴趣。,第三个可选参数名为conflictResolutionStrategy。它可以引用一个类(从,ClosureSignatureConflictResolver扩展而来),如果在初始推断计算完成后发现了多个参数类型,则该类可以执行额外的参数类型解析。Groovy提供了一个默认类型解析器,它什么都不做,另一个则在找到多个签名时选择第一个签名。解析器仅在发现多个签名时调用,并且被设计为后处理器。任何需要注入类型信息的语句都必须传递一个通过类型提示确定的参数签名。解析器然后从返回的候选签名中选择。,类型检查器使用@DelegatesTo注释推断委托的类型。它允许API设计者指示编译器委托的类型和委托策略。@DelegatesTo注释将在其他内容中进行专门的讨论。这里就不扩展了。,在类型检查部分,我们已经看到Groovy通过@TypeChecked注释提供了可选的类型检查。类型检查器在编译时运行,并对动态代码执行静态分析。无论是否启用类型检查,程序的行为都完全相同。这意味着@TypeChecked注释对于程序的语义是中立的。尽管可能有必要在源中添加类型信息以使程序被认为是类型安全的,但最终,程序的语义是相同的。,虽然这听起来很好,但实际上有一个问题:在编译时执行的动态代码的类型检查,根据定义,只有在没有发生特定于运行时的行为时才正确。例如,下面的程序通过了类型检查:,有两种计算方法。一个接受String并返回int,另一个接受int并返回String。如果你编译这个,它被认为是类型安全的:内部compute(‘foobar’)调用将返回一个int,并且在这个int上调用compute将返回一个String。,现在,在调用test()之前,考虑添加以下行:,使用运行时编程,我们实际上是在修改compute(String)方法的行为,这样它就不会返回所提供的参数的长度,而是返回一个Date。如果执行该程序,它将在运行时失败。因为这一行可以在任何线程的任何地方添加,所以类型检查器绝对没有办法静态地确保没有这样的事情发生。简而言之,类型检查器很容易受到猴子修补的攻击。这只是一个例子,但它说明了对动态程序进行静态分析本质上是错误的。,Groovy为@typecheck提供了另一种注释,它实际上将确保被推断为被调用的方法将在运行时有效地被调用。该注释将Groovy编译器转换为静态编译器,其中所有方法调用都在编译时解析,生成的字节码确保实现这一点:注释是@groovy.transform.CompileStatic。,@CompileStatic注释可以添加到@TypeChecked注释可以使用的任何地方,也就是说,在类或方法上。没有必要同时添加@TypeChecked和@CompileStatic,因为@CompileStatic执行@TypeChecked所做的一切,但是还会触发静态编译。,让我们以失败的例子为例,但这一次让我们用@CompileStatic替换@TypeChecked注释:,这是唯一的区别。如果我们执行这个程序,这次就不会出现运行时错误。test方法不再受猴子补丁的影响,因为在它的主体中调用的计算方法在编译时是链接的,所以即使Computer的元类发生了变化,程序仍然按照类型检查器的预期行事。,在代码中使用@CompileStatic有几个好处:,性能的提高取决于所执行程序的类型。,如果它受I/O限制,静态编译代码和动态代码之间的区别几乎不明显。,对于高度CPU密集型的代码,由于生成的字节码与Java为等效程序生成的字节码非常接近(如果不是相等的话),因此性能得到了极大的提高。,到这里关于类型的相关知识就介绍完毕了,以上内容可以通过Groovy官方文档:Groovy Language Documentation (groovy-lang.org)了解更多知识。,PS:类型知识的介绍更多的是从各种概念定义等方面详细介绍各种类型推断的过程。我们其实可以简单了解。
© 版权声明
文章版权归作者所有,未经允许请勿转载。