1.Introduction这是Typing相关知识的最后一部分。介绍类型闭包和类型推断的关系,以及最终类型静态编译相关的知识点。2.闭包和类型推断类型检查器对闭包进行特殊的推断,一方面进行额外的检查,另一方面提高流畅性。2.1返回类型推断类型检查器可以做的第一件事是推断闭包的返回类型。下面的示例简单地说明了这一点:@groovy.transform.TypeCheckedinttestClosureReturnTypeInference(Stringarg){defcl={"Arg:$arg"}//定义一个返回GString字符串的闭包defval=cl()//调用closureandassigntheresulttoavariableval.length()//类型检查器推断闭包将返回一个字符串,因此允许调用length()}如上所示,与显式声明其返回的方法不同类型,不需要声明闭包的返回类型:它的类型是从闭包的主体中推断出来的。2.2闭包与方法返回类型推断仅适用于闭包。虽然类型检查器可以对方法执行相同的操作,但这并不是真正可取的:通常,方法可以被覆盖,并且静态地不可能确保被调用的方法不是被覆盖的版本。所以流类型实际上会认为一个方法返回一些东西,而实际上它可以返回其他东西,如下例所示:@TypeCheckedclassA{defcompute(){'somestring'}//classAdefinitionAmethodcompute,它返回一个字符串defcomputeFully(){compute().toUpperCase()//这会导致编译失败,因为compute的返回类型是def(akaObject)}}@TypeCheckedclassBextendsA{defcompute(){123}//ClassBextendsAandredefinedcompute,whichreturnsaninteger}从上面的例子我们可以知道类型检查器是否依赖于方法的推断返回类型(使用流类型),类型检查器可以确定是否可以调用toUpperCase。这实际上是一个错误,因为子类可以覆盖计算并返回不同的对象。此处,B.compute返回一个整数,因此在B的实例上调用computeFully会出现运行时错误。编译器通过使用方法声明的返回类型而不是推断它来防止这种情况发生。为了保持一致性,此行为对于每个方法都是相同的,即使它们是静态的或最终的。2.3参数类型推断除了返回类型外,闭包还可以从上下文推断其参数类型。编译器有两种推断参数类型的方法:通过API元数据进行隐式SAM类型强制让我们从一个编译失败的例子开始,因为类型检查器无法推断参数类型:classPerson{Stringnameintage}voidinviteIf(Personp,Closurepredicate){//inviteIf方法接受一个Person和一个闭包if(predicate.call(p)){//发送邀请//...}}@groovy.transform.TypeCheckedvoidfailCompilation(){Personp=newPerson(name:'Gerard',age:55)inviteIf(p){it.age>=18//没有这个属性:age没有静态调用Person,所以编译失败}}在这个例子中,闭包体包含它。年龄。对于动态的、非类型检查的代码,这是有效的,因为它的类型在运行时是Person。不幸的是,在编译时,没有办法知道它的类型,只能通过阅读inviteIf的签名。2.3.1显式闭包参数简而言之,类型检查器没有足够的关于inviteIf方法的上下文信息来静态确定它的类型。这意味着方法调用需要这样重写:inviteIf(p){Personit->//itstypeneedstobeexplicitlydeclaredit.age>=18}通过显式声明it变量的类型,这个问题可以解决,并使这段代码静态检查。2.3.2从单个抽象方法类型推断出的参数API或框架设计者有两种方法可以使用户更优雅,这样他们就不必为闭包参数声明显式类型。第一种方法,也是最简单的方法,是用SAM类型替换闭包:interfacePredicate{booleanapply(One)}//使用apply方法声明SAM接口voidinviteIf(Personp,Predicatepredicate){if(predicate.apply(p)){//发送邀请//...}}@groovy.transform.TypeCheckedvoidpassesCompilation(){Personp=newPerson(name:'Gerard',age:55)inviteIf(p){//不再需要声明it变量的类型it.age>=18//当it.age正确编译时,它的类型是从Predicate#apply方法签名中推断出来的}通过使用这种技术,我们可以利用Groovy自动将闭包转换为SAM类型的能力。我们应该使用SAM类型还是闭包的问题实际上取决于需要做什么。在许多情况下,使用SAM接口就足够了,尤其是在考虑Java8中的函数式接口时。但是,闭包提供了函数式接口无法访问的特性。特别是,闭包可以有委托和所有者,并且可以在调用之前作为对象进行操作(例如,克隆、序列化、柯里化等)。它们还可以支持多个签名(多态性)。因此,如果需要这样的操作,最好切换到下面介绍的最高级别的类型推断注解。说到闭包参数类型推断,最初需要解决的问题是Groovy类型系统继承了Java类型系统,而Java类型系统不足以描述参数的类型,即静态判断闭包的参数类型,无需显式声明。2.3.3使用@ClosureParams注解Groovy提供了@ClosureParams注解来完成类型信息。此注释主要针对希望通过提供类型推断元数据来扩展类型检查器功能的框架和API开发人员。如果我们的库使用闭包并且我们还想要最高级别的工具支持,那么这一点非常重要。让我们通过修改原始示例来说明这一点,引入@ClosureParams注释:参数用@ClosureParams注释if(predicate.call(p)){//sendinvite//...}}inviteIf(p){it.age>=18//不需要使用显式类型它,因为它是推断的}@ClosureParams注解接受至少一个参数,它被命名为类型提示。类型提示是一个类,负责在编译时为闭包完成类型信息。在此示例中,使用的类型提示是groovy.transform.stc.FirstParam,它向类型检查器指示闭包将接受一个类型为方法第一个参数类型的参数。在这种情况下,该方法的第一个参数是Person,因此它向类型检查器表明闭包的第一个参数实际上是一个Person。第二个可选参数是命名选项。它的语义取决于类型提示类。Groovy提供了各种捆绑的类型提示,如下表所示:类型提示多态性描述和示例第二,第三)参数类型的方法:importgroovy.transform.stc.FirstParamvoiddoSomething(Stringstr,@ClosureParams(FirstParam)Closurec){c(str)}doSomething('foo'){printlnit.toUpperCase()}``导入groovy.transform.stc.SecondParamvoidwithHash(Stringstr,intseed,@ClosureParams(SecondParam)Closurec){c(31*str.hashCode()+seed)}withHash('foo',(int)System.currentTimeMillis()){intmod=it%2}``importgroovy.transform.stc.ThirdParamStringformat(Stringprefix,Stringpostfix,Stringo,@ClosureParams(ThirdParam)Closurec){"$prefix${c(o)}$postfix"}assertformat('foo','bar','baz'){it.toUpperCase()}=='fooBAZbar'FirstParam.FirstGenericTypeSecondParam.FirstGenericTypeThirdParam.FirstGenericTypeNo第一个泛型类型(resp。二、三)方法的参数importgroovy.transform.stc.FirstParampublicvoiddoSomething(Liststrings,@ClosureParams(FirstParam.FirstGenericType)Closurec){strings.each{c(it)}}doSomething(['foo','bar']){printlnit.toUpperCase()}doSomething([1,2,3]){println(2*it)}SecondGenericType的变体和ThirdGenericType存在于所有FirstParam,SecondParam和ThirdParam类型提示。SimpleTypeNo闭包参数的类型来自选项字符串类型提示。导入groovy.transform.stc.SimpleTypepublicvoiddoSomething(@ClosureParams(value=SimpleType,options=['java.lang.String','int'])Closurec){c('foo',3)}doSomething{str,len->assertstr.length()==len}此类型提示支持单个签名,并使用完全限定类型名称或原始类型将每个参数指定为选项数组的值。MapEntryOrKeyValue是一个专用的闭包类型提示,可以作为Map.Entry中的单个参数,或者分别对应key和value的两个参数。导入groovy.transform.stc.MapEntryOrKeyValuepublicvoiddoSomething(Mapmap,@ClosureParams(MapEntryOrKeyValue)Closurec){//...}doSomething([a:'A']){k,v->assertk.toUpperCase()==v.toUpperCase()}doSomething([abc:3]){e->asserte.key.length()==e.value}这种类型hint要求第一个参数是Map类型,并从Map的实际键/值类型推断闭包参数类型。FromAbstractTypeMethods是从某种类型的抽象方法推断闭包参数类型。为每个抽象方法推断出一个签名。导入groovy.transform.stc.FromAbstractTypeMethods抽象类Foo{abstractvoidfirstSignature(intx,inty)abstractvoidsecondSignature(Stringstr)}voiddoSomething(@ClosureParams(value=FromAbstractTypeMethods,options=["Foo"])Closurecl){//...}doSomething{a,b->a+b}doSomething{s->s.toUpperCase()}如果像上面的例子有多个签名,那么只有在每个类型检查器中才能如果两种方法的元数不同,则推断参数的类型。在上面的例子中,firstSignature接受2个参数,secondSignature接受1个参数,因此类型检查器可以根据参数的数量推断参数类型。但是请参阅下面讨论的可选解析器类属性。FromString是从选项参数推断闭包参数类型。options参数由逗号分隔的非原始类型数组组成。数组中的每个元素对应一个签名,元素中的每个逗号对应签名的一个参数。简而言之,这是最通用的类??型提示,选项映射的每个字符串都像带符号的文字一样被解析。虽然此类型提示非常强大,但必须尽可能避免使用它,因为由于需要解析类型签名,它会增加编译时间。接受字符串的闭包的单个签名::importgroovy.transform.stc.FromStringvoiddoSomething(@ClosureParams(value=FromString,options=["String","String,Integer"])Closurecl){//...}doSomething{s->s.toUpperCase()}doSomething{s,i->s.toUpperCase()*i}接受String或String的多态闭包,Integer:importgroovy.transform.stc.FromStringvoiddoSomething(@ClosureParams(value=FromString,options=["String","String,Integer"])Closurecl){//...}doSomething{s->s.toUpperCase()}doSomething{s,i->s.toUpperCase()*i}接受一个T或一对T的多态闭包,T:importgroovy.transform.stc。FromStringpublicvoiddoSomething(Te,@ClosureParams(value=FromString,options=["T","T,T"])Closurecl){//...}doSomething('foo'){s->s.toUpperCase()}doSomething('foo'){s1,s2->asserts1.toUpperCase()==s2.toUpperCase()}即使你使用FirstParam,SecondParam或ThirdParam作为类型提示,这并不严格意味着将传递给闭包的参数将是方法调用的第一个(分别是第二个、第三个)参数。这只是意味着闭包的参数类型将与第一个相同(resp。方法调用的第二个、第三个)参数。PS:上表直接从Groovy赋值。所以读表是丑陋的简而言之,在接受闭包的方法上缺少@ClosureParams注释不会导致编译失败。如果它存在(它可以出现在Java源代码中,它也可以出现在Groovy源代码中),类型检查器有更多的信息并且可以执行额外的类型推断。这使得此功能对框架开发人员特别感兴趣。第三个可选参数名为conflictResolutionStrategy。它可以引用一个类(从ClosureSignatureConflictResolver扩展),如果在初始推理计算完成后找到多个参数类型,则可以执行额外的参数类型解析。Groovy提供了一个不执行任何操作的默认类型解析器,以及另一个在找到多个签名时选择第一个签名的解析器。解析器仅在找到多个签名时被调用,并且被设计为后处理器。任何需要注入类型信息的语句都必须传递通过类型提示确定的参数签名。然后解析器从返回的候选签名中进行选择。类型检查器使用@DelegatesTo注释来推断委托的类型。它允许API设计者指示编译器要委托的类型和委托策略。@DelegatesTo注释在别处专门讨论。这里没有展开。3.静态编译3.1动态和静态在类型检查部分,我们已经看到Groovy通过@TypeChecked注解提供可选的类型检查。类型检查器在编译时运行并对动态代码执行静态分析。无论是否启用类型检查,程序的行为都完全相同。这意味着@TypeChecked注释对程序的语义是中立的。尽管可能需要将类型信息添加到源代码中才能使程序被认为是类型安全的,但最终,程序的语义是相同的。虽然这听起来不错,但实际上存在一个问题:根据定义,在编译时执行的动态代码的类型检查只有在没有发生特定于运行时的行为时才是正确的。例如,下面的程序通过了类型检查:{defcomputer=newComputer()computer.with{assertcompute(compute('foobar'))=='6'}}有两种计算方式。一个接受一个String并返回一个int,另一个接受一个int并返回一个String。如果你编译它,它被认为是类型安全的:内部compute('foobar')调用将返回一个int,而调用这个int上的compute将返回一个String。现在,在调用test()之前,考虑添加以下行:Computer.metaClass.compute={Stringstr->newDate()}使用运行时编程,我们实际上正在修改compute(String)方法的行为,这它不返回所提供参数的长度,而是返回一个日期。如果程序被执行,它会在运行时失败。由于此行可以添加到任何线程的任何位置,因此类型检查器绝对没有办法静态地确保不会发生这种情况。简而言之,类型检查器很容易受到猴子补丁的攻击。这只是一个例子,但它表明动态程序的静态分析在本质上是错误的。Groovy为@typecheck提供了另一个注解,它实际上将确保被推导调用的方法将在运行时有效地被调用。这个注释将Groovy编译器变成了一个静态编译器,其中所有方法调用都在编译时解析,生成的字节码确保了这一点:注释是@groovy.transform.CompileStatic。3.2@CompileStatic注解@CompileStatic注解可以添加到任何可以使用@TypeChecked注解的地方,即类或方法上。没有必要同时添加@TypeChecked和@CompileStatic,因为@CompileStatic做了@TypeChecked做的所有事情,但也会触发静态编译。让我们以失败的例子为例,但这次让我们用@CompileStatic替换@TypeChecked注解:}@groovy.transform.CompileStaticvoidtest(){defcomputer=newComputer()computer.with{assertcompute(compute('foobar'))=='6'}}Computer.metaClass.compute={Stringstr->newDate()}test()这是唯一的区别。如果我们执行这个程序,这次不会有运行时错误。测试方法不再受到猴子补丁的影响,因为在其主体中调用的计算方法在编译时被链接,所以即使Computer元类发生变化,程序仍然按照类型检查器的预期运行。3.3主要好处在代码中使用@CompileStatic有几个好处:类型安全免疫猴子补丁性能改进性能取决于正在执行的程序的类型。如果它受I/O限制,则静态编译代码和动态代码之间的区别几乎不可察觉。对于高度CPU密集型代码,性能会大大提高,因为生成的字节码非常接近(如果不等于)Java为等效程序生成的字节码。4.小结至此,关于类型的相关知识已经介绍完毕。您可以通过Groovy官方文档:GroovyLanguageDocumentation(groovy-lang.org)了解更多关于以上内容。PS:类型知识的介绍,更多的是从各种概念定义等方面详细推断各种类型的过程。其实我们可以简单的理解。