我正在阅读 Groovy 闭包文档 https://groovy-lang.org/closures.html#this。对GString行为有疑问。
- GStrings中的闭包
该文件提到以下内容:
采用以下代码:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码的行为符合您的预期,但是如果您添加:
x = 2
assert gs == 'x = 2'
您将看到断言失败!这有两个原因:
a GString 仅延迟计算值的 toString 表示形式
GString 中的语法 ${x} 不表示闭包,而是在创建 GString 时计算的$x表达式。
在我们的示例中,GString 是使用引用 x 的表达式创建的。创建 GString 时,x 的值为 1,因此创建 GString 的值为 1。触发断言时,将计算 GString,并使用 toString 将 1 转换为字符串。当我们将 x 更改为 2 时,我们确实更改了 x 的值,但它是一个不同的对象,并且 GString 仍然引用旧的对象。
GString 只有在它引用的值发生变化时才会更改其 toString 表示形式。如果引用更改,则不会发生任何情况。
我的问题是关于上面引用的解释,在示例代码中,1 显然是一个值,而不是引用类型,那么如果这个陈述是正确的,它应该在 GString 中更新为 2 对吗?
下面列出的下一个示例我也觉得有点困惑(最后一部分) 为什么如果我们把山姆改成露西,这次GString被正确地变异了?? 我期待它不会变异??为什么这两个示例中的行为如此不同?
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
def gs = "Name: ${p}"
assert gs == 'Name: Sam'
p = Lucy. //if we change p to Lucy
assert gs == 'Name: Sam' // the string still evaluates to Sam because it was the value of p when the GString was created
/* I would expect below to be 'Name: Sam' as well
* if previous example is true. According to the
* explanation mentioned previously.
*/
sam.name = 'Lucy' // so if we mutate Sam to change his name to Lucy
assert gs == 'Name: Lucy' // this time the GString is correctly mutated
为什么评论说"这次GString正确突变了?在之前的评论中,它只是暗示
字符串的计算结果仍然是 Sam,因为它是创建 GString 时 p 的值,创建字符串时 p 的值是"Sam">
因此我认为它不应该在这里改变?? 感谢您的帮助。
这两个示例解释了两种不同的用例。在第一个示例中,表达式"x = ${x}"
创建一个内部存储strings = ['x = ']
和values = [1]
的GString
对象。您可以使用println gs.dump()
检查此特定GString
的内部结构:
<org.codehaus.groovy.runtime.GStringImpl@6aa798b strings=[x = , ] values=[1]>
这两个对象(strings
数组中的String
对象和values
数组中的Integer
对象)都是不可变的。(值是不可变的,而不是数组。当x
变量被赋给新值时,它会在内存中创建一个与存储在GString.values
数组中的1
无关的新对象。x = 2
不是突变。这是新对象的创建。这不是Groovy特有的东西,这就是Java的工作方式。您可以尝试以下纯 Java 示例以查看其工作原理:
List<Integer> list = new ArrayList<>();
Integer number = 2;
list.add(number);
number = 4;
System.out.println(list); // prints: [2]
Person
类的用例是不同的。在这里,您可以看到对象的突变是如何工作的。当您将sam.name
更改为Lucy
时,您将改变存储在GString.values
数组中的对象的内部阶段。相反,如果您创建一个新对象并将其分配给sam
变量(例如sam = new Person(name:"Adam")
),它不会影响现有GString
对象的内部结构。存储在GString
内部的对象没有变异。在这种情况下,变量sam
仅引用内存中的不同对象。当你执行sam.name = "Lucy"
时,你会改变内存中的对象,因此GString
(使用对同一对象的引用)看到这种变化。它类似于以下普通的 Java 用例:
List<List<Integer>> list2 = new ArrayList<>();
List<Integer> nested = new ArrayList<>();
nested.add(1);
list2.add(nested);
System.out.println(list2); // prints: [[1]]
nested.add(3);
System.out.println(list2); // prints: [[1,3]]
nested = new ArrayList<>();
System.out.println(list2); // prints: [[1,3]]
您可以看到,list2
将对对象的引用存储在nested
变量表示的内存中,nested
添加到list2
中。当您通过向列表中添加新数字来更改列表nested
时,这些更改将反映在list2
中,因为您更改了list2
有权访问的内存中的对象。但是,当您用新列表覆盖nested
时,您将创建一个新对象,并且list2
与内存中的这个新对象没有任何联系。您可以将整数添加到这个新的nested
列表中,list2
不会受到影响 - 它将对不同对象的引用存储在内存中。(以前可以使用变量引用的对象nested
但稍后在代码中用新对象重写了此引用。
在这种情况下,GString
的行为类似于我上面显示的列表示例。如果改变插值对象的状态(例如sam.name
或将整数添加到nested
列表中),此更改反映在调用方法时生成字符串的GString.toString()
中。(创建的字符串使用存储在values
内部数组中的值的当前状态。另一方面,如果您用新对象覆盖变量(例如x = 2
、sam = new Person(name:"Adam")
或nested = new ArrayList()
),它不会更改GString.toString()
方法生成的内容,因为它仍然使用存储在内存中的一个或多个对象,并且该对象以前与您分配给新对象的变量名相关联。
这几乎是整个故事,因为您可以使用闭包进行 GString 评估,因此代替仅使用变量:
def gs = "x = ${x}"
您可以使用返回变量的闭包:
def gs = "x = ${-> x}"
这意味着在 GString 更改为字符串时计算x
值,因此这就可以工作(从原始问题开始)
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'