当 GString 将更改其 toString 表示时

When a GString will change its toString representation

我正在阅读 https://groovy-lang.org/closures.html#this 中的 Groovy 闭包文档。对 GString 行为有疑问。

  1. Closures in GStrings

文档提到了以下内容:

取以下代码:

def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'

代码的行为符合您的预期,但是如果您添加以下代码会发生什么:

x = 2
assert gs == 'x = 2'

你会看到断言失败了!这有两个原因:

a GString 仅延迟计算值的 toString 表示形式

GString 中的语法 ${x} 不代表闭包,而是代表 $x 的表达式,在创建 GString 时求值。

在我们的示例中,GString 是使用引用 x 的表达式创建的。当创建 GString 时,x 的值为 1,因此创建的 GString 的值为 1。当触发断言时,会评估 GString 并使用 toString 将 1 转换为 String。当我们把 x 改成 2 的时候,我们确实改变了 x 的值,但是它是一个不同的对象,GString 仍然引用旧的。

A GString will only change its toString representation if the values it references are mutating. If the references change, nothing will happen.

我的问题是关于上面引用的解释,在示例代码中,1显然是一个值,而不是引用类型,那么如果这个说法是正确的,它应该在GString中更新为2吧?

下面列出的下一个例子我也觉得有点困惑(最后一部分) 为什么如果我们对 Sam 进行变异以将他的名字更改为 Lucy,这次 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 被正确地改变了?在之前的评论中它只是提到

the string still evaluates to Sam because it was the value of p when the GString was created, the value of p is 'Sam' when the String was created

所以我觉得这里不应该改?? 感谢您的帮助。

这两个例子解释了两个不同的用例。在第一个示例中,表达式 "x = ${x}" 创建一个 GString 对象,该对象在内部存储 strings = ['x = ']values = [1]。您可以使用 println gs.dump():

检查此特定 GString 的内部结构
<org.codehaus.groovy.runtime.GStringImpl@6aa798b strings=[x = , ] values=[1]>

两个对象,strings 数组中的 Stringvalues 数组中的 Integer 都是 不可变的. (值是不可变的,而不是数组。)当 x 变量被分配给一个新值时,它会在内存中创建一个新对象,该对象与存储在 [=25= 中的 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 class 的用例不同。在这里你可以看到一个对象的变化是如何工作的。当您将 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]]

可以看到在nested被添加到list2时,list2存储了对nested变量所代表的内存中对象的引用。当您通过向列表中添加新数字来改变 nested 列表时,这些更改会反映在 list2 中,因为您改变了 list2 有权访问的内存中的对象。但是当你用一个新列表覆盖nested时,你创建了一个新对象,而list2在内存中与这个新对象没有任何联系。您可以向这个新的 nested 列表添加整数,并且 list2 不会受到影响 - 它在内存中存储对不同对象的引用。 (以前可以使用 nested 变量引用的对象,但稍后在代码中用新对象覆盖了此引用。)

在这种情况下,

GString 的行为类似于我上面显示的列表示例。如果您改变内插对象的状态(例如 sam.name,或将整数添加到 nested 列表),此更改会反映在 GString.toString() 中,该方法在调用该方法时会生成一个字符串。 (创建的字符串使用存储在 values 内部数组中的值的当前状态。)另一方面,如果您用新对象覆盖变量(例如 x = 2sam = new Person(name:"Adam"),或 nested = new ArrayList()),它不会改变 GString.toString() 方法产生的结果,因为它仍然使用存储在内存中的对象(或多个对象),并且之前与变量名相关联您分配给了一个新对象。

这就是 几乎 的全部内容,因为您可以使用 Closure 进行 GString 评估,而不是仅使用变量:

def gs = "x = ${x}"

您可以使用闭包 returns 变量:

def gs = "x = ${-> x}"

这意味着值 x 在 GString 更改为 String 时被评估,因此这将起作用(来自原始问题)

def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'