2012-03-23 39 views
11

Như được thảo luận trong bài đăng trên blog của Eric Lippert Closing over the loop variable considered harmful, việc đóng biến vòng lặp trong C# có thể có hậu quả không mong muốn. Tôi đã cố gắng để hiểu nếu cùng một "gotcha" áp dụng cho Scala.Đóng trên biến vòng lặp trong Scala

Trước hết, vì đây là một câu hỏi Scala, tôi sẽ cố gắng giải thích Eric Lippert của C# ví dụ thêm một vài ý kiến ​​để mã của ông

// 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()); 

Hầu hết mọi người mong đợi chương trình này để in 100, 110, 120 Nó thực sự in 120, 120, 120. Vấn đề là chức năng () => v chúng tôi thêm vào danh sách funcs đóng trên v biến, chứ không phải là giá trị. Khi giá trị thay đổi v, trong vòng đầu tiên, tất cả ba đóng chúng tôi thêm vào danh sách funcs "xem" cùng biến v, mà (vào thời điểm chúng ta áp dụng chúng trong vòng thứ hai) có giá trị 120 cho tất cả chúng.

tôi đã cố gắng để dịch mã ví dụ để 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? 

Liệu Scala thực sự không bị vấn đề tương tự hoặc đã tôi chỉ dịch đang Eric Lippert của nặng và đã thất bại trong việc tái tạo nó?

Hành vi này đã vấp phải rất nhiều nhà phát triển C# dũng cảm, vì vậy tôi muốn đảm bảo không có gotchas tương tự lạ với Scala. Nhưng cũng có thể, một khi bạn hiểu tại sao C# hoạt động theo cách của nó, đầu ra của mã ví dụ của Eric Lippert có ý nghĩa (đó là cách đóng cửa hoạt động, về cơ bản): vậy Scala làm gì khác?

+2

'v' không phải là biến có thể thay đổi trong mã Scala. Hãy nhớ rằng 'for' comprehensions là _not_' for' loops. Mã Scala thực sự chuyển thành một cái gì đó có chức năng hơn rất nhiều so với một vòng lặp 'for' chuẩn, vì vậy, nơi bạn có một' v' với nhiều giá trị trong mã C#, bạn có nhiều 'v' mà mỗi giá trị của riêng chúng trong mã Scala. – Destin

+0

@Destin: cảm ơn, bạn nên đăng câu trả lời đó. Tôi sẽ có ít nhất upvoted nó. (Bạn vẫn có thể làm điều đó, thực sự) –

Trả lời

9

Scala không có cùng vấn đề vì v không phải là var, đó là val. Do đó, khi bạn viết

() => v 

trình biên dịch hiểu rằng nó được cho là tạo ra một hàm trả về giá trị tĩnh đó.

Nếu thay vào đó bạn sử dụng var, bạn có thể gặp phải vấn đề tương tự.Nhưng nó rõ ràng hơn rất nhiều rằng đây là hành vi hỏi-cho, vì bạn một cách rõ ràng tạo ra một var, và sau đó có chức năng trả lại:

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())) 

(Lưu ý rằng nếu bạn cố gắng funcs += (() => values(i)) bạn sẽ nhận được một ngoài giới hạn ngoại trừ vì bạn đã đóng trên biến số i mà, khi bạn gọi, bây giờ là 3!)

+0

cảm ơn bạn đã tái tạo cùng một hành vi trong scala. Bây giờ tôi đã nhìn thấy cách không thành ngữ scala kết quả sẽ được, tôi tự tin rằng (như bạn nói) nó không phải là một cái gì đó có khả năng xảy ra một cách tình cờ. –

5

Tương đương gần giống với ví dụ C# sẽ có vòng lặp whilevar. Nó sẽ hoạt động như trong C#.

Mặt khác, for(v <- values) funcs += (() => v) được phiên dịch sang values.foreach(v => funcs +=() => v)

chỉ để cung cấp cho tên, đó có thể là

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

Việc đóng cửa () => v xuất hiện trong cơ thể lặp lại, và những gì nó chụp không phải là một số var được chia sẻ bởi tất cả các lần lặp, nhưng đối số của cuộc gọi lặp lại, không được chia sẻ và hơn thế nữa là một giá trị không đổi thay vì một biến. Điều này ngăn chặn hành vi không trực quan.

Cũng có thể có một biến trong việc triển khai foreach, nhưng đó không phải là những gì mà việc đóng cửa nhìn thấy.

Nếu trong C#, bạn di chuyển phần thân của vòng lặp trong một phương thức riêng biệt, bạn sẽ có được hiệu ứng tương tự.

+0

eeek .... Hai câu trả lời tuyệt vời được đăng cùng lúc và tôi chỉ có thể đánh dấu một câu trả lời! Xin lỗi, tôi chỉ có thể cung cấp cho bạn +1, nhưng tôi thực sự đánh giá cao sự thấu hiểu của bạn –

1

Nếu bạn tháo rời ví dụ C#, bạn sẽ thấy rằng một lớp để giữ các biến đóng được tạo bởi trình biên dịch. Reflector làm cho rằng lớp như:

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

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

Reflector làm cho như vậy khá C#, bạn có thể không thực sự xem làm thế nào lớp đó đang được sử dụng. Để thấy rằng bạn cần phải nhìn vào IL thô.

.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 
} 

Bên trong đầu tiên, bạn có thể thấy chỉ có một trường hợp của lớp đó được tạo. Các giá trị của trình lặp được gán vào trường v công khai của cá thể đó. Danh sách funcs được điền với các đại biểu theo phương thức b__1 của đối tượng đó.

Vì vậy, về cơ bản những gì của xảy ra trong C# là

  1. Tạo một đối tượng phạm vi đóng cửa
  2. iterating trên các giá trị ...
    1. Đẩy một tham chiếu đến chức năng accessor của việc đóng cửa vào funcs
    2. Cập nhật đối tượng của phạm vi kết nối là v với giá trị hiện tại.
  3. Iterator over funcs. Mỗi cuộc gọi sẽ trả lại giá trị hiện tại của v.
2

Lưu ý rằng tính năng đọc hiểu của Scala hoạt động theo cách rất khác. Đây:

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

được dịch tại thời gian biên dịch thành này:

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

Vì vậy v là một mới biến cho mỗi giá trị.

Các vấn đề liên quan