关闭Scala中的循环变量

Posted

技术标签:

【中文标题】关闭Scala中的循环变量【英文标题】:Closing over the loop variable in Scala 【发布时间】:2012-04-08 04:57:15 【问题描述】:

正如 Eric Lippert 的博文 Closing over the loop variable considered harmful 中所讨论的,在 C# 中关闭循环变量可能会产生意想不到的后果。我试图了解相同的“陷阱”是否适用于 Scala。

首先,由于这是一个 Scala 问题,我将尝试解释 Eric Lippert 的 C# 示例,在他的代码中添加一些 cmets

// 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 列表中的() =&gt; v 函数关闭了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 有什么不同呢?

【问题讨论】:

v 在 Scala 代码中不是可变变量。请记住,for 推导式 not for 循环。 Scala 代码实际上转换为本质上比标准 for 循环更实用的东西,因此,如果您有一个 v 在 C# 代码中有许多值,那么您有多个 vs,每个都有自己的单个值在 Scala 代码中。 @Destin:谢谢,您应该将其发布为答案。我至少会赞成它。 (实际上,你仍然可以这样做) 【参考方案1】:

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 += (() =&gt; values(i)),您将得到一个越界异常,因为您已经关闭了变量i,当您调用它时,它现在是3!)

【讨论】:

感谢您在 scala 中重现相同的行为。现在我已经看到生成的 scala 是多么不习惯,我相信(正如你所说)它不会偶然发生。【参考方案2】:

C# 示例的近似等效项是使用while 循环和var。它的行为就像在 C# 中一样。

另一方面,for(v &lt;- values) funcs += (() =&gt; v) 被翻译成values.foreach(v =&gt; funcs += () =&gt; v)

只是给个名字,可能是

def iteration(v: Int) = funcs += () => v)
values.foreach(iteration)

闭包() =&gt; v出现在迭代体中,捕获的不是所有迭代共享的某个var,而是迭代调用的参数,不共享,而且是一个常量值而不是一个变量。这可以防止不直观的行为。

foreach 的实现中很可能有一个变量,但它不是闭包看到的。

如果在 C# 中,您在单独的方法中移动循环体,您将获得相同的效果。

【讨论】:

eeek.... 两个很好的答案同时发布,我只能将一个标记为答案!抱歉,我只能给你 +1,但我非常感谢你的洞察力【参考方案3】:

请注意,Scala 的 for-comprehension 以非常不同的方式工作。这个:

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

在编译时被翻译成这样:

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

所以v 是每个值的变量。

【讨论】:

【参考方案4】:

如果您反汇编 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 的当前值。

【讨论】:

以上是关于关闭Scala中的循环变量的主要内容,如果未能解决你的问题,请参考以下文章

Scala:开发环境搭建变量判断循环函数集合

Scala:如何在循环中组合数据帧

Scala中的yield关键字| for / yield示例

while循环PHP中的未定义变量错误-访问[关闭]

Scala:如何在循环中合并数据帧

Scala的基础用法 和 Java相对应学习变量循环语法