Когда GString изменит свое представление toString - PullRequest
4 голосов
/ 15 апреля 2020

Я читаю документацию закрытия 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

, синтаксис $ {x} в GString представляет не замыкание, а выражение для $ x, оценивается при создании GString.

В нашем примере GString создается с выражением, ссылающимся на x. Когда GString создается, значение x равно 1, поэтому GString создается со значением 1. Когда инициируется assert, GString оценивается и 1 преобразуется в String с помощью toString. Когда мы изменили x на 2, мы изменили значение x, но это другой объект, и GString по-прежнему ссылается на старый.

GString изменит свое представление toString, только если значения, на которые он ссылается, мутируют. Если ссылки изменятся, ничего не произойдет.

Мой вопрос касается приведенного выше объяснения, в примере кода 1, очевидно, является значением, а не ссылочным типом, тогда, если это утверждение истинно, оно должно обновиться до 2 в GString верно?

Следующий пример, приведенный ниже, меня тоже немного смущает (последняя часть), почему, если мы изменяем Сэма, чтобы изменить его имя на Люси, на этот раз 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 корректно мутировал? В предыдущих комментариях он только что упомянул

, что строка все еще оценивается как Сэм, потому что это было значение p при создании GString, значение p равно 'Sam' при создании String

таким образом, я думаю, что это не должно измениться здесь ?? Спасибо за помощь.

Ответы [ 2 ]

6 голосов
/ 15 апреля 2020

Эти два примера объясняют два разных варианта использования. В первом примере выражение "x = ${x}" создает объект GString, который внутренне хранит strings = ['x = '] и values = [1]. Вы можете проверить внутренние компоненты этого конкретного GString с помощью println gs.dump():

<org.codehaus.groovy.runtime.GStringImpl@6aa798b strings=[x = , ] values=[1]>

Оба объекта, один String один в массиве strings и Integer один в values массив неизменный . (Значения являются неизменяемыми, а не массивами.) Когда переменной x назначается новое значение, в памяти создается новый объект, который не связан с 1, хранящимся в массиве GString.values. x = 2 не является мутацией. Это создание нового объекта. Это не Groovy специфицированная c вещь, это то, как 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 в этом случае ведет себя аналогично примерам со списками I показал вам выше. Если вы изменяете состояние интерполированного объекта (например, sam.name или добавляете целые числа в список nested), это изменение отражается в GString.toString(), который создает строку при вызове метода. (Созданная строка использует текущее состояние значений, хранящихся во внутреннем массиве values.) С другой стороны, если вы переопределяете переменную новым объектом (например, x = 2, sam = new Person(name:"Adam") или nested = new ArrayList() ), он не изменит то, что создает метод GString.toString(), поскольку он по-прежнему использует объект (или объекты), который хранится в памяти и ранее был связан с именем переменной, назначенной для нового объекта.

3 голосов
/ 15 апреля 2020

Это почти всей истории, так как вы можете использовать Закрытие для оценки GString, поэтому вместо использования только переменной:

def gs = "x = ${x}"

Вы можете использовать закрытие, которое возвращает переменную:

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

Это означает, что значение x вычисляется во время изменения строки GString на String, поэтому это работает (из исходного вопроса)

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