Так реализован метод GString.toString()
. Если вы посмотрите на исходный код класса GString
, вы найдете что-то вроде этого:
public String toString() {
StringWriter buffer = new StringWriter();
try {
writeTo(buffer);
}
catch (IOException e) {
throw new StringWriterIOException(e);
}
return buffer.toString();
}
public Writer writeTo(Writer out) throws IOException {
String[] s = getStrings();
int numberOfValues = values.length;
for (int i = 0, size = s.length; i < size; i++) {
out.write(s[i]);
if (i < numberOfValues) {
final Object value = values[i];
if (value instanceof Closure) {
final Closure c = (Closure) value;
if (c.getMaximumNumberOfParameters() == 0) {
InvokerHelper.write(out, c.call());
} else if (c.getMaximumNumberOfParameters() == 1) {
c.call(out);
} else {
throw new GroovyRuntimeException("Trying to evaluate a GString containing a Closure taking "
+ c.getMaximumNumberOfParameters() + " parameters");
}
} else {
InvokerHelper.write(out, value);
}
}
}
return out;
}
Обратите внимание, что метод writeTo
проверяет, какие значения передаются для интерполяции, и в случае закрытия он вызывает его. Именно так GString выполняет ленивую оценку интерполированных значений.
Теперь давайте рассмотрим несколько примеров. Давайте предположим, что мы хотим напечатать GString и интерполировать значение, возвращаемое некоторым вызовом метода. Этот метод также выводит что-то на консоль, поэтому мы можем видеть, был ли вызов метода активным или ленивым.
Пример 1: Жадная оценка
class GStringLazyEvaluation {
static void main(String[] args) {
def var = 1
def str = "${loadValue(var++)}"
println "Starting the loop..."
5.times {
println str
}
println "Loop ended..."
}
static Integer loadValue(int val) {
println "This method returns value $val"
return val
}
}
Выход:
This method returns value 1
Starting the loop...
1
1
1
1
1
Loop ended...
Поведение по умолчанию. Метод loadValue()
был вызван до того, как мы распечатали str
на консоли.
Пример 2: Ленивая оценка
class GStringLazyEvaluation {
static void main(String[] args) {
def var = 1
def str = "${ -> loadValue(var++)}"
println "Starting the loop..."
5.times {
println str
}
println "Loop ended..."
}
static Integer loadValue(int val) {
println "This method returns value $val"
return val
}
}
Выход:
Starting the loop...
This method returns value 1
1
This method returns value 2
2
This method returns value 3
3
This method returns value 4
4
This method returns value 5
5
Loop ended...
Во втором примере мы используем ленивую оценку. Мы определяем str
с помощью замыкания, которое вызывает метод loadValue()
, и этот вызов выполняется, когда мы явно выводим str
на консоль (точнее, когда выполняется метод GString.toString()
).
Пример 3: Ленивая оценка и напоминание о закрытии
class GStringLazyEvaluation {
static void main(String[] args) {
def var = 1
def closure = { -> loadValue(var++)}
def str = "${closure.memoize()}"
println "Starting the loop..."
5.times {
println str
}
println "Loop ended..."
}
static Integer loadValue(int val) {
println "This method returns value $val"
return val
}
}
Выход:
Starting the loop...
This method returns value 1
1
1
1
1
1
Loop ended...
А вот пример, который вы, скорее всего, ищете. В этом примере мы по-прежнему используем ленивую оценку благодаря параметру закрытия. Однако в этом случае мы используем функцию запоминания закрытия . Оценка строки откладывается до первого GString.toString()
вызова, и результат закрытия запоминается, поэтому при следующем вызове он возвращает результат, а не переоценивает замыкание.
В чем разница между ${{->myVar1}}
и ${->myVar1}
?
Как было упомянуто ранее, метод GString.toString()
использует GString.writeTo(out)
, который проверяет, сохраняет ли данный заполнитель замыкание для отложенной оценки. Каждый экземпляр GString хранит значения заполнителя в массиве GString.values
, и он инициализируется во время инициализации GString. Давайте рассмотрим следующий пример:
def str = "${myVar1} ... ${-> myVar1} ... ${{-> myVar1}}"
Теперь давайте посмотрим GString.values
инициализация массива:
${myVar1} --> evaluates `myVar1` expression and copies its return value to the values array
${-> myVar1} --> it sees this is closure expression so it copies the closure to values array
${{-> myVar1}} --> evaluates `{-> myVar1}` which is closure definition expression in this case and copies its return value (a closure) to the values array
Как вы можете видеть, в 1-м и 3-м примере он делал то же самое - он вычислял выражение и сохранял его в массиве GString.values
типа Object[]
. И вот важная часть: выражение типа {->something}
не является выражением вызова замыкания . Выражение, которое оценивает замыкание:
{->myVar1}()
или
{->myVar1}.call()
Это можно проиллюстрировать на следующем примере:
def str = "${println 'B'; 2 * 4} ${{ -> println 'C'; 2 * 5}} ${{ -> println 'A'; 2 * 6}.call()}"
println str
Значение инициализации выглядит следующим образом:
${println 'B'; 2 * 4} ---> evaluates the expression which prints 'B' and returns 8 - this value is stored in values array.
${{ -> println 'C'; 2 * 5}} ---> evaluates the expression which is nothing else than creation of a closure. This closure is stored in the values array.
${{ -> println 'A'; 2 * 6}.call()}" ---> evaluates the expression which creates a closure and then calls it explicitely. It prints 'A' and returns 12 which is stored in the values array at the last index.
Именно поэтому после GString
инициализации объекта мы получаем массив values
, например:
[8, script$_main_closure1, 12]
Теперь создание этого GString
вызвало побочный эффект - на консоли отображаются следующие символы:
B
A
Это потому, что оценка 1-го и 3-го значений вызвала println
вызов метода.
Теперь, когда мы наконец вызовем println str
, который вызывает метод GString.toString()
, все значения обрабатываются. Когда начинается процесс интерполяции, он делает следующее:
value[0] --> 8 --> writes "8"
value[1] --> script$_main_closure1 --> invoke script$_main_closure1.call() --> prints 'C' --> returns 10 --> 10 --> writes "10"
value[2] --> 12 --> writes "12"
Вот почему окончательный вывод на консоль выглядит так:
B
A
C
8 10 12
Вот почему на практике выражения типа ${->myVar1}
и ${{->myVar1}}
похожи. В первом случае инициализация GString не оценивает выражение замыкания и помещает его непосредственно в массив значений, во втором примере выполняется заполнение заполнителя, а вычисляемое выражение создает и возвращает замыкание, которое затем сохраняется в массиве значений.
Примечание к Groovy 3.x
Если вы попытаетесь выполнить выражение ${{->myVar1}}
в Groovy 3.x, вы получите следующую ошибку компилятора:
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
General error during conversion: java.lang.NullPointerException
java.lang.NullPointerException
at org.apache.groovy.parser.antlr4.AstBuilder.lambda$visitGstring$28(AstBuilder.java:3579)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at org.apache.groovy.parser.antlr4.AstBuilder.visitGstring(AstBuilder.java:3591)
at org.apache.groovy.parser.antlr4.AstBuilder.visitGstring(AstBuilder.java:356)
at org.apache.groovy.parser.antlr4.GroovyParser$GstringContext.accept(GroovyParser.java:4182)
at groovyjarjarantlr4.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:20)
at org.apache.groovy.parser.antlr4.AstBuilder.visit(AstBuilder.java:4287)
.....
at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:565)
at org.codehaus.groovy.tools.FileSystemCompiler.compile(FileSystemCompiler.java:72)
at org.codehaus.groovy.tools.FileSystemCompiler.doCompilation(FileSystemCompiler.java:240)
at org.codehaus.groovy.tools.FileSystemCompiler.commandLineCompile(FileSystemCompiler.java:163)
at org.codehaus.groovy.tools.FileSystemCompiler.commandLineCompileWithErrorHandling(FileSystemCompiler.java:203)
at org.codehaus.groovy.tools.FileSystemCompiler.main(FileSystemCompiler.java:187)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.tools.GroovyStarter.rootLoader(GroovyStarter.java:114)
at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:136)
1 error