brown padlock on black metal fence during daytime

Apache Groovy 闭包:超越基础

我们已经掌握了 Apache Groovy 闭包的基础知识。本高级教程将向您展示如何利用…
首页 » 博客 » Apache Groovy 闭包:超越基础

我们已经探索了它们的强大功能,现在准备深入了解如何在 Groovy 丰富的库生态系统中使用闭包。我们还将释放它们在项目中编写更简洁、更清晰代码的潜力。(如果您尚未安装 Groovy,请阅读本系列的介绍。)

让我们从所有 Groovy 脚本编写者不时会遇到的那种数据集开始——分隔符文本文件。我将使用 OpenIntro 慷慨提供的测温数据集
它看起来像这样

body.temp,gender,heart.rate
96.3,male,70
96.7,male,71
96.9,male,74
97,male,80
97.1,male,73
97.1,male,75
97.1,male,82
97.2,male,64
97.3,male,69
...

第一行提供列名,后续行中包含对应的数据。行中的字段用逗号分隔。

现在编写一个简单的 Groovy 脚本来计算男性和女性的平均温度和标准差

1   if (args.length != 1) {
2   System.err.println "Usage: groovy Groovy15a.groovy input- file"
3   System.exit(0)
4   }
       
5   // the data:
6   // body.temp,gender,heart.rate
7   // 96.3,male,70
8   // 96.7,male,71
9   // ...
       
10   def count = [male: 0, female: 0]
11   def tempTotal = [male: 0d, female: 0d]
12   def tempSumSq = [male: 0d, female: 0d]
       
13   new File(args[0]).withReader { reader ->
14   def fieldNames = reader.
15   readLine().
16   split(',') as ArrayList<String>
17   def fieldsByName = fieldNames.
18   withIndex().
19   collectEntries { name, index ->
20   [(name): index]
21   }
22   reader.splitEachLine(',') { fieldList ->
23   double t = fieldList[fieldsByName['body.temp']] as Double
24   String g = fieldList[fieldsByName['gender']]
25   count[g]++
26   tempTotal[g] += t
27   tempSumSq[g] += (t * t)
28   }
29   }
       
30   def tempMean = tempTotal.collectEntries { k, v ->
31   int n = count[k]
32   [(k): v / n]
33   }
34   def tempStdDev = tempSumSq.collectEntries { k, v ->
35   int n = count[k]
36   double t = tempMean[k]
37   double t2 = t * t
38   [(k): Math.sqrt((v - n * t2) / (n - 1))]
39   }
       
40   println "tempMean $tempMean"
41   println "tempStdDev $tempStdDev"

当您运行它时,您将看到

$ groovy Groovy15a.groovy 
Groovy15_thermometry.csv
tempMean [male:98.1046153846154, 
female:98.39384615384616]
tempStdDev [male:0.6987557623247544, 
female:0.7434877527343278]
$

让我们仔细看看上面的代码。

第 1 到 9 行检查用法,并在下面以注释的形式为您提供要使用的数据示例。

第 10-12 行定义累加器 – count,用于记录按性别统计的温度出现次数。您可以使用tempTotal 来记录按性别统计的所有温度观测值之和。使用 tempSumSq 记录按性别统计的所有温度观测值的平方和。这里您使用映射来收集性别值。

第 13 行使用 withReader 方法在命令行上指定的文件上打开一个 Reader 实例。这接受一个 Closure 实例,并将其传递给打开的 reader。withReader 方法是 Groovy 设计模式“loan my resource”(借用我的资源)的一个示例。它处理打开和关闭 reader 的开销,以便程序员可以专注于对 reader 执行的操作。

第 14-16 行读取文件的第一行,并将其拆分为字段名称。

第 17-21 行创建一个映射,其键是字段名称,值是字段索引。我通常这样做是为了能够通过名称而不是索引来引用字段。这似乎使我的代码更不容易出错。

第 22 行迭代文件中剩余的行,将它们拆分为字段,并接受一个 Closure 实例,并将字段列表传递给它。

第 23-24 行获取温度作为 double 类型,性别作为 String 类型。

第 25-27 行按性别累加 counttempTotaltempSumSq

第 28-29 行分别结束行处理 Closure 和 reader 处理 Closure

第 30-33 行处理 tempTotal 映射,将总数除以 count 以获得按性别划分的平均值。

第 34-39 行处理 tempSumSq 映射,计算按性别划分的标准差。

第 40-41 行打印最终结果。

第 10-25 行中的代码不是最优雅的 Groovy 代码。可以合理地认为,通过对读取行产生的值集合应用 Groovy 的 inject() 来使用更函数式的方法。

您可能会编写许多脚本,这些脚本需要接收分隔符文本文件,对其行中的字段执行操作并返回结果。如果您创建一个应用“loan my resource pattern”(借用我的资源模式)的方法来处理所有开销。这将留给闭包来执行每行中必要的特定“操作”。

看看

1   if (args.length != 1) {
2   System.err.println "Usage: groovy Groovy15a.groovy input-file"
3   System.exit(0)
4   }
       
4   // the data:
6   // body.temp,gender,heart.rate
7   // 96.3,male,70
8   // 96.7,male,71
9   // ...
       
10   def reduce(fileName, delimiter, result, worker) {
11   new File(fileName).withReader { reader ->
12   def fieldNames = reader.
13   readLine().
14   split(delimiter) as ArrayList<String>
15   reader.splitEachLine(',') { fieldList ->
16   def fieldValues = fieldNames.
17   withIndex().
18   collectEntries { name, index ->
19   [(name): fieldList[index]]
20}
21   result = worker(result, fieldValues)
22   }
23   }
24   result
25   }
       
26   def intermediate = reduce(args[0], ',', [
27   count: [male: 0, female: 0],
28   tempTotal: [male: 0d, female:0d],
29   tempSumSq: [male:0d, female: 0d]
30   ]) { result, fieldValues ->
31   double t = fieldValues['body.temp'] as Double
32   String g = fieldValues['gender']
33   if (g == 'male')
34   [
35   count: [male: result.count.male + 1,
36   female: result.count.female],
37   tempTotal: [male: result.tempTotal.male + t,
38   female: result.tempTotal.female],
39   tempSumSq: [male: result.tempSumSq.male + t*t,
40   female: result.tempSumSq.female]
41   ]
42   else
43   [
44   count: [male: result.count.male,
45   female: result.count.female + 1],
46   tempTotal: [male: result.tempTotal.male,
47   female: result.tempTotal.female + t],
48   tempSumSq: [male: result.tempSumSq.male,
49   female: result.tempSumSq.female + t*t]
50   ]
51   }
       
52   def tempMean = intermediate.tempTotal.collectEntries { k, v ->
53   int n = intermediate.count[k]
54   [(k): v / n]
55   }
56   def tempStdDev = intermediate.tempSumSq.collectEntries { k, v ->
57   int n = intermediate.count[k]
58   double m = tempMean[k]
59   double m2 = m * m
60   [(k): Math.sqrt((v - n * m2) / (n - 1))]
61   }
       
62   println "tempMean $tempMean"
63   println "tempStdDev $tempStdDev"

当您运行它时,您会看到

$ groovy Groovy15b.groovy 
Groovy15_thermometry.csv
tempMean [male:98.1046153846154,
female:98.39384615384616]
tempStdDev [male:0.6987557623247544,
female:0.7434877527343278]
$

让我们回顾一下代码。

第 1 到 9 行没有改变,它只是检查用法并提供数据结构的提醒。

第 10-25 行是新的。这里您正在定义一个方法 reduce(),您将应用于输入以生成摘要数据。

第 10 行显示 reduce() 有四个参数:fileName、分隔符字符或正则表达式、结果累加器(最初应设置为“zeros”)和 worker Closure 实例。

第 11 行在 fileName 指定的文件上打开一个 Reader 实例,并将其传递给处理 reader 的 Closure 实例。

第 12-14 行读取文件的第一行,并将其拆分为字段名称。请注意,在此版本中,您不必费心构建字段名称到字段索引的映射。相反,您在处理的每一行上将字段值复制到映射中(见下文)。

第 15 行迭代文件中剩余的行,将它们拆分为字段,并接受一个 Closure 实例,并将字段值列表传递给它。

第 16-20 行将字段值复制到一个映射中,该映射的键是字段名称,值是字段值。

第 21 行调用 worker Closure 实例,将到目前为止的结果和字段值映射传递给它,并期望返回已处理行的更新结果。

第 22 行结束第 15 行开始的行处理闭包。

第 23 行结束第 11 行开始的 reader 处理闭包。

第 24 行返回处理结果。

第 25 行结束 reduce() 方法。

第 26-61 行使用 reduce() 方法来计算中间结果。详细信息

第 26-30 行调用 reduce() 方法,将文件名作为脚本的第一个参数,逗号作为分隔符,一个初始化为零的结果映射的映射,以及一个用于处理每一行的 Closure 实例。这接收到目前为止的结果。它还接收字段值映射。

第 31-32 行获取温度作为 double 类型,性别作为 String 类型。

第 33-50 行根据行中数据的性别更新男性或女性的 counttempTotaltempSumSq 中间结果。

第 51 行结束处理每一行的 Closure 实例。

第 52-55 行通过将相应的总数除以观测次数来计算男性和女性的平均温度。这以类似于第一个示例的方式完成。

第 56-61 行计算男性和女性温度的标准差。这以类似于第一个示例的方式完成。

第 62-63 行打印结果。

希望您理解了文件的管理开销,并按照我演示的方式创建了文件结构。如果您这样做,那么您可能已准备好进行下一步,即创建一个新类。您需要一个新类来包含 reduce() 方法代码。您可以在脚本中引用它。

那些觉得映射的映射结果结构笨拙的人也可以创建一个行为更像第一个示例的版本。结果被声明和初始化,Closure 实例的主体引用这些结果。

现在是建议转到 Collection、List、Map 和 Interface 的参考描述的好时机,这些描述可以在 Groovy GDK 文档中方便地找到。您还应该阅读 Groovy 关于 Closure 的文档

结论

Groovy 闭包早于 Java lambda,并提供了一些有趣的优势——它们不限于引用包含作用域中实际上是 final 的变量。它们(至少从我的角度来看)更容易学习,因为它们不依赖于涉及流、函数等的一整套新的且复杂的类。

根据我自己的经验,我发现每次在 Groovy 中编写代码时,几乎都会将闭包用作匿名函数。我很少创建像上面 reduce() 方法这样的代码,该代码调用闭包来处理其处理。尽管如此,了解如何这样做意味着在工具箱中拥有一些很棒的可重用脚本工具。

请继续关注下一个教程,您将在其中学习如何在 Groovy 中使用 sort 和 spaceship 运算符。

作者

如果您喜欢这篇文章,您可能还会喜欢这些