前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >35. Groovy 语法 类型知识详解-第二篇 类型推断

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

作者头像
zinyan.com
发布2023-02-23 17:51:31
5990
发布2023-02-23 17:51:31
举报
文章被收录于专栏:zinyan

1. 介绍

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

上一篇内容分享了关于静态类型检测的部分知识要点。34. Groovy 语法 类型知识详解-第一篇

本章继续。

2 类型推断

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

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

代码语言:javascript
复制
def message = 'Welcome to Groovy!'       //变量使用def关键字声明       
println message.toUpperCase()                   
println message.upper() // compile time error

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

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

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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表示为一种更复杂的类型。

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

代码语言:javascript
复制
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}

BottomSerializableFooImpl的最小上界是多少?

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

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

代码语言:javascript
复制
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()                                       
}

错误信息如下所示:

代码语言:javascript
复制
[Static type checking] - Cannot find matching method Greeter or Salute#exit()

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

2.1.4 实例推导

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

代码语言:javascript
复制
class Greeter {
    String greeting() { 'Hello' }
}

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

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

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

代码语言:javascript
复制
if (o instanceof Greeter) {
    System.out.println(((Greeter)o).greeting());
}

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

2.1.5 流类型-Flow typing

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

代码语言:javascript
复制
@groovy.transform.TypeChecked
void flowTyping() {
    def o = 'foo'                       
    o = o.toUpperCase()                 
    o = 9d                              
    o = Math.sqrt(o)                    
}

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

代码语言:javascript
复制
o = 9d
o = o.toUpperCase()

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

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

代码语言:javascript
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list = list*.toUpperCase()          
    list = 'foo'                        
}

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

代码语言:javascript
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list.add(1)                         
}

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

代码语言:javascript
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List<? extends Serializable> list = []                      
    list.addAll(['a','b','c'])                                  
    list.add(1)                                                 
}

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

代码语言:javascript
复制
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中,我们可以这样写:

代码语言:javascript
复制
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 高级类型推断

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

代码语言:javascript
复制
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),特别是闭包共享变量,也存在同样的推理。闭包共享变量是定义在闭包外部,但在闭包内部使用的变量,如下例所示:

代码语言:javascript
复制
def text = 'Hello, zinyan.com!'                          
def closure = {
    println text                                    
}

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

代码语言:javascript
复制
String result
doSomething { String it ->
    result = "Result: $it"
}
result = result?.toUpperCase()

问题是闭包是一个独立的代码块,可以在任何时候执行(也可以不执行)。特别是,例如,doSomething可能是异步的。这意味着闭包的主体不属于主控制流。

因此,对于每个闭包共享变量,类型检查器也会计算该变量的所有赋值的LUB,并将该LUB用作闭包作用域之外的推断类型,如下例所示:

代码语言:javascript
复制
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)

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

下一篇,是关于类型的最后一篇内容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 zinyan 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 介绍
  • 2 类型推断
    • 2.1.1 类型推断中的变量与字段
      • 2.1.2 集合文字类型推断
        • 2.1.3 最小上界-LUB
          • 2.1.4 实例推导
            • 2.1.5 流类型-Flow typing
              • 2.1.6 高级类型推断
              • 3. 小结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档