2011-12-13 30 views
5

Tôi gặp phải kết quả không mong đợi khi thử nghiệm phương thức mở rộng đơn giản ForEach.Hành động/đại biểu có thể thay đổi giá trị đối số của nó không?

ForEach phương pháp

public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) 
{ 
    if (action == null) throw new ArgumentNullException("action"); 

    foreach (T element in list) 
    { 
     action(element); 
    } 
} 

Test phương pháp

[TestMethod] 
public void BasicForEachTest() 
{ 
    int[] numbers = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 

    numbers.ForEach(num => 
    { 
     num = 0; 
    }); 

    Assert.AreEqual(0, numbers.Sum()); 
} 

Tại sao numbers.Sum() tính bằng 55 và không phải 0?

Trả lời

5

num là bản sao của giá trị của phần tử hiện tại bạn đang lặp lại. Vì vậy, bạn chỉ cần thay đổi bản sao.

Những gì bạn làm là về cơ bản này:

foreach(int num in numbers) 
{ 
    num = 0; 
} 

Chắc chắn bạn không mong đợi điều này để thay đổi nội dung của mảng?

Sửa: Những gì bạn muốn điều này là:

for (int i in numbers.Length) 
{ 
    numbers[i] = 0; 
} 

Trong trường hợp cụ thể của bạn, bạn có thể duy trì một chỉ mục trong phương pháp khuyến nông ForEach của bạn và thông qua đó như là đối số thứ hai để hành động và sau đó sử dụng nó như thế này :

numbers.ForEachWithIndex((num, index) => numbers[index] = 0); 

Tuy nhiên nói chung: Tạo phương pháp mở rộng kiểu LINQ sửa đổi bộ sưu tập được áp dụng là kiểu xấu (IMO). Nếu bạn viết một phương pháp mở rộng mà không thể áp dụng cho một số IEnumerable<T> bạn thực sự nên suy nghĩ kỹ về nó nếu bạn thực sự cần nó (đặc biệt là khi bạn viết với ý định sửa đổi bộ sưu tập). Bạn không có nhiều để đạt được nhưng nhiều để mất (như tác dụng phụ không mong muốn). Tôi chắc chắn có những ngoại lệ nhưng tôi tuân theo quy tắc đó và nó đã phục vụ tôi tốt.

+0

@ 249076: Có điều này sẽ hiệu quả. –

+0

Tôi đồng ý những gì tôi muốn là một vòng lặp for, nhưng làm thế nào tôi sẽ làm cho nó hoạt động trong một phương pháp mở rộng nếu đối số được thông qua bởi giá trị khi gọi hành động được thực hiện? Dường như không có cách nào để chuyển giá trị bằng cách tham chiếu đến ủy nhiệm tác vụ được chuyển tới hàm ForEach. Tôi đoán mọi người đều biết điều đó, nhưng tôi đã không nhận ra rằng foreach (int num in numbers) {num = 0; } Sẽ không làm việc. Tại sao lại là một bản sao tạm thời và không phải là một tham chiếu? Tôi cho rằng foreach chỉ là cú pháp cú pháp cho "for". Tôi đoán tôi cần phải ngừng đưa ra rất nhiều giả định. – 249076

+0

Tôi muốn tìm ra một số cách để viết rằng phần mở rộng ForEach để nó có thể thay đổi giá trị của một int. Tôi không đoán điều đó có ý nghĩa, nhưng đó là một câu đố tôi muốn giải quyết. – 249076

0

intvalue type và được chuyển đến phương pháp tiện ích của bạn dưới dạng thông số giá trị. Do đó, một bản sao của numbers được chuyển đến phương thức ForEach của bạn. Các giá trị được lưu trữ trong mảng numbers được khởi tạo trong phương thức BasicForEachTest không bao giờ được sửa đổi.

Kiểm tra điều này article bởi Jon Skeet để đọc thêm về các loại giá trị và thông số giá trị.

1

Vì num là bản sao. Như thể bạn đang làm điều này:

int i = numbers[0]; 
i = 0; 

Bạn sẽ không mong đợi thay đổi số [0], phải không?

0

Tôi không tuyên bố rằng mã trong câu trả lời này hữu ích, nhưng (nó hoạt động và) tôi nghĩ nó minh họa những gì bạn cần để làm cho phương pháp tiếp cận của bạn hoạt động. Đối số phải được đánh dấu ref.BCL không có một loại đại biểu với ref, vì vậy chỉ cần viết riêng của bạn (không phải bên trong bất kỳ lớp):

public delegate void MyActionRef<T>(ref T arg); 

Cùng với đó, phương pháp của bạn trở thành:

public static void ForEach2<T>(this T[] list, MyActionRef<T> actionRef) 
{ 
    if (actionRef == null) 
    throw new ArgumentNullException("actionRef"); 

    for (int idx = 0; idx < list.Length; idx++) 
    { 
    actionRef(ref list[idx]); 
    } 
} 

Bây giờ, hãy nhớ sử dụng ref từ khóa trong phương pháp thử nghiệm của bạn:

numbers.ForEach2((ref int num) => 
{ 
    num = 0; 
}); 

này hoạt động bởi vì nó là OK để vượt qua một entry mảng ByRef (ref).

Nếu bạn muốn mở rộng IList<> thay vào đó, bạn phải làm:

public static void ForEach3<T>(this IList<T> list, MyActionRef<T> actionRef) 
{ 
    if (actionRef == null) 
    throw new ArgumentNullException("actionRef"); 

    for (int idx = 0; idx < list.Count; idx++) 
    { 
    var temp = list[idx]; 
    actionRef(ref temp); 
    list[idx] = temp; 
    } 
} 

Hope this helps hiểu biết của bạn.

Lưu ý: Tôi phải sử dụng các vòng for. Trong C#, trong foreach (var x in Yyyy) { /* ... */ }, nó không được phép gán cho x (bao gồm đi qua x ByRef (với ref hoặc out)) bên trong thân vòng lặp.

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