作为学习Groovy的一部分,我正在尝试探索由字符串插值提供的所有复杂可能性。 我的一个小实验给出的结果对我来说是没有意义的,现在我想知道我是否完全误解了Groovy中的惰性和急切插值的基本概念。
这是我运行的代码:
def myVar1 = 3
// An eager interpolation containing just a closure.
def myStr = "${{->myVar1}}"
print ("Just after the creation of myStr\n")
print (myStr as String)
myVar1 += 1 // Bump up myVar1.
print ("\nJust after incrementing myVar1\n")
print (myStr as String)
这是我得到的输出:
Just after the creation of myStr
3
Just after incrementing myVar1
4
很明显,该关闭已被第二次调用。闭包可能被重新执行的唯一方法是对包含的插值进行重新评估。但是,包含插值本身并不是闭包,尽管它包含闭包。那么,为什么要对其进行重新评估?
答案 0 :(得分:3)
这是GString.toString()
方法的实现方式。如果您查看GString
类的at the source code,将会发现类似以下内容:
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并插入某个方法调用返回的值。此方法还将打印一些内容到控制台,因此我们可以查看该方法调用是急于还是懒惰地触发。
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
。
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()
方法时,将执行此调用被执行)。
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...
这是您最可能寻找的示例。在此示例中,由于闭包参数,我们仍然可以利用延迟评估。但是,在这种情况下,我们使用closure's memoization feature。字符串的求值被推迟到第一次GString.toString()
调用时,并且存储了闭包的结果,因此下次调用它时,它返回结果,而不是重新评估闭包。
${{->myVar1}}
和${->myVar1}
有什么区别?如前所述,GString.toString()
方法使用checks if the given placeholder stores a closure的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
如您所见,在第一个示例和第三个示例中,它做的完全相同-评估了表达式并将其存储在类型为GString.values
的{{1}}数组中。这是关键部分:类似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
方法调用。
现在,当我们最终调用调用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}
这样的表达式的原因。在第一种情况下,GString初始化不评估闭包表达式并将其直接放置到values数组中,在第二个示例中,占位符被求值,它评估的表达式创建并返回闭包,然后将其存储在values数组中。
如果尝试在Groovy 3.x中执行表达式${{->myVar1}}
,则将出现以下编译器错误:
${{->myVar1}}