Groovy 类型检查器

作者: Paul King
发布: 2024-01-20 08:30PM


默认情况下,Groovy 在编译期间仅执行最少的静态类型检查。这允许您编写使用语言的非常动态功能的脚本,例如在运行时添加方法。但许多程序不使用这种动态功能,因此 Groovy 允许您在需要时通过使用 Groovy 的 静态类型检查 功能来提高编译器执行的类型检查量。

但 Groovy 并没有只提供动态/静态(或开启/关闭)类型检查功能,它提供了一种 类型检查扩展 机制。事实证明,在许多情况下,您希望进行大部分类似 Java 的类型检查,但对一些轻微的动态行为进行一些小的放松。相反,有时您需要进行比 Java 更严格的检查。这里我们将介绍这后一种情况的两个内置检查器。

  • 在 Groovy 4 中引入了 RegexChecker。当使用 Groovy 的正则表达式运算符或调用 Java 正则表达式 API 调用时,它会执行额外的检查。它查找非法字符、未关闭的捕获组或范围、非法或未知语法以及许多其他通常在运行时发生的错误。使用检查器可以确保如果您的程序编译成功,您将不会收到运行时错误。

  • 在 Groovy 5 的 alpha 版本中提供了 FormatStringChecker。它检查各种 printf 和格式方法。它查找非法精度规范、未知格式转换、类型转换不匹配和其他错误。使用检查器时,如果您的程序编译成功,则您不应该收到此类错误。

这两个检查器都依赖于在代码中的某个地方提供正则表达式或格式字符串。如果您将此类表达式存储在数据库或属性文件中,或者在其他地方,那么您就运气不佳。但请记住,该机制是可扩展的,因此添加更多支持只需几行(或可能几十行)代码。

一个例子

让我们来看一个使用这两个检查器的例子。这是一个使用正则表达式从 Groovy 源代码库中第一个提交的日志消息中提取一些信息的例子。为了简单起见,我们将日志消息复制为一个字符串,但如果我们想从 git 中提取它,Groovy 有一些很好的功能。

var firstCommitLog = 'Date:   Thu Aug 28 18:48:39 2003 +0000' // (1)

@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker') // (2)
def getMatcher(String text) {
    text =~ /Date:\s*(\w{3})\s(\w{3})\s(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s(\d{4})/
}

@TypeChecked(extensions = 'groovy.typecheckers.FormatStringChecker') // (3)
def displayInfo(String day, boolean afternoon, BigDecimal numYears) {
    printf 'Day: %s, Afternoon: %x, Years ago: %3.1f', day, afternoon, numYears
}

var (_, day, month, date, hour, _, _, year) = getMatcher(firstCommitLog)[0] // (4)

var afternoon = hour.toInteger() >= 12
int monthNumber = DateTimeFormatter.ofPattern('MMM').parse(month)[MONTH_OF_YEAR]
var now = LocalDate.now()
var firstCommitDate = LocalDate.of(year.toInteger(), monthNumber, date.toInteger())
var numYears = (now - firstCommitDate) / 365  // (5)

displayInfo(day, afternoon, numYears)
  1. 第一个 git 提交消息中的其中一行

  2. 带有额外正则表达式检查的方法

  3. 带有额外格式字符串检查的方法

  4. 从第一个匹配中提取组件

  5. 计算自第一次提交以来的年数,以浮点数形式表示

当我们运行此脚本时,我们得到以下输出

Day: Thu, Afternoon: true, Years ago: 20.4

因此,我们知道第一次提交是在 20 多年前的一个星期四的下午/晚上进行的。

在这里,我们用 TypeChecked 注释了两个有问题的 method,并明确列出了我们在每种情况下想要的 extensions。Groovy 提供了编程的 编译器配置配置脚本 来隐藏这些细节,但我们这里不作进一步介绍。

让我们看看如果我们在脚本中犯了错误,我们会看到哪些编译时错误。

让我们在表达式的开头放一个意外的闭合分组括号

[Static type checking] - Bad regex: Unmatched closing ')'
)Date:\s*(\w{3})\s(\w{3})\s(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s(\d{4})

或者在第一个重复项上省略闭合花括号

[Static type checking] - Bad regex: Unclosed counted closure near index 13
Date:\s*(\w{3)\s(\w{3})\s(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s(\d{4})
             ^

或者使用错误的转义序列

[Static type checking] - Bad regex: Illegal/unsupported escape sequence near index 6
Date:\g*(\w{3})\s(\w{3})\s(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s(\d{4})
      ^

同样,我们也可能在显示收集的信息时犯错误。可能我们将类型弄错,这里将最后两个参数的顺序搞乱了

[Static type checking] - IllegalFormatConversion: f != java.lang.Boolean
       printf 'Day: %s, Afternoon: %b, Years ago: %3.1f', day, numYears, afternoon

或者可能只是完全弄错了转换字符,使用 %y 表示年份,但没有这样的转换字符

[Static type checking] - UnknownFormatConversion: Conversion = 'y'
       printf 'Day: %s, Afternoon: %b, Years ago: %y', day, afternoon, numYears

或者可能要求将布尔值填充为 0

[Static type checking] - FormatFlagsConversionMismatch: Conversion = b, Flags = '0'
       printf 'Day: %s, Afternoon: %0b, Years ago: %3.1f', day, afternoon, numYears

查看文档以了解有关检测到的 30 种左右错误的更多信息。

结论

正则表达式和格式字符串是利基活动,因此您可能并不经常需要进行此类检查。但您并不总是使用这些功能的事实会增加犯错的可能性。有这些额外的检查器可以使您放心。请记住,这些只是使用 Groovy 的可扩展类型检查的例子。为什么不为自己的场景添加额外的检查呢!玩得开心!