改变 Lambda 表达式的“自由变量”
Mutating `free variables` of Lambda Expressions
我正在阅读 this 关于 Lambda 表达式的精彩文章,但我还不清楚以下内容:
- Lambda 表达式是否将自由变量的值或 refernse/pointer 保存到每个变量? (我想答案是后者,因为如果不是,变异自由变量将是有效的)。
Don't count on the compiler to catch all concurrent access errors. The
prohibition against mutation holds only for local variables.
我不确定自我实验是否涵盖所有情况,因此我正在寻找定义明确的规则:
- 哪些自由变量可以在 Lambda 表达式 (static/properties/local variables/parameters) 中变异,哪些可以在 Lambda 表达式中使用时在外部变异?
- 在 Lambda 表达式中使用(读取或调用他的方法之一)之后,我可以在 Lambda 表达式块结束后改变 每个 自由变量吗?
Don't count on the compiler to catch all concurrent access errors. The
prohibition against mutation holds only for local variables.
If
matchesis an instance or static variable of an enclosing class, then
no error is reported, even though the result is just as undefined.
- 即使我使用同步算法,变异的结果是否未定义?
更新 1:
free variables - that is, the variables that are not parameters and not defined inside the code.
简单来说,我可以得出结论,自由变量是所有不是 Lambda 表达式的参数且未在同一 Lambda 表达式中定义的变量?
您的术语“自由变量”充其量只是一种误导。如果您不是在谈论局部变量(必须是有效的 final 才能被捕获),那么您是在谈论 堆变量.
堆变量可能是实例字段、static
字段或数组元素。对于从周围上下文对实例变量的非限定访问,lambda 表达式可以(并且将)通过捕获的 this
引用访问它们。对于其他实例字段以及数组元素,您无论如何都需要通过变量进行显式访问,因此很清楚,将如何访问堆变量。只有 static
个字段被直接访问。
规则很简单,除非被声明final
,您可以在lambda 表达式内部或外部修改所有这些规则。请记住,lambda 表达式可以调用任意方法,无论如何都包含任意代码。这是否会导致问题取决于您如何使用 lambda 表达式。您甚至可以在没有任何并发性的情况下使用不直接修改变量的函数来创建问题,例如
ArrayList<String> list=new ArrayList<>(Arrays.asList("foo", "bar"));
list.removeIf(s -> list.remove("bar"));
可能会由于正在进行的迭代中的列表修改而抛出 java.util.ConcurrentModificationException
。
同样,在并发上下文中修改变量或资源可能会破坏它,即使您确保变量本身的修改是以线程安全的方式完成的。这都是关于您正在使用的 API 的合同。
最值得注意的是,在使用并行流时,您必须意识到函数不仅由不同的线程计算,它们还在计算流的任意元素,无论它们的遇到顺序如何。对于 Stream 处理的最终结果,实现将 assemble 部分结果以重新建立遇到顺序的方式进行,如果有必要,但中间操作以任意顺序评估元素,因此您的函数不仅必须是线程安全,而且也不依赖于特定的处理顺序。在某些情况下,他们甚至可能会处理对最终结果没有贡献的元素。
由于你的项目符号 3 指的是“在块结束之后”,我想强调的是,修改(或可感知的副作用)发生在你的 lambda 表达式中的哪个位置是无关紧要的。
通常,您最好使用没有此类副作用的函数。但这并不意味着它们在一般情况下是被禁止的。
这看起来像一个简单主题的复杂 "words"。规则 pretty 与匿名 类.
非常相似
例如,编译器捕获了这个:
int x = 3;
Runnable r = () -> {
x = 6; // Local variable x defined in an enclosing scope must be final or effectively final
};
但同时这样做是完全合法的(从编译器的角度来看):
final int x[] = { 0 };
Runnable r = () -> {
x[0] = 6;
};
您提供并使用的示例matches
:
List<Path> matches = new ArrayList<>();
List<Path> files = List.of();
for (Path p : files) {
new Thread(() -> {
if (1 == 1) {
matches.add(p);
}
}).start();
}
有同样的问题。编译器 不会抱怨 关于你编辑匹配项(因为你没有更改引用 matches
- 所以它是 effectively final
);但同时这可以有undefined results
。此操作有 side-effects
,一般情况下不鼓励这样做。
未定义的结果将来自这样一个事实,即您的 matches
显然不是 thread-safe
集合。
最后一点:Does the result of the mutation is undefined even when I use a synchroniziton algorithm?
。当然不是。通过适当的同步更新变量 outside
lambda(或流)将起作用 - 但不鼓励,主要是因为还有其他方法可以实现。
编辑
好的,所以 自由变量 是那些没有在 lambda 代码本身中定义的变量 或 不是lambda 本身的参数。
在这种情况下,1) 的答案是:lambda 表达式被去糖化为方法,free-variables
的规则与匿名 类 的规则相同。这已被讨论过无数次,例如 here。这实际上也回答了第二个问题——因为规则是一样的。显然任何 final or effectively final
都可以被改变。对于原语——这意味着它们不能被改变;对于对象,您不能改变引用(但可以更改基础数据——如我的示例所示)。对于 3) - 是的。
我正在阅读 this 关于 Lambda 表达式的精彩文章,但我还不清楚以下内容:
- Lambda 表达式是否将自由变量的值或 refernse/pointer 保存到每个变量? (我想答案是后者,因为如果不是,变异自由变量将是有效的)。
Don't count on the compiler to catch all concurrent access errors. The prohibition against mutation holds only for local variables.
我不确定自我实验是否涵盖所有情况,因此我正在寻找定义明确的规则:
- 哪些自由变量可以在 Lambda 表达式 (static/properties/local variables/parameters) 中变异,哪些可以在 Lambda 表达式中使用时在外部变异?
- 在 Lambda 表达式中使用(读取或调用他的方法之一)之后,我可以在 Lambda 表达式块结束后改变 每个 自由变量吗?
Don't count on the compiler to catch all concurrent access errors. The prohibition against mutation holds only for local variables. If matchesis an instance or static variable of an enclosing class, then no error is reported, even though the result is just as undefined.
- 即使我使用同步算法,变异的结果是否未定义?
更新 1:
free variables - that is, the variables that are not parameters and not defined inside the code.
简单来说,我可以得出结论,自由变量是所有不是 Lambda 表达式的参数且未在同一 Lambda 表达式中定义的变量?
您的术语“自由变量”充其量只是一种误导。如果您不是在谈论局部变量(必须是有效的 final 才能被捕获),那么您是在谈论 堆变量.
堆变量可能是实例字段、static
字段或数组元素。对于从周围上下文对实例变量的非限定访问,lambda 表达式可以(并且将)通过捕获的 this
引用访问它们。对于其他实例字段以及数组元素,您无论如何都需要通过变量进行显式访问,因此很清楚,将如何访问堆变量。只有 static
个字段被直接访问。
规则很简单,除非被声明final
,您可以在lambda 表达式内部或外部修改所有这些规则。请记住,lambda 表达式可以调用任意方法,无论如何都包含任意代码。这是否会导致问题取决于您如何使用 lambda 表达式。您甚至可以在没有任何并发性的情况下使用不直接修改变量的函数来创建问题,例如
ArrayList<String> list=new ArrayList<>(Arrays.asList("foo", "bar"));
list.removeIf(s -> list.remove("bar"));
可能会由于正在进行的迭代中的列表修改而抛出 java.util.ConcurrentModificationException
。
同样,在并发上下文中修改变量或资源可能会破坏它,即使您确保变量本身的修改是以线程安全的方式完成的。这都是关于您正在使用的 API 的合同。
最值得注意的是,在使用并行流时,您必须意识到函数不仅由不同的线程计算,它们还在计算流的任意元素,无论它们的遇到顺序如何。对于 Stream 处理的最终结果,实现将 assemble 部分结果以重新建立遇到顺序的方式进行,如果有必要,但中间操作以任意顺序评估元素,因此您的函数不仅必须是线程安全,而且也不依赖于特定的处理顺序。在某些情况下,他们甚至可能会处理对最终结果没有贡献的元素。
由于你的项目符号 3 指的是“在块结束之后”,我想强调的是,修改(或可感知的副作用)发生在你的 lambda 表达式中的哪个位置是无关紧要的。
通常,您最好使用没有此类副作用的函数。但这并不意味着它们在一般情况下是被禁止的。
这看起来像一个简单主题的复杂 "words"。规则 pretty 与匿名 类.
非常相似例如,编译器捕获了这个:
int x = 3;
Runnable r = () -> {
x = 6; // Local variable x defined in an enclosing scope must be final or effectively final
};
但同时这样做是完全合法的(从编译器的角度来看):
final int x[] = { 0 };
Runnable r = () -> {
x[0] = 6;
};
您提供并使用的示例matches
:
List<Path> matches = new ArrayList<>();
List<Path> files = List.of();
for (Path p : files) {
new Thread(() -> {
if (1 == 1) {
matches.add(p);
}
}).start();
}
有同样的问题。编译器 不会抱怨 关于你编辑匹配项(因为你没有更改引用 matches
- 所以它是 effectively final
);但同时这可以有undefined results
。此操作有 side-effects
,一般情况下不鼓励这样做。
未定义的结果将来自这样一个事实,即您的 matches
显然不是 thread-safe
集合。
最后一点:Does the result of the mutation is undefined even when I use a synchroniziton algorithm?
。当然不是。通过适当的同步更新变量 outside
lambda(或流)将起作用 - 但不鼓励,主要是因为还有其他方法可以实现。
编辑
好的,所以 自由变量 是那些没有在 lambda 代码本身中定义的变量 或 不是lambda 本身的参数。
在这种情况下,1) 的答案是:lambda 表达式被去糖化为方法,free-variables
的规则与匿名 类 的规则相同。这已被讨论过无数次,例如 here。这实际上也回答了第二个问题——因为规则是一样的。显然任何 final or effectively final
都可以被改变。对于原语——这意味着它们不能被改变;对于对象,您不能改变引用(但可以更改基础数据——如我的示例所示)。对于 3) - 是的。