2012-03-23 169 views
11

如Eric Lippert的博客文章Closing over the loop variable considered harmful中所述,關閉C#中的循環變量可能會產生意想不到的後果。我試圖瞭解是否將相同的「陷阱」應用於Scala關閉Scala中的循環變量

首先,因爲這是一個Scala的問題,我會盡力解釋埃裏克利珀的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。 問題是我們添加到funcs列表() => v函數關閉的變量變量,而不是v的。當v改變值時,在第一個循環中,我們添加到funcs列表中的所有三個閉包「看到」相同的變量v,(當我們在第二個循環中應用它們時)對於它們全部具有值120。

我試着翻譯示例代碼來斯卡拉:

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? 

斯卡拉是否確實不是來自同一個問題遭受或曾經我只是翻譯埃裏克利珀的代碼不好,未能重現呢?

這種行爲已經絆倒了很多勇敢的C#開發人員,所以我想確保沒有奇怪的與Scala類似的陷阱。但是,一旦你明白了爲什麼C#的行爲如此,那麼Eric Lippert的示例代碼類型的輸出就是有意義的(基本上這就是閉包的工作方式):那麼Scala的行爲有什麼不同呢?

+2

'v'不是Scala代碼中的可變變量。請記住,'for'的理解是''不''循環。實際上,Scala代碼本質上比標準的'for'循環更具功能性,因此,在C#代碼中有一個'v'的值很多的情況下,你有多個'v's,每個''都有自己的單值在Scala代碼中。 – Destin 2012-03-23 16:15:47

+0

@Destin:謝謝,你應該發佈這個答案。我至少會投票贊成。 (你仍然可以這樣做,實際上) – 2012-03-23 16:38:51

回答

9

Scala沒有相同的問題,因爲v不是var,它是val。因此,當你寫

() => v 

編譯器明白它應該產生一個返回該靜態值的函數。

如果您改爲使用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!)

+0

謝謝你在scala中重現相同的行爲。現在我已經看到了由此產生的scala會有多麼地道,我相信(正如你所說)它不會偶然發生。 – 2012-03-23 16:46:13

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#中,您在單獨的方法中移動循環的主體,您會得到相同的效果。

+0

eeek ....在同一時間發佈兩個很好的答案,我可以標記只有一個答案!對不起,我只能給你+1,但我真的很感謝你的洞察 – 2012-03-23 16:52:47

1

如果你反彙編C#的例子,你會發現編譯器會生成一個保存關閉變量的類。 Reflector將呈現類,如:

[CompilerGenerated] 
private sealed class <>c__DisplayClass2 
{ 
    // Fields 
    public int v; 

    // Methods 
    public int <Main>b__1() 
    { 
     return this.v; 
    } 
} 

反射使得這種漂亮的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#中出現的情況是

  1. 在值創建一個封閉範圍對象
  2. 迭代...
    1. 推到閉合的存取函數的引用到funcs
    2. 更新關閉範圍對象的v與當前值。
  3. Iterator over funcs。每次通話將返回當前值v
2

請注意,Scala的理解工作方式非常不同。這:

for(v <- values) funcs += (() => v) 

翻譯在編譯時到這一點:

values.foreach(v => funcs += (() => v)) 

所以v是每個價值變量。