仅仅使用闭包的渴望插值就表现得像一个懒惰的行为?

时间:2019-01-06 05:00:13

标签: groovy

作为学习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

很明显,该关闭已被第二次调用。闭包可能被重新执行的唯一方法是对包含的插值进行重新评估。但是,包含插值本身并不是闭包,尽管它包含闭包。那么,为什么要对其进行重新评估?

1 个答案:

答案 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并插入某个方法调用返回的值。此方法还将打印一些内容到控制台,因此我们可以查看该方法调用是急于还是懒惰地触发。

Ex.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()方法时,将执行此调用被执行)。

Ex.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...

这是您最可能寻找的示例。在此示例中,由于闭包参数,我们仍然可以利用延迟评估。但是,在这种情况下,我们使用closure's memoization feature。字符串的求值被推迟到第一次GString.toString()调用时,并且存储了闭包的结果,因此下次调用它时,它返回结果,而不是重新评估闭包。

${{->myVar1}}${->myVar1}有什么区别?

如前所述,GString.toString()方法使用checks if the given placeholder stores a closureGString.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的说明

如果尝试在Groovy 3.x中执行表达式${{->myVar1}},则将出现以下编译器错误:

${{->myVar1}}