2009-02-04 31 views
73

Trong hai đoạn mã sau đây, là đoạn mã đầu tiên an toàn hoặc bạn phải làm điều thứ hai?Mã định danh và đóng cửa foreach

An toàn có nghĩa là mỗi luồng được đảm bảo gọi phương thức trên Foo từ cùng một vòng lặp mà trong đó chuỗi được tạo ra?

Hoặc bạn phải sao chép tham chiếu đến biến mới "cục bộ" cho mỗi lần lặp của vòng lặp?

var threads = new List<Thread>(); 
foreach (Foo f in ListOfFoo) 
{  
    Thread thread = new Thread(() => f.DoSomething()); 
    threads.Add(thread); 
    thread.Start(); 
} 

-

var threads = new List<Thread>(); 
foreach (Foo f in ListOfFoo) 
{  
    Foo f2 = f; 
    Thread thread = new Thread(() => f2.DoSomething()); 
    threads.Add(thread); 
    thread.Start(); 
} 

Cập nhật: Như đã chỉ ra trong câu trả lời của Jon Skeet, điều này không có gì đặc biệt để làm với luồng.

+0

Thực ra tôi cảm thấy nó phải làm với luồng như thể bạn không sử dụng luồng, bạn sẽ gọi đại biểu phù hợp. Trong mẫu của Jon Skeet mà không có luồng, vấn đề là có 2 vòng lặp. Đây là chỉ có một, do đó, không nên có vấn đề ... trừ khi bạn không biết chính xác khi nào mã sẽ được thực hiện (có nghĩa là nếu bạn sử dụng luồng - câu trả lời của Marc Gravell cho thấy rằng hoàn hảo). – user276648

+0

có thể trùng lặp của [Truy cập vào Sửa đổi Đóng cửa (2)] (http://stackoverflow.com/questions/304258/access-to-modified-closure-2) – nawfal

+0

@ user276648 Nó không yêu cầu luồng. Trì hoãn việc thực hiện của các đại biểu cho đến sau khi vòng lặp sẽ là đủ để có được hành vi này. – binki

Trả lời

97

Chỉnh sửa: tất cả thay đổi này trong C# 5, với thay đổi về nơi biến được xác định (trong con mắt của trình biên dịch). Từ C# 5 trở đi, chúng giống nhau.


Thứ hai là an toàn; đầu tiên là không.

Với foreach, các biến được khai báo ngoài vòng lặp - tức là

Foo f; 
while(iterator.MoveNext()) 
{ 
    f = iterator.Current; 
    // do something with f 
} 

này có nghĩa là chỉ có 1 f về phạm vi đóng cửa là, và các chủ đề rất có khả năng có thể bị lẫn lộn - gọi phương pháp nhiều lần trên một số trường hợp và hoàn toàn không phải trên các trường hợp khác. Bạn có thể khắc phục điều này với một khai báo biến thứ hai bên vòng lặp:

foreach(Foo f in ...) { 
    Foo tmp = f; 
    // do something with tmp 
} 

này sau đó có riêng tmp trong mỗi phạm vi đóng cửa, vì vậy không có nguy cơ về vấn đề này.

Dưới đây là một bằng chứng đơn giản của vấn đề:

static void Main() 
    { 
     int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 
     foreach (int i in data) 
     { 
      new Thread(() => Console.WriteLine(i)).Start(); 
     } 
     Console.ReadLine(); 
    } 

đầu ra (một cách ngẫu nhiên):

1 
3 
4 
4 
5 
7 
7 
8 
9 
9 

Thêm một biến tạm thời và nó hoạt động:

 foreach (int i in data) 
     { 
      int j = i; 
      new Thread(() => Console.WriteLine(j)).Start(); 
     } 

(mỗi số một lần, nhưng tất nhiên thứ tự không được đảm bảo)

+0

Cảm ơn bạn, Marc. Bài đăng hay. – xyz

+0

Bò thánh ... bài cũ đó đã cứu tôi rất nhiều đau đầu. Tôi luôn luôn mong đợi biến foreach được scoped bên trong vòng lặp. Đó là một trong những kinh nghiệm chính của WTF. – chris

+0

Trên thực tế, nó được coi là một lỗi trong vòng lặp foreach và cố định trong trình biên dịch. (Không giống như vòng lặp nơi biến có một cá thể duy nhất cho toàn bộ vòng lặp.) –

-5
Foo f2 = f; 

điểm để tham chiếu giống như

f 

Vì vậy, không có gì bị mất và không có gì đã đạt được ...

+0

Vấn đề là trình biên dịch thực hiện một số phép thuật ở đây để nâng biến cục bộ vào phạm vi của việc đóng cửa được tạo ra ngầm. Câu hỏi đặt ra là nếu trình biên dịch hiểu để làm điều này cho biến vòng lặp. Trình biên dịch được biết đến với một vài điểm yếu liên quan đến việc nâng nên đây là một câu hỏi hay. –

+1

Nó không phải là ma thuật. Nó chỉ đơn giản là chụp môi trường. Vấn đề ở đây và với các vòng lặp, là biến chụp được biến đổi (được gán lại). – leppie

+1

leppie: trình biên dịch tạo mã cho bạn, và nó không dễ dàng để nhìn thấy nói chung * những gì * mã này là chính xác. Đây là định nghĩa * của phép thuật biên dịch nếu bao giờ có một. –

16

nhu cầu của bạn để sử dụng tùy chọn 2, tạo ra một đóng cửa khoảng một biến thay đổi sẽ sử dụng giá trị của biến khi biến được sử dụng và không ở thời gian tạo đóng.

The implementation of anonymous methods in C# and its consequences (part 1)

The implementation of anonymous methods in C# and its consequences (part 2)

The implementation of anonymous methods in C# and its consequences (part 3)

Edit: để làm cho nó rõ ràng, trong C# đóng cửa là "đóng cửa từ vựng" có nghĩa là họ không nắm bắt giá trị của một biến nhưng biến riêng của mình. Điều đó có nghĩa là khi tạo một đóng cửa cho một biến thay đổi, việc đóng lại thực sự là một tham chiếu tới biến không phải là một bản sao của giá trị của nó.

Chỉnh sửa2: thêm liên kết vào tất cả các bài đăng trên blog nếu có ai quan tâm đến việc đọc về trình biên dịch nội bộ.

+0

Tôi nghĩ rằng nó có giá trị và kiểu tham chiếu. – leppie

33

Câu trả lời của Pop Catalin và Marc Gravell là chính xác. Tất cả những gì tôi muốn thêm là một liên kết đến my article about closures (mà nói về cả Java và C#). Chỉ nghĩ rằng nó có thể thêm một chút giá trị.

EDIT: Tôi nghĩ rằng nó đáng để đưa ra một ví dụ không có khả năng đoán trước luồng. Đây là một chương trình ngắn nhưng đầy đủ cho thấy cả hai cách tiếp cận. Danh sách "hành động xấu" in ra 10 mười lần; danh sách "hành động tốt" tính từ 0 đến 9.

using System; 
using System.Collections.Generic; 

class Test 
{ 
    static void Main() 
    { 
     List<Action> badActions = new List<Action>(); 
     List<Action> goodActions = new List<Action>(); 
     for (int i=0; i < 10; i++) 
     { 
      int copy = i; 
      badActions.Add(() => Console.WriteLine(i)); 
      goodActions.Add(() => Console.WriteLine(copy)); 
     } 
     Console.WriteLine("Bad actions:"); 
     foreach (Action action in badActions) 
     { 
      action(); 
     } 
     Console.WriteLine("Good actions:"); 
     foreach (Action action in goodActions) 
     { 
      action(); 
     } 
    } 
} 
+1

Cảm ơn - Tôi đã thêm câu hỏi để nói rằng nó không thực sự về chủ đề. – xyz

+0

Đó cũng là một trong những cuộc đàm phán mà bạn có trong video trên trang web của bạn http://csharpindepth.com/Talks.aspx – rizzle

+0

Có, tôi dường như nhớ tôi đã sử dụng một phiên bản luồng ở đó và một trong các đề xuất phản hồi là tránh các chủ đề - nó rõ ràng hơn để sử dụng một ví dụ như ở trên. –

3

Đây là một câu hỏi thú vị và có vẻ như chúng tôi đã thấy mọi người trả lời bằng nhiều cách khác nhau. Tôi đã ấn tượng rằng cách thứ hai sẽ là cách an toàn duy nhất. Tôi đã đánh dấu bằng chứng thực sự nhanh chóng:

class Foo 
{ 
    private int _id; 
    public Foo(int id) 
    { 
     _id = id; 
    } 
    public void DoSomething() 
    { 
     Console.WriteLine(string.Format("Thread: {0} Id: {1}", Thread.CurrentThread.ManagedThreadId, this._id)); 
    } 
} 
class Program 
{ 
    static void Main(string[] args) 
    { 
     var ListOfFoo = new List<Foo>(); 
     ListOfFoo.Add(new Foo(1)); 
     ListOfFoo.Add(new Foo(2)); 
     ListOfFoo.Add(new Foo(3)); 
     ListOfFoo.Add(new Foo(4)); 


     var threads = new List<Thread>(); 
     foreach (Foo f in ListOfFoo) 
     { 
      Thread thread = new Thread(() => f.DoSomething()); 
      threads.Add(thread); 
      thread.Start(); 
     } 
    } 
} 

nếu bạn chạy điều này, bạn sẽ thấy tùy chọn 1 chắc chắn không an toàn.

+0

Cảm ơn toàn bộ chương trình :) – xyz

1

Trong trường hợp của bạn, bạn có thể tránh được những vấn đề mà không sử dụng các thủ thuật sao chép bằng cách ánh xạ của bạn ListOfFoo đến một chuỗi các chủ đề:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething())); 
foreach (var t in threads) 
{ 
    t.Start(); 
}