正如Eric Lippert在博客文章 Closing over the loop variable considered harmful 中所讨论的那样,在C#中关闭循环变量会产生意想不到的后果。我试图理解是否将相同的“问题”应用于Scala。
首先,由于这是一个Scala问题,我将尝试解释Eric Lippert的C#示例,为他的代码添加一些注释
// Create a list of integers
var values = new List<int>() { 100, 110, 120 };
// Create a mutable, empty list of functions that take no input and return an int
var funcs = new List<Func<int>>();
// For each integer in the list of integers we're trying
// to add a function to the list of functions
// that takes no input and returns that integer
// (actually that's not what we're doing and there's the gotcha).
foreach(var v in values)
funcs.Add( ()=>v );
// Apply the functions in the list and print the returned integers.
foreach(var f in funcs)
Console.WriteLine(f());
大多数人都希望这个程序打印100,110,120。它实际打印120,120,120。
问题是我们添加到() => v
列表的funcs
函数会关闭v 变量,而不是v的值。当v改变值时,在第一个循环中,我们添加到funcs
列表的所有三个闭包“看到”相同的变量v,(当我们在第二个循环中应用它们时)具有所有值120他们。
我尝试将示例代码翻译为Scala:
import collection.mutable.Buffer
val values = List(100, 110, 120)
val funcs = Buffer[() => Int]()
for(v <- values) funcs += (() => v)
funcs foreach ( f => println(f()) )
// prints 100 110 120
// so Scala can close on the loop variable with no issue, or can it?
Scala确实没有遇到同样的问题,或者我只是严重翻译了Eric Lippert的代码并且无法重现它?
这种行为使许多勇敢的C#开发者绊倒了,所以我想确保Scala没有类似的奇怪问题。但是,一旦你理解了为什么C#的行为方式,Eric Lippert的示例代码的输出是有意义的(这就是闭包的工作方式,基本上):那么Scala做什么不同呢?
答案 0 :(得分:9)
Scala没有同样的问题,因为v
不是var,它是一个val。因此,当你写
() => v
编译器理解它应该生成一个返回该静态值的函数。
如果您使用var
,则可能会遇到同样的问题。但是更明确的是这是被要求的行为,因为你明确地创建了一个var,然后让函数返回它:
val values = Array(100, 110, 120)
val funcs = collection.mutable.Buffer[() => Int]()
var value = 0
var i = 0
while (i < values.length) {
value = values(i)
funcs += (() => value)
i += 1
}
funcs foreach (f => println(f()))
(请注意,如果您尝试funcs += (() => values(i))
,您将获得一个越界异常,因为您已关闭变量i
,当您调用时,它现在是3
!)< / p>
答案 1 :(得分:5)
C#示例的近似等价物是while
循环和var
。它的行为与C#相同。
另一方面,for(v <- values) funcs += (() => v)
被翻译为values.foreach(v => funcs += () => v)
只是给出名字,可能是
def iteration(v: Int) = {funcs += () => v)
values.foreach(iteration)
闭包() => v
出现在迭代体中,它捕获的不是所有迭代共享的var,而是迭代调用的参数,不是共享的,而且是一个常量值而不是变量。这可以防止不直观的行为。
foreach
的实现中可能存在变量,但它不是闭包所看到的。
如果在C#中,您以单独的方法移动循环体,则会获得相同的效果。
答案 2 :(得分:2)
请注意,Scala的for-comprehension以非常不同的方式工作。这样:
for(v <- values) funcs += (() => v)
在编译时被翻译成:
values.foreach(v => funcs += (() => v))
所以v
是每个值的 new 变量。
答案 3 :(得分:1)
如果您反汇编C#示例,您将看到编译器生成一个用于保存已关闭变量的类。 Reflector将该类呈现为:
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Fields
public int v;
// Methods
public int <Main>b__1()
{
return this.v;
}
}
Reflector渲染了这样漂亮的C#,你无法真正看到该类是如何被使用的。要看到你需要查看原始IL。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 4
.locals init (
[0] class [mscorlib]System.Collections.Generic.List`1<int32> values,
[1] class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>> funcs,
[2] class ConsoleApplication1.Program/<>c__DisplayClass2 CS$<>8__locals3,
[3] class [mscorlib]System.Func`1<int32> f,
[4] class [mscorlib]System.Collections.Generic.List`1<int32> <>g__initLocal0,
[5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32> CS$5$0000,
[6] bool CS$4$0001,
[7] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>> CS$5$0002)
L_0000: nop
L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
L_0006: stloc.s <>g__initLocal0
L_0008: ldloc.s <>g__initLocal0
L_000a: ldc.i4.s 100
L_000c: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_0011: nop
L_0012: ldloc.s <>g__initLocal0
L_0014: ldc.i4.s 110
L_0016: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_001b: nop
L_001c: ldloc.s <>g__initLocal0
L_001e: ldc.i4.s 120
L_0020: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_0025: nop
L_0026: ldloc.s <>g__initLocal0
L_0028: stloc.0
L_0029: newobj instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::.ctor()
L_002e: stloc.1
L_002f: nop
L_0030: ldloc.0
L_0031: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<!0> [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
L_0036: stloc.s CS$5$0000
L_0038: newobj instance void ConsoleApplication1.Program/<>c__DisplayClass2::.ctor()
L_003d: stloc.2
L_003e: br.s L_0060
L_0040: ldloc.2
L_0041: ldloca.s CS$5$0000
L_0043: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>::get_Current()
L_0048: stfld int32 ConsoleApplication1.Program/<>c__DisplayClass2::v
L_004d: ldloc.1
L_004e: ldloc.2
L_004f: ldftn instance int32 ConsoleApplication1.Program/<>c__DisplayClass2::<Main>b__1()
L_0055: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
L_005a: callvirt instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::Add(!0)
L_005f: nop
L_0060: ldloca.s CS$5$0000
L_0062: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>::MoveNext()
L_0067: stloc.s CS$4$0001
L_0069: ldloc.s CS$4$0001
L_006b: brtrue.s L_0040
L_006d: leave.s L_007e
L_006f: ldloca.s CS$5$0000
L_0071: constrained. [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>
L_0077: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_007c: nop
L_007d: endfinally
L_007e: nop
L_007f: nop
L_0080: ldloc.1
L_0081: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<!0> [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::GetEnumerator()
L_0086: stloc.s CS$5$0002
L_0088: br.s L_009e
L_008a: ldloca.s CS$5$0002
L_008c: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>::get_Current()
L_0091: stloc.3
L_0092: ldloc.3
L_0093: callvirt instance !0 [mscorlib]System.Func`1<int32>::Invoke()
L_0098: call void [mscorlib]System.Console::WriteLine(int32)
L_009d: nop
L_009e: ldloca.s CS$5$0002
L_00a0: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>::MoveNext()
L_00a5: stloc.s CS$4$0001
L_00a7: ldloc.s CS$4$0001
L_00a9: brtrue.s L_008a
L_00ab: leave.s L_00bc
L_00ad: ldloca.s CS$5$0002
L_00af: constrained. [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>
L_00b5: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_00ba: nop
L_00bb: endfinally
L_00bc: nop
L_00bd: ret
.try L_0038 to L_006f finally handler L_006f to L_007e
.try L_0088 to L_00ad finally handler L_00ad to L_00bc
}
在第一个foreach中,您可以看到只创建了该类的一个实例。迭代器的值被分配到该实例的公共v
字段中。 funcs
列表中填充了该对象的b__1
方法的委托。
基本上在C#中发生的是
funcs
v
。funcs
。每次调用都会返回当前值v
。