首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >35. Groovy 语法 类型知识详解-第二篇 类型推断

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

作者头像
zinyan.com
发布于 2023-02-23 09:51:31
发布于 2023-02-23 09:51:31
68800
代码可运行
举报
文章被收录于专栏:zinyanzinyan
运行总次数:0
代码可运行

1. 介绍

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

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

本章继续。

2 类型推断

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

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

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}

BottomSerializableFooImpl的最小上界是多少?

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
[Static type checking] - Cannot find matching method Greeter or Salute#exit()

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

2.1.4 实例推导

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
if (o instanceof Greeter) {
    System.out.println(((Greeter)o).greeting());
}

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

2.1.5 流类型-Flow typing

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTyping() {
    def o = 'foo'                       
    o = o.toUpperCase()                 
    o = 9d                              
    o = Math.sqrt(o)                    
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
o = 9d
o = o.toUpperCase()

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list.add(1)                         
}

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
def text = 'Hello, zinyan.com!'                          
def closure = {
    println text                                    
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String result
doSomething { String it ->
    result = "Result: $it"
}
result = result?.toUpperCase()

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Day4下午解题报告
预计分数:30+30+0=60 实际分数:30+30+10=70 稳有个毛线用,,又拿不出成绩来,, T1 https://www.luogu.org/problem/show?pid=T15626
attack
2018/04/11
6770
Day4下午解题报告
学会 SAM,做完这几道题目就足够了
上周我们讲解了 sam 的基本原理,这一周,我们将把目光转向他的应用,主要通过题目讲解。
ACM算法日常
2021/09/07
4710
【CodeForces 613B】Skills
  给你n个数,可以花费1使得数字+1,最大加到A,最多花费m。最后,n个数里的最小值为min,为A的有k个,给你cm和cf,求force=min*cm+k*cf 的最大值,和n个数操作后的结果。
饶文津
2020/06/02
2960
Codeforces Round #361 div2
  有n个城市, 第i个城市到第j个城市需要消耗abs(i - j)的能量, 然后每个城市可以转移到另一个城市, 只需要一个能量, 转移是单向的。
熙世
2019/07/14
4870
Codeforces Round #526 (Div. 2) B. Kvass and the Fair Nut(思维)
题目链接:http://codeforces.com/contest/1084/problem/B
Ch_Zaqdt
2019/01/10
4850
程序员进阶之算法练习(五十五)
题目链接 题目大意: 小明有一只猫,现在猫的饥饿值为H,并且每分钟会增加D; 他可以选择现在就买猫粮,1份猫粮价格为C,可以减少猫的饥饿值N;(猫粮只能一份一份购买) 他也可以选择晚上20点之后购买,商店会打8折;(当前的时间为hh时mm分) 问,小明最少需要花费多少,才能把猫的饥饿值降到0;
落影
2021/11/24
2710
HDU 3388 Coprime(容斥原理+二分)
Coprime Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 849    Accepted Submission(s): 232 Problem Description Please write a program to calculate the k-th positive integer that is coprime with m
ShenduCC
2018/04/26
6260
Codeforces Round #754 (Div. 2) C-E
给你一个只含有’a’, b’, ‘c’ 的字符串,长度1e5内,让你寻找最短的子串满足:
Here_SDUT
2022/09/19
4480
Codeforces Round #754 (Div. 2) C-E
Codeforces #576 div 2 ABCD
A.暴力 #include <bits/stdc++.h> using namespace std; int a[1000005]; int main() { int n,x,y,mi=0; scanf("%d %d %d",&n,&x,&y); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } for(int i=1;i<=n;i++){ int fg = 1; for(int j=i-x;j<=i+y;j++){ if(j<1||j>n|
用户2965768
2019/08/14
2470
【校赛小分队之我们有个女生】训练赛1
校赛小分队之我们有个女生队——这是我、ljh学长、zk大神组的队,我取得闪亮亮的队名!
饶文津
2020/06/02
3560
Codeforces 的题目真的值得算法竞赛选手训练吗?
个串,有两种操作,一种是给某个串加一个字符,另一种是求存不存在一个串是查询串的子串。强制在线。
ACM算法日常
2021/11/10
9840
Educational Codeforces Round 45 (Rated for Div. 2)
第一次打cf,确实有很多不适应的地方,第一题上来把$n$和$m$看反了,然后特判的时候写的是$M % N$,直接wa到飞
attack
2019/01/30
3710
「2017 Multi-University Training Contest 1」2017多校训练1
1001 Add More Zero(签到题) 题目链接 HDU6033 Add More Zero image.png #include <cstdio> #include <cmath> int cas,m; int main(){ while(~scanf("%d",&m)){ printf("Case #%d: %d\n",++cas,(int)(m*log(2)/log(10))); } return 0; } 1002 Balala Power!(贪心) 题目链接 HDU6034 Ba
饶文津
2020/06/02
3900
「2017 Multi-University Training Contest 1」2017多校训练1
ACM / ICPC 2018亚洲区预选赛北京赛站网络赛 3题签到
选择一个城市开始,必须按照顺时针遍历完所有城市,第一次到这个城市获得ai经费,到下一个城市的代价是bi
用户2965768
2018/09/29
4950
Codeforces Round #360 div2
  有d天, n个人。如果这n个人同时出现, 那么你就赢不了他们所有的人, 除此之外, 你可以赢他们所有到场的人。
熙世
2019/07/14
3870
CodeForces - 1245 D - Shichikuji and Power Grid
Shichikuji is the new resident deity of the South Black Snail Temple. Her first job is as follows:
风骨散人Chiam
2020/10/28
5880
CodeForces - 1245 D - Shichikuji and Power Grid
Codeforces 19C. Deletion of Repeats
题目链接 一开始感觉是求最短不包含重复段的后缀 卡了好久qwq 后来发现forfor就完事了 本来想写sa hash 暴力三种写法的 结果写完暴力发现比sa还快 瞬间索然无味 我写sa干嘛呢 离散化一下然后枚举就行 就当复习sa了555
wenzhuan
2022/08/15
1980
“玲珑杯”ACM比赛 Round #13 题解&源码
A 题目链接:http://www.ifrog.cc/acm/problem/1111 分析:容易发现本题就是排序不等式, 将A数组与B数组分别排序之后, 答案即N∑i=1Ai×Bi 此题有坑,反正据
Angel_Kitty
2018/04/08
6880
“玲珑杯”ACM比赛 Round #13 题解&源码
NOIP2011题解
为了准备一个独特的颁奖典礼,组织者在会场的一片矩形区域(可看做是平面直角坐标系的第一象限)铺上一些矩形地毯,一共有n张地毯,编号从 1 到n。现在将这些地毯按照编号从小到大的顺序平行于坐标轴先后铺设,后铺的地毯覆盖在前面已经铺好的地毯之上。 地毯铺设完成后,组织者想知道覆盖地面某个点的最上面的那张地毯的编号。注意:在矩形地毯边界和四个顶点上的点也算被地毯覆盖。
全栈程序员站长
2022/09/07
3650
【2016 ACM/ICPC Asia Regional Qingdao Online】
而且,不超过1e9的乘积不过5000多个,于是预处理出来,然后每次二分找就可以了。
饶文津
2020/06/02
8300
相关推荐
Day4下午解题报告
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档