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

34. Groovy 语法 类型知识详解-第一篇

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

1. 介绍

本篇内容开始介绍Groovy中的各种类型知识。将会分多篇文章详细介绍和学习Groovy中的有关于类型的相关知识点。

内容来源于Groovy官方文档中得到1.6.6. Typing中的相关知识点。

内容比较多。可以通过目录查询想了解的模块。

2. 可选类型-Optional typing

可选类型是指即使不在变量上设置显式类型,程序也可以工作。作为一种动态语言,Groovy自然实现了这一特性,例如,当声明一个变量时:

代码语言:javascript
复制
String aString = 'zinyan.com'   //声明了一个变量字符串                    
//我们调用这个字符串的大小写转换方法并输出
println aString.toUpperCase()   //输出:ZINYAN.COM

在Groovy中,我们可以通过可选类型关键字:def 来代替:

代码语言:javascript
复制
def aString = 'zinyan.com'   //声明了一个变量字符串                    

println aString.toUpperCase() 

两种写法是一样的。def不只是可以代替String,它可以代替任何的一种数据类型。

所以在这里使用显式类型并不重要。当我们将此功能与静态类型检查相结合时,这尤其有趣,因为类型检查器执行类型推断。

同样,Groovy不强制在方法中声明参数的类型:

代码语言:javascript
复制
String concat(String a, String b) {
    a+b
}
println concat('zinyan','.com') //输出:zinyan.com

可以使用def作为返回类型和参数类型来重写,以便利用duck类型,如以下示例所示:

代码语言:javascript
复制
def concat(def a, def b) {                              
    a+b
}
println concat('zinyan','.com') //输出:zinyan.com      
println concat(1,2) //输出:3

我们通过def可选类型,就能实现动态的参数处理了。扩展了方法的使用范围。

建议在这里使用def关键字来描述一个方法的意图,该方法应该适用于任何类型,但从技术上讲,我们可以使用Object,结果是一样的:在Groovy中,def严格等同于使用Object

最终,可以从返回类型和描述符中完全删除该类型。但如果要从返回类型中删除它,则需要为该方法添加显式修饰符,以便编译器可以在方法声明和方法调用之间产生差异,如以下示例所示:

代码语言:javascript
复制
private concat(a,b) {                                   
    a+b
}
println concat('zinyan','.com') //输出:zinyan.com      
println concat(1,2) //输出:3  

我们直接将def给省略了。

在公共API的方法参数或方法返回类型中,省略类型通常被认为是一种不好的做法。虽然在局部变量中使用def并不是一个真正的问题,因为变量的可见性仅限于方法本身,但在方法参数上设置def时,def将在方法签名中转换为Object,这使得用户很难知道哪种类型的参数是期望的类型。

PS:总结来说,我们可以将类型定义为def,然后还能将def给省略掉。但是不建议大家在对外提供的api中省略def。容易造成阅读困难。 其次,def就是java中的Object对象。只是中间的各种转换解析等功能Groovy在编译器中帮我们进行了转换。

3. 静态类型检测-Static type checking

默认情况下,Groovy在编译时执行最小的类型检查。由于它主要是一种动态语言,所以静态编译器通常无法在编译时进行的大多数检查。通过运行时元编程添加的方法可能会改变类或对象的运行时行为。

通过示例介绍一下:

代码语言:javascript
复制
//创建一个对象
class Person {                                                          
    String firstName
    String lastName
}
//初始化该实例对象
def p = new Person(firstName: 'Zin', lastName: 'yan')  
println p.formattedName  

在动态语言中,像上述示例这样的代码不抛出任何错误是很常见的。在Java中,这通常会在编译时失败。

我们直接执行上面的代码就会输出:

代码语言:javascript
复制
Caught: groovy.lang.MissingPropertyException: No such property: formattedName for class: Person
groovy.lang.MissingPropertyException: No such property: formattedName for class: Person
    at zinyan.run(zinyan.groovy:8)

错误提示,我们如果想正常运行,就需要执行依赖运行时元编程。也就是说修改运行时状态,执行动态特性:

代码语言:javascript
复制
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }

完整示例为:

代码语言:javascript
复制
//创建一个对象
class Person {                                                          
    String firstName
    String lastName
}
//初始化该实例对象
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }
def p = new Person(firstName: 'Zin', lastName: 'yan')  
println p.formattedName

这意味着,一般来说,在Groovy中,除了声明类型之外,我们不能对对象的类型做出任何假设,即使我们知道它,也无法在编译时确定将调用什么方法,或者将检索哪个属性。这个特性用在DSL和测试脚本编写中有不少的特性。这里就不展开了。

然而,如果我们的程序不依赖动态特性,并且来自静态世界(特别是来自Java思维),那么在编译时没有捕捉到这样的“错误”可能会出现崩溃。正如我们在前面的示例中看到的,编译器不能确定这是一个错误。为了让编译器意识到这一点,必须明确指示编译器我们正在切换到类型检查模式。这可以通过使用

@groovy.transform.TypeChecked注释类或方法来完成。

当激活类型检查时,编译器将新增以下的工作:

  1. 类型推断被激活,这意味着即使对局部变量使用def,类型检查器也能够从赋值中推断出变量的类型.
  2. 方法调用在编译时解析,这意味着如果没有在类上声明方法,编译器将抛出错误
  3. 通常,在静态语言中查找的所有编译时错误都会出现:方法未找到、属性未找到、方法调用的不兼容类型、数字精度错误等…

下面让我们描述类型检查器在各种情况下的行为,并解释在代码中使用@TypeChecked的限制。

3.1 @TypeChecked注解

在编译时激活类型检查。

我们可以将@groovy.transform.TypeChecked注解添加到类的开头,

让编译器编译该类时启用类型检测:

代码语言:javascript
复制
@groovy.transform.TypeChecked
class Calculator {
    int sum(int x, int y) { x+y }
}

或添加到方法中:

代码语言:javascript
复制
class Calculator {
    @groovy.transform.TypeChecked
    int sum(int x, int y) { x+y }
}

在第一种情况下,所有方法、属性、字段、内部类… 注释类的类型将被检查,而在第二种情况下,只有方法和它包含的潜在闭包或匿名内部类将被类型检查。

我们同时也可以通过@TypeChecked(TypeCheckingMode.skip)对其进行注释来指示类型检查器跳过类型检测:

代码语言:javascript
复制
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
//对GreetingService类的所有方法和类进行类型检测。
@TypeChecked                                        
class GreetingService {
    String greeting() {                             
        doGreet()
    }
	//对doGreet方法跳过类型检测。
    @TypeChecked(TypeCheckingMode.SKIP)             
    private String doGreet() {
        def b = new SentenceBuilder()
        b.Hello.my.name.is.Zinyan                     
        b
    }
}
def s = new GreetingService()
assert s.greeting() == 'Hello my name is Zinyan'

在前面的示例中,SentenceBuilder依赖于动态代码。没有真正的Hello方法或属性,因此类型检查器通常会发出异常,编译将失败。因为使用生成器的方法被标记为TypeCheckingMode.SKIP,此方法跳过了类型检查,因此即使类的其余部分进行了类型检查也会编译代码。

以下部分描述Groovy中类型检查的语义。

3.2 类型检查分配

类型A的对象o可以赋值给类型T的变量当且仅当:

  • T 等于A
  • 或者T 是以下几种类型之一:String, boolean, BooleanClass
  • 或者o 是空的,T不是一个基本类型。
  • 或者TA 是一个数组, A 的组件类型可分配给 T 的组件类型。
  • 或者T 是一个数组,A 是一个集合或流(stream ), A的组件类型可分配给 T的组件类型。
  • 或者TA 的超类。
  • 或者T是由 A 实现的接口。
  • 或者TA 是基本类型,它们的封装类型是可赋值的。
  • 或者Textedns groovy.lang.Closure是一个闭包,同时A 是SAM类型(单一抽象方法类型)。
  • 或者TA源自java.lang. Number,并遵循下表:

T

A

Examples

Double

Any but BigDecimal or BigInteger

Double d1 = 4d Double d2 = 4f Double d3 = 4l Double d4 = 4i Double d5 = (short) 4 Double d6 = (byte) 4

Float

Any type but BigDecimal, BigInteger or Double

Float f1 = 4f Float f2 = 4l Float f3 = 4i Float f4 = (short) 4 Float f5 = (byte) 4

Long

Any type but BigDecimal, BigInteger, Double or Float

Long l1 = 4l Long l2 = 4i Long l3 = (short) 4 Long l4 = (byte) 4

Integer

Any type but BigDecimal, BigInteger, Double, Float or Long

Integer i1 = 4i Integer i2 = (short) 4 Integer i3 = (byte) 4

Short

Any type but BigDecimal, BigInteger, Double, Float, Long or Integer

Short s1 = (short) 4 Short s2 = (byte) 4

Byte

Byte

Byte b1 = (byte) 4

3.3 List 和Map 的构造函数

除了上面的赋值规则,如果赋值被认为是无效的,在类型检查模式下,如果满足以下条件,List或Map A可以赋值给类型T的变量:

  • 赋值是一个变量声明,A是一个List,T有一个构造函数,其参数与List的元素类型匹配。
  • 赋值是一个变量声明,A是一个map,T有一个无参数构造函数,每个map键都有一个属性。

具体示例如下:

代码语言:javascript
复制
@groovy.transform.TupleConstructor
class Person {
    String firstName
    String lastName
}
Person classic = new Person('Zin','yan')

可以使用“列表构造函数”:

代码语言:javascript
复制
Person list = ['Zin','yan']

创建一个Person对象出来,也可以使用Map构造函数创建一个Person对象:

代码语言:javascript
复制
Person map = [firstName:'Zin', lastName:'yan']

如果使用Map构造函数,则会对映射的键进行额外检查,以检查是否定义了同名的属性。例如,以下代码将在编译时失败:

代码语言:javascript
复制
@groovy.transform.TupleConstructor
class Person {
    String firstName
    String lastName
}
Person map = [firstName:'Zin', lastName:'yan', age: 1024]

就会触发以下错误:

代码语言:javascript
复制
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '{firstName=Zin, lastName=yan, age=1024}' with class 'java.util.LinkedHashMap' to class 'Person' due to: org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack: No such property: age for class: Person

3.4 方法解析

在类型检查模式下,方法在编译时解析。解析通过名称和参数工作。返回类型与方法选择无关。参数类型与以下规则中的参数类型匹配:

类型A的参数o可以用于类型T的参数,当且仅当:

  • T 等于A
  • 或者T是一个String,A是一个GString
  • 或者o为空,T不是基础类型。
  • 或者T是一个数组,A是一个数组,A的组件类型可以分配给T的组件类型。
  • 或者TA的超类。
  • 或者TA实现的接口。
  • 或者TA是基本类型,它们的封装类型是可赋值的。
  • T扩展了groovy.lang.Closure,而A是SAM类型(单一抽象方法类型)。
  • 或者TA派生自java.lang.Number,并遵循与数字赋值相同的规则。

如果在编译时没有找到具有适当名称和参数的方法,则抛出错误。下面的例子说明了与“正常”Groovy的区别:

代码语言:javascript
复制
class MyService {
    void doSomething() {
        printLine 'Do something'            
    }
}

printLine是一个错误,但由于我们处于动态模式,错误在编译时不会被捕获

上面的例子展示了一个Groovy能够编译的类。但是,如果尝试创建MyService的实例并调用doSomething方法,那么它将在运行时失败,因为printLine不存在。当然,我们已经展示了Groovy如何使它成为一个完全有效的调用,例如通过捕获MethodMissingException或实现一个自定义元类,但如果你知道你不是在这种情况下,@typecheck会派上用场:

代码语言:javascript
复制
@groovy.transform.TypeChecked
class MyService {
    void doSomething() {
        printLine 'Do something'            
    }
}

仅仅添加@typecheck就会触发编译时方法解析。类型检查器将尝试在MyService类上找到一个接受String的方法printLine,如果找不到。它将编译失败,并显示以下消息:

Cannot find matching method MyService#printLine(java.lang.String)

理解类型检查器背后的逻辑很重要:它是一种编译时检查,因此根据定义,类型检查器不知道我们所做的任何类型的运行时元编程。这意味着如果激活类型检查,没有@TypeChecked也完全有效的代码将不再编译。如果你想到duck typing,这一点尤其重要:

代码语言:javascript
复制
class Duck {
    void quack() {              
        println 'Quack!'
    }
}
class QuackingBird {
    void quack() {              
        println 'Quack!'
    }
}
@groovy.transform.TypeChecked
void accept(quacker) {
    quacker.quack()             
}
accept(new Duck())  

比如引入一个接口,但基本上,通过激活类型检查,获得了类型安全,但失去了语言的一些特性。希望Groovy能引入一些特性,比如流类型,以缩小类型检查和非类型检查Groovy之间的差距。

4. 小结

本篇内容未完待续。可以通过下一篇了解完整内容。

以上内容参考Groovy 官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_typing

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 介绍
  • 2. 可选类型-Optional typing
  • 3. 静态类型检测-Static type checking
    • 3.1 @TypeChecked注解
      • 3.2 类型检查分配
        • 3.3 List 和Map 的构造函数
          • 3.4 方法解析
          • 4. 小结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档