在令人沮丧的调试会话之后阅读JLS,我发现lambdas将捕获有效最终局部变量的值,但是如果你引用一个实例变量,它会捕获对变量的引用,这对多线程代码有严重影响。
例如,以下是从更大的程序中提取的MCVE:
public class LambdaCapture
{
public static void main(String[] args) throws Exception
{
Launcher i1 = new Launcher();
i1.launchAsynchTask();
}
public static class Launcher
{
private int value = 10;
public void launchAsynchTask() throws Exception
{
System.out.printf("In launchAsynchTask value is %s\n",value);
Thread t = new Thread(()->doSomething(value));
t.start();
value = -1;
t.join();
}
public void doSomething(int value)
{
System.out.printf("In asynch task, value is %s\n",value);
}
}
}
我发现输出令人惊讶。它是
In launchAsynchTask value is 10
In asynch task, value is -1
因为我最初(在JLS研究之前)直觉地期望lambda捕获变量value
的值而不是对它的引用。
如果我必须保证捕获当前值而不是引用,那么显而易见的解决方案是创建一个本地最终临时值:
final int capture = this.value;
Thread t = new Thread(()->doSomething(capture));
我的问题:这是强制价值捕获的惯用方法,还是有其他更自然的方式来做?
答案 0 :(得分:4)
我...直观地期望lambda捕获变量值的值而不是对它的引用。
那个(捕获值)是局部变量的结果。
对于字段,实际发生的是您正在捕获对该字段所属对象实例的引用。在您的情况下,它是对my_dropdownMenu = function(...,
type = c("messages", "notifications", "tasks"),
badgeStatus = "primary", icon = NULL, .list = NULL)
{
message("Got here!")
# ...
# (COPY AND PASTE body(mydropdownMenu) HERE)
# (and then make your modifications)
}
# helper function, see
# https://stat.ethz.ch/pipermail/r-help/2008-August/171217.html
rebindPackageVar = function(pkg, name, new) {
# assignInNamespace() no longer works here, thanks nannies
ns=asNamespace(pkg)
unlockBinding(name,ns)
assign(name,new,envir=ns,inherits=F)
assign(name,new,envir=globalenv())
lockBinding(name,ns)
}
# make sure we can call non-exported functions (like validateStatus())
environment(my_dropdownMenu) = asNamespace("shinydashboard")
# now rebind the dropdownMenu function
rebindPackageVar("shinydashboard", "dropdownMenu", my_dropdownMenu);
对象的引用。 (当你声明一个内部类时会发生同样的事情。)
我的问题:这是强制价值捕获的惯用方法,还是有其他更自然的方式来做?
我想不出更好的方法。
答案 1 :(得分:2)
因为您使用速记语法,所以发生的事情并不明显。
当您编写value
来访问字段时,它隐含地表示this.value
。
lambda表达式捕获绝对最终的"局部变量" this
隐含于所有非静态方法。
lambda表达式
()->doSomething(value)
逻辑上等同于
new Lambda$1(this)
其中Lambda$1
声明为此(使用任意名称):
private static final class Lambda$1 implements Runnable {
private final Launcher ref;
Lambda$1(Launcher ref) {
this.ref = ref;
}
@Override
public void run() {
this.ref.doSomething(this.ref.value);
}
}
如您所见,lambda表达式()->doSomething(value)
实际上并未捕获value
。不合格的字段访问模糊了实际发生的情况。
仅供参考:在value
方法中隐藏参数value
后面的doSomething()
字段是一个坏主意。名称冲突使得代码非常容易被程序员误解,好的IDE会警告你(除非你禁用了那个警告)。
希望在创建MCVE时错误地发生在这里,你不会在实际代码中这样做。 : - )
答案 2 :(得分:1)
我通常喜欢的是最小化直接访问字段的代码部分,这样你就可以在这样的函数中包装启动线程的部分:
public void launchAsynchTask() throws Exception
{
System.out.printf("In launchAsynchTask value is %s\n", this.value);
Thread t = launchAsynchTaskWithValue(this.value);
this.value = -1;
t.join();
}
public Thread launchAsynchTaskWithValue(int launchValue) throws Exception
{
Thread t = new Thread(()->doSomething(launchValue));
t.start();
return t;
}