Định danh foreach và đóng


79

Trong hai đoạn mã sau, đoạn đầu tiên là an toàn hay bạn phải làm đoạn thứ hai?

Ý tôi là mỗi luồng có được đảm bảo gọi phương thức trên Foo từ cùng một lần lặp vòng lặp mà luồng được tạo không?

Hay bạn phải sao chép tham chiếu đến một 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 liên quan cụ thể đến luồng.


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


@ user276648 Nó không yêu cầu phân luồng. Trì hoãn việc thực thi các ủy quyền cho đến sau vòng lặp sẽ đủ để có hành vi này.
binki

Câu trả lời:


103

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


Trước C # 5

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

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

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

Điều này có nghĩa là chỉ có 1 fvề phạm vi đóng và các luồng rất có thể bị nhầm lẫn - gọi phương thức nhiều lần trên một số trường hợp chứ không phải trên các trường hợp khác. Bạn có thể sửa lỗi này bằng một khai báo biến thứ hai bên trong vòng lặp:

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

Điều này sau đó có một sự riêng biệt tmptrong mỗi phạm vi đóng, vì vậy không có rủi ro về vấn đề này.

Đây là một bằng chứng đơn giản về 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();
    }

Kết quả đầu ra (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)


Chúa ơi ... cái bài cũ đó đỡ nhức đầu quá. Tôi luôn mong đợi biến foreach được xác định phạm vi bên trong vòng lặp. Đó là một kinh nghiệm chính của WTF.
chris

Trên thực tế, đó được coi là một lỗi trong vòng lặp foreach và được sửa trong trình biên dịch. (Không giống như cho-loop nơi biến có trường hợp duy nhất cho toàn bộ vòng lặp.)
Ihar Bury

@Orlangur Tôi đã trò chuyện trực tiếp với Eric, Mads và Anders về vấn đề này trong nhiều năm. Trình biên dịch tuân theo các thông số kỹ thuật như vậy là đúng. Các thông số kỹ thuật đã đưa ra một sự lựa chọn. Đơn giản: lựa chọn đó đã được thay đổi.
Marc Gravell

Câu trả lời này áp dụng cho C # 4, nhưng không áp dụng cho các phiên bản sau: "Trong C # 5, biến vòng lặp của một foreach sẽ nằm bên trong vòng lặp một cách hợp lý và do đó, các bao đóng sẽ đóng trên một bản sao mới của biến mỗi lần." ( Eric Lippert )
Douglas

@Douglas vâng, tôi đã sửa những điều này khi chúng tôi bắt đầu, nhưng đó là một trở ngại phổ biến, vì vậy: có khá nhiều việc phải làm!
Marc Gravell

37

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

CHỈNH SỬA: Tôi nghĩ rằng nó đáng để đưa ra một ví dụ không có sự khó đoán của 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 lần; danh sách "hành động tốt" được 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

Đây cũng là một trong các cuộc đàm phán mà bạn có trong video trên trang web của bạn csharpindepth.com/Talks.aspx
missaghi

Có, tôi dường như nhớ rằng tôi đã sử dụng một phiên bản phân luồng ở đó và một trong những đề xuất phản hồi là tránh các chuỗi - sẽ rõ ràng hơn nếu sử dụng một ví dụ như ở trên.
Jon Skeet

Rất vui được biết các đoạn video đang nhận được quan sát mặc dù :)
Jon Skeet

Ngay cả khi hiểu rằng biến tồn tại bên ngoài forvòng lặp, hành vi này cũng khiến tôi bối rối. Ví dụ: trong ví dụ của bạn về hành vi đóng, stackoverflow.com/a/428624/20774 , biến tồn tại bên ngoài bao đóng nhưng liên kết chính xác. Tại sao điều này lại khác nhau?
James McMahon

17

Bạn cần sử dụng tùy chọn 2, việc tạo bao đóng xung quanh một biến đang thay đổi sẽ sử dụng giá trị của biến khi biến được sử dụng chứ không phải tại thời điểm tạo bao đóng.

Việc triển khai các phương thức ẩn danh trong C # và hậu quả của nó (phần 1)

Việc triển khai các phương thức ẩn danh trong C # và hậu quả của nó (phần 2)

Việc triển khai các phương thức ẩn danh trong C # và hậu quả của nó (phần 3)

Chỉnh sửa: để làm rõ hơn, trong C # các bao đóng là "bao đóng từ vựng " nghĩa là chúng không nắm bắt giá trị của một biến mà là chính biến đó. Điều đó có nghĩa là khi tạo một bao đóng cho một biến đang thay đổi, bao đóng thực sự là một tham chiếu đến biến không phải là bản sao của giá trị đó.

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


Tôi nghĩ rằng điều đó phù hợp với các loại giá trị và tham chiếu.
leppie

3

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

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 nó, bạn sẽ thấy tùy chọn 1 được xác định là không an toàn.


1

Trong trường hợp của bạn, bạn có thể tránh sự cố mà không cần sử dụng thủ thuật sao chép bằng cách ánh xạ của bạn ListOfFootới một chuỗi các chuỗi:

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


-5
Foo f2 = f;

trỏ đến cùng một tham chiếu như

f 

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


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

1
leppie: trình biên dịch tạo mã cho bạn và nhìn chung không dễ để biết mã này chính xác là gì. Đây là các định nghĩa của trình biên dịch ma thuật nếu bao giờ có một.
Konrad Rudolph

3
@leppie: Tôi với Konrad đây. Độ dài mà trình biên dịch trải qua để cảm thấy giống như ma thuật, và mặc dù ngữ nghĩa được xác định rõ ràng nhưng chúng không được hiểu rõ. Câu nói cũ về bất cứ thứ gì không được hiểu rõ có thể so sánh với ma thuật là gì?
Jon Skeet

2
@Jon Skeet Ý của bạn là "bất kỳ công nghệ đủ tiên tiến nào đều không thể phân biệt được với ma thuật" en.wikipedia.org/wiki/Clarke%27s_three_laws :)
AwesomeTown

2
Nó không trỏ đến một tham chiếu. Nó là một tài liệu tham khảo. Nó trỏ đến cùng một đối tượng, nhưng nó là một tham chiếu khác.
mqp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.