侧边栏壁纸
  • 累计撰写 416 篇文章
  • 累计创建 65 个标签
  • 累计收到 145 条评论

目 录CONTENT

文章目录

35. Groovy 语法 类型知识详解-第二篇 类型推断

Z同学
2022-12-15 / 0 评论 / 3 点赞 / 260 阅读 / 3,394 字
温馨提示:
本文最后更新于 2022-12-20,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1. 介绍

接着上篇介绍的类型相关的知识内容,继续了解Groovy中关于类型Typing的相关知识内容。

上一篇内容分享了关于静态类型检测的部分知识要点。本章接着继续。

34. Groovy 语法 类型知识详解-第一篇 (zinyan.com)

2 类型推断

类型推断的原则:当代码被@typecheck注释时,编译器执行类型推断。它不仅仅依赖于静态类型,而且还使用各种技术来推断变量的类型、返回类型、字面量等等,这样即使激活了类型检查器,代码也尽可能保持干净。

下面通过简单的示例来了解类型推断:

def message = 'Welcome to Groovy!'       //变量使用def关键字声明       
println message.toUpperCase()                   
println message.upper() // compile time error

toUpperCase调用能够工作的原因是消息类型被推断为String

2.1.1 类型推断中的变量与字段

值得注意的是,尽管编译器对局部变量执行类型推断,但它不会对字段执行任何类型的类型推断,总是返回到字段的声明类型。为了说明这一点,让我们来看看这个例子:

class SomeClass {
    def someUntypedField                                                                
    String someTypedField                                                               

    void someMethod() {
        someUntypedField = '123'                                                        
        someUntypedField = someUntypedField.toUpperCase()  // compile-time error        
    }

    void someSafeMethod() {
        someTypedField = '123'                                                          
        someTypedField = someTypedField.toUpperCase()                                   
    }

    void someMethodUsingLocalVariable() {
        def localVariable = '123'                                                       
        someUntypedField = localVariable.toUpperCase()                                  
    }
}

为什么会有这样的差异? 原因是线程安全。

在编译时,我们不能保证字段的类型。任何线程都可以在任何时间访问任何字段,并且在方法中为字段分配某种类型的变量和之后使用的时间之间,另一个线程可能已经更改了字段的内容。

对于局部变量则不是这样:我们知道它们是否“转义”,因此我们可以确保变量的类型随着时间的推移是常量(或非常量)。

请注意,即使字段是final的,JVM也不会保证它,因此无论字段是否是final的,类型检查器的行为都不会有所不同。

这是Groovy建议使用类型化字段的原因之一。虽然由于类型推断,对于局部变量使用def是完全可以的,但对于字段就不是这样了,因为字段也属于类的公共API,因此类型很重要。

2.1.2 集合文字类型推断

Groovy为各种类型文字提供了一种语法。Groovy中有三种原生集合:

  • lists:通过 [] 符号
  • maps:通过 [:] 符号
  • ranges: 区间通过from..to (包括), from..<to (右边不包括),from<..to (左边不包括) 和from<..<to (全部都不包括)

集合的推断类型取决于集合的元素,如下表所示:

示例 类型
def list = [] java.util.List
def list = ['foo','bar'] java.util.List<String>
def list = ["${foo}","${bar}"] java.util.List<GString>
def map = [:] java.util.LinkedHashMap
def map1 = [someKey: 'someValue'] def map2 = ['someKey': 'someValue'] java.util.LinkedHashMap<String,String>
def map = ["${someKey}": 'someValue'] java.util.LinkedHashMap<GString,String>
def intRange = (0..10) groovy.lang.IntRange
def charRange = ('a'..'z') groovy.lang.Range<String> 使用边界的类型来推断范围的组件类型

正如我们所看到的,除了IntRange之外,推断类型使用泛型类型来描述集合的内容。如果集合包含不同类型的元素,类型检查器仍然执行组件的类型推断,但使用最小上界的概念。

2.1.3 最小上界-LUB

在Groovy中,两种类型AB的最小上界定义为:

  • 超类,对应于AB的公共超类
  • 接口,对应于AB实现的接口
  • 如果AB是基本类型,且A不等于B,则AB的最小上界是它们包装器类型的最小上界

如果AB只有一个公共接口,并且它们的公共超类是Object,那么两者的LUB(最小上界)就是公共接口。

最小上界表示AB都能赋值的最小类型。例如,如果AB都是String,那么两者的LUB(最小上界)也是String

class Top {}
class Bottom1 extends Top {}
class Bottom2 extends Top {}

assert leastUpperBound(String, String) == String                    
assert leastUpperBound(ArrayList, LinkedList) == AbstractList       
assert leastUpperBound(ArrayList, List) == List                     
assert leastUpperBound(List, List) == List                          
assert leastUpperBound(Bottom1, Bottom2) == Top                     
assert leastUpperBound(List, Serializable) == Object  

在这些示例中,LUB总是可以表示为JVM支持的普通类型。但是Groovy在内部将LUB表示为一种更复杂的类型,

例如,不能使用它来定义变量:

interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}

BottomSerializableFooImpl的最小上界是多少?

它们没有共同的超类(除了Object),但是它们共享两个接口(SerializableFoo),所以它们的最小上界是一个表示两个接口(SerializableFoo)并集的类型。这种类型不能在源代码中定义,但Groovy知道它。

在集合类型推断(以及一般的泛型类型推断)上下文中,这变得很方便,因为组件的类型被推断为最小上界。我们可以在下面的例子中说明为什么这很重要:

interface Greeter { void greet() }                  
interface Salute { void salute() }                  

class A implements Greeter, Salute {                
    void greet() { println "Hello, I'm A!" }
    void salute() { println "Bye from A!" }
}
class B implements Greeter, Salute {                
    void greet() { println "Hello, I'm B!" }
    void salute() { println "Bye from B!" }
    void exit() { println 'No way!' }               
}
def list = [new A(), new B()]                       
list.each {
    it.greet()                                      
    it.salute()                                     
    it.exit()                                       
}

错误信息如下所示:

[Static type checking] - Cannot find matching method Greeter or Salute#exit()

这表明exit方法既没有在Greiter上定义,也没有在Salute上定义,这两个接口定义在AB的最小上界中。

2.1.4 实例推导

在正常的、非类型检查的Groovy中,我们可以这样写:

class Greeter {
    String greeting() { 'Hello' }
}

void doSomething(def o) {
    if (o instanceof Greeter) {     
        println o.greeting()        
    }
}

doSomething(new Greeter()) //输出: Hello

方法调用可以工作是因为动态分派(方法是在运行时选择的)。Java中的等效代码需要在调用greeting方法之前将o转换为Greeter,因为方法是在编译时选择的:

if (o instanceof Greeter) {
    System.out.println(((Greeter)o).greeting());
}

然而,在Groovy中,即使在doSomething方法上添加了@TypeChecked(从而激活了类型检查),强制转换也不是必需的。编译器嵌入instanceof推理,使强制转换成为可选的。

2.1.5 流类型-Flow typing

流类型是类型检查模式中Groovy的一个重要概念,也是类型推断的扩展。其思想是,编译器能够推断代码流中的变量类型,而不仅仅是在初始化时:

@groovy.transform.TypeChecked
void flowTyping() {
    def o = 'foo'                       
    o = o.toUpperCase()                 
    o = 9d                              
    o = Math.sqrt(o)                    
}

因此,类型检查器知道变量的具体类型随着时间的推移而不同。特别是,如果将最后的赋值替换为:

o = 9d
o = o.toUpperCase()

类型检查器现在将在编译时失败,因为当toUpperCase被调用时,它知道o是一个double类型,因此这是一个类型错误。

重要的是要理解,使用def声明变量并不是触发类型推断的事实。流类型适用于任何类型的任何变量。用显式类型声明变量只限制你可以赋值给变量的内容:

@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list = list*.toUpperCase()          
    list = 'foo'                        
}

还可以注意到,即使变量声明时没有泛型信息,类型检查器也知道组件类型是什么。因此,这样的代码将无法编译:

@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list.add(1)                         
}

解决这个问题需要在声明中添加显式泛型类型:

@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List<? extends Serializable> list = []                      
    list.addAll(['a','b','c'])                                  
    list.add(1)                                                 
}

引入流类型是为了减少经典Groovy和静态Groovy之间的语义差异。特别地,考虑这段代码在Java中的行为:

public Integer compute(String str) {
    return str.length();
}
public String compute(Object o) {
    return "Nope";
}
// ...
Object string = "Some string";          
Object result = compute(string);        
System.out.println(result); 

在Java中,这段代码将输出Nope,因为方法选择是在编译时根据声明的类型完成的。因此,即使o在运行时是一个字符串,它仍然是被调用的对象版本,因为o已经声明为对象。简而言之,在Java中,声明的类型是最重要的,无论是变量类型、参数类型还是返回类型。

在Groovy中,我们可以这样写:

int compute(String string) { string.length() }
String compute(Object o) { "Nope" }
Object o = 'string'
def result = compute(o)
println result

但这一次,它将返回6,因为所选择的方法是在运行时根据实际的参数类型选择的。所以在运行时,o是一个字符串,所以使用了字符串变量。注意,此行为与类型检查无关,它是Groovy的一般工作方式:动态分派

在类型检查的Groovy中,我们希望确保类型检查器在编译时选择与运行时相同的方法。

由于语言的语义,这在一般情况下是不可能的,但我们可以使用流类型使事情变得更好。

使用流类型,在调用compute方法时,o被推断为String,因此选择接受String并返回int的版本。这意味着我们可以推断方法的返回类型是int,而不是String。这对于后续调用和类型安全非常重要。

因此,在类型检查的Groovy中,流类型是一个非常重要的概念,这也意味着,如果应用了@TypeChecked,则根据参数的推断类型选择方法,而不是根据声明的类型。这并不能确保100%的类型安全,因为类型检查器可能会选择错误的方法,但它确保了最接近动态Groovy的语义。

2.1.6 高级类型推断

流类型和最小上界推理的组合用于执行高级类型推断,并确保在多种情况下的类型安全。特别是,程序控制结构可能会改变变量的推断类型:

class Top {
   void methodFromTop() {}
}
class Bottom extends Top {
   void methodFromBottom() {}
}
def o
if (someCondition) {
    o = new Top()                               
} else {
    o = new Bottom()                            
}
o.methodFromTop()                               
o.methodFromBottom()  // compilation error  

上述的代码执行后,会报编译错误

当类型检查器访问if/else控制结构时,它检查if/else分支中赋值的所有变量,并计算所有赋值的最小上界。这个类型是if/else块之后的推断变量的类型,所以在这个例子中,oif分支中被分配了一个Top,在else分支中被分配了一个Bottom。其中的LUB是一个Top,所以在条件分支之后,编译器推断o是一个Top。因此,允许调用methodFromTop,但不允许调用methodFromBottom

对于闭包(closures),特别是闭包共享变量,也存在同样的推理。闭包共享变量是定义在闭包外部,但在闭包内部使用的变量,如下例所示:

def text = 'Hello, zinyan.com!'                          
def closure = {
    println text                                    
}

Groovy允许开发人员使用这些变量,而不要求它们是final变量。这意味着闭包共享变量可以在闭包内部重新赋值:

String result
doSomething { String it ->
    result = "Result: $it"
}
result = result?.toUpperCase()

问题是闭包是一个独立的代码块,可以在任何时候执行(也可以不执行)。特别是,例如,doSomething可能是异步的。这意味着闭包的主体不属于主控制流。因此,对于每个闭包共享变量,类型检查器也会计算该变量的所有赋值的LUB,并将该LUB用作闭包作用域之外的推断类型,如下例所示:

class Top {
   void methodFromTop() {}
}
class Bottom extends Top {
   void methodFromBottom() {}
}
def o = new Top()                               
Thread.start {
    o = new Bottom()                            
}
o.methodFromTop()                               
o.methodFromBottom()  // compilation error

在这里,很明显,当methodFromBottom被调用时,不能保证在编译时或运行时o的类型将有效地是Bottom。这是有可能的,但我们不能确定,因为它是异步的。所以类型检查器只允许调用最小的上界,也就是这里的Top

所以上面的代码中,当我们调用methodFromBottom后就会出现编译错误了。

3. 小结

本篇内容主要介绍了各种类型推断,以及相关推断的过程和Groovy处理逻辑。相关知识可以参考Groovy 官方文档:

Groovy Language Documentation (groovy-lang.org)

如果觉得本篇内容总结的还可以,希望能够点个赞鼓励一下。谢谢。

3

评论区