foreach vs someList.ForEach () {}


167

Rõ ràng có nhiều cách để lặp lại qua một bộ sưu tập. Tò mò nếu có bất kỳ sự khác biệt, hoặc tại sao bạn sử dụng một cách khác.

Loại thứ nhất:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Cách khác:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Tôi cho rằng ngoài đỉnh đầu của tôi, rằng thay vì đại biểu ẩn danh tôi sử dụng ở trên, bạn sẽ có một đại biểu có thể sử dụng lại mà bạn có thể chỉ định ...


Câu trả lời:


134

Có một sự khác biệt quan trọng và hữu ích giữa hai điều này.

Bởi vì .orEach sử dụng một forvòng lặp để lặp lại bộ sưu tập, điều này là hợp lệ (chỉnh sửa: trước .net 4.5 - việc triển khai đã thay đổi và cả hai đều ném):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

trong khi foreachsử dụng một điều tra viên, vì vậy điều này không hợp lệ:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: KHÔNG sao chép mã này vào ứng dụng của bạn!

Những ví dụ này không phải là cách thực hành tốt nhất, chúng chỉ để chứng minh sự khác biệt giữa ForEach()foreach.

Loại bỏ các mục khỏi danh sách trong một forvòng lặp có thể có tác dụng phụ. Một phổ biến nhất được mô tả trong các ý kiến ​​cho câu hỏi này.

Nói chung, nếu bạn đang tìm cách xóa nhiều mục khỏi danh sách, bạn sẽ muốn tách riêng việc xác định mục nào cần xóa khỏi mục xóa thực tế. Nó không giữ cho mã của bạn nhỏ gọn, nhưng nó đảm bảo rằng bạn không bỏ lỡ bất kỳ mục nào.


35
ngay cả khi đó, bạn nên sử dụng someList.Remove ALL (x => x.RemoveMe) thay vào đó
Đánh dấu Cidade

2
Với Linq, tất cả mọi thứ có thể được thực hiện tốt hơn. Tôi vừa trình bày một ví dụ về sửa đổi bộ sưu tập trong foreach ...

2
Remove ALL () là một phương thức trong Danh sách <T>.
Đánh dấu Cidade

3
Bạn có thể nhận thức được điều này, nhưng mọi người nên cẩn thận khi xóa các mục theo cách này; nếu bạn xóa mục N thì phép lặp sẽ bỏ qua mục (N + 1) và bạn sẽ không thấy mục đó trong đại biểu của mình hoặc có cơ hội xóa mục đó, giống như bạn đã thực hiện việc này trong vòng lặp của riêng mình.
El Zorko

9
Nếu bạn lặp lại danh sách ngược với vòng lặp for, bạn có thể xóa các mục mà không gặp bất kỳ vấn đề thay đổi chỉ mục nào.
S. Tarık Çetin

78

Chúng tôi đã có một số mã ở đây (trong VS2005 và C # 2.0) nơi các kỹ sư trước đã hết cách sử dụng list.ForEach( delegate(item) { foo;});thay vì foreach(item in list) {foo; };cho tất cả mã mà họ đã viết. ví dụ: một khối mã để đọc các hàng từ dataReader.

Tôi vẫn không biết chính xác tại sao họ làm điều này.

Hạn chế của list.ForEach()là:

  • Nó dài dòng hơn trong C # 2.0. Tuy nhiên, trong C # 3 trở đi, bạn có thể sử dụng =>cú pháp "" để tạo một số biểu thức ngắn gọn độc đáo.

  • Nó ít quen thuộc hơn. Những người phải duy trì mã này sẽ tự hỏi tại sao bạn lại làm theo cách đó. Tôi đã mất một lúc để quyết định rằng không có bất kỳ lý do nào, ngoại trừ có thể làm cho người viết có vẻ thông minh (chất lượng của phần còn lại của mã làm suy yếu điều đó). Nó cũng ít đọc hơn, với " })" ở cuối khối mã đại biểu.

  • Xem thêm cuốn sách "Hiệu quả C #: 50 cách cụ thể để cải thiện C # của Bill Wagner" trong đó ông nói về lý do tại sao foreach được ưa thích hơn các vòng lặp khác như trong hoặc vòng lặp - điểm chính là bạn đang để trình biên dịch quyết định cách tốt nhất để xây dựng vòng lặp. Nếu một phiên bản tương lai của trình biên dịch quản lý để sử dụng một kỹ thuật nhanh hơn, thì bạn sẽ có được điều này miễn phí bằng cách sử dụng foreach và xây dựng lại, thay vì thay đổi mã của bạn.

  • một foreach(item in list)cấu trúc cho phép bạn sử dụng breakhoặc continuenếu bạn cần thoát khỏi vòng lặp hoặc vòng lặp. Nhưng bạn không thể thay đổi danh sách trong vòng lặp foreach.

Tôi ngạc nhiên khi thấy nó list.ForEachnhanh hơn một chút. Nhưng đó có lẽ không phải là một lý do hợp lệ để sử dụng nó xuyên suốt, đó sẽ là tối ưu hóa sớm. Nếu ứng dụng của bạn sử dụng cơ sở dữ liệu hoặc dịch vụ web, chứ không phải điều khiển vòng lặp, hầu như luôn luôn là nơi thời gian trôi qua. Và bạn đã điểm chuẩn nó chống lại một forvòng lặp quá? Có list.ForEachthể nhanh hơn do sử dụng nội bộ đó và một forvòng lặp không có trình bao bọc sẽ còn nhanh hơn nữa.

Tôi không đồng ý rằng list.ForEach(delegate)phiên bản này "có nhiều chức năng hơn" theo bất kỳ cách quan trọng nào. Nó không truyền một chức năng cho một chức năng, nhưng không có sự khác biệt lớn trong kết quả hoặc tổ chức chương trình.

Tôi không nghĩ rằng foreach(item in list) "nói chính xác cách bạn muốn nó được thực hiện" - một for(int 1 = 0; i < count; i++)vòng lặp thực hiện điều đó, một foreachvòng lặp để lại sự lựa chọn kiểm soát cho trình biên dịch.

Cảm giác của tôi là, trong một dự án mới, sử dụng foreach(item in list)cho hầu hết các vòng lặp để tuân thủ cách sử dụng phổ biến và dễ đọc, và list.Foreach()chỉ sử dụng cho các khối ngắn, khi bạn có thể làm điều gì đó thanh lịch hoặc gọn nhẹ hơn với toán tử C # 3 " =>". Trong trường hợp như vậy, có thể đã có một phương thức mở rộng LINQ cụ thể hơn ForEach(). Xem Where(), Select(), Any(), All(), Max()hoặc một trong các nhiều phương pháp LINQ kia không đã làm những gì bạn muốn từ vòng lặp.


Chỉ vì tò mò ... hãy nhìn vào triển khai của Microsoft ... Referenceource.microsoft.com/#mscorlib/system/collections/ chủ
Alexandre

17

Để giải trí, tôi đã đưa Danh sách vào gương phản xạ và đây là kết quả C #:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

Tương tự, MoveNext trong Enumerator, thứ được sử dụng bởi foreach là:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach được cắt giảm nhiều hơn so với MoveNext - xử lý ít hơn nhiều - sẽ có nhiều khả năng JIT thành một thứ gì đó hiệu quả ..

Ngoài ra, foreach () sẽ phân bổ một Enumerator mới bất kể điều gì. GC là bạn của bạn, nhưng nếu bạn lặp đi lặp lại cùng một hướng dẫn, điều này sẽ tạo ra nhiều đối tượng vứt đi, trái ngược với việc tái sử dụng cùng một đại biểu - NHƯNG - đây thực sự là một trường hợp bên lề. Trong cách sử dụng thông thường, bạn sẽ thấy ít hoặc không có sự khác biệt.


1
Bạn không đảm bảo rằng mã được tạo bởi foreach sẽ giống nhau giữa các phiên bản trình biên dịch. Mã được tạo ra có thể được cải thiện bởi một phiên bản trong tương lai.
Anthony

1
Kể từ .NET Core 3.1 ForEach vẫn nhanh hơn.
jackmott

13

Tôi biết hai điều tối nghĩa làm cho chúng khác nhau. Đi tôi

Thứ nhất, có một lỗi kinh điển trong việc tạo đại biểu cho từng mục trong danh sách. Nếu bạn sử dụng từ khóa foreach, tất cả các đại biểu của bạn có thể sẽ tham khảo mục cuối cùng của danh sách:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

Phương thức List.ForEach không có vấn đề này. Mục hiện tại của phép lặp được truyền theo giá trị dưới dạng đối số cho lambda bên ngoài, và sau đó lambda bên trong nắm bắt chính xác đối số đó trong bao đóng riêng của nó. Vấn đề được giải quyết.

(Đáng buồn là tôi tin ForEach là một thành viên của Danh sách, chứ không phải là một phương thức mở rộng, mặc dù thật dễ dàng để tự xác định nó để bạn có cơ sở này trên bất kỳ loại nào.)

Thứ hai, phương pháp ForEach có một hạn chế. Nếu bạn đang triển khai IEnumerable bằng cách sử dụng lợi nhuận, bạn không thể thực hiện hoàn vốn trong lambda. Vì vậy, việc lặp qua các mục trong một bộ sưu tập để mang lại kết quả là không thể thực hiện được bằng phương pháp này. Bạn sẽ phải sử dụng từ khóa foreach và giải quyết vấn đề đóng bằng cách tạo một bản sao của giá trị vòng lặp hiện tại bên trong vòng lặp.

Thêm ở đây


5
Vấn đề bạn đề cập đến foreachlà "đã được sửa" trong C # 5. stackoverflow.com/questions/8898925/
mẹo

13

Tôi đoán someList.ForEach()cuộc gọi có thể dễ dàng song song trong khi bình thường foreachkhông dễ chạy song song. Bạn có thể dễ dàng chạy một số đại biểu khác nhau trên các lõi khác nhau, điều này không dễ thực hiện với một người bình thường foreach.
Chỉ 2 xu của tôi


2
Tôi nghĩ rằng ông có nghĩa là công cụ thời gian chạy có thể song song nó tự động. Mặt khác, cả foreach và .orEach có thể được song song bằng tay bằng cách sử dụng một luồng từ nhóm trong mỗi đại biểu hành động
Isak Savo

@Isak nhưng đó sẽ là một giả định không chính xác. Nếu phương thức ẩn danh nâng một địa phương hoặc thành viên, thời gian chạy sẽ không nhanh chóng) có thể làm tê liệt
Rune FS

6

Như họ nói, ma quỷ nằm trong chi tiết ...

Sự khác biệt lớn nhất giữa hai phương pháp liệt kê bộ sưu tập là foreachmang trạng thái, trong khi ForEach(x => { })không.

Nhưng hãy đào sâu hơn một chút, bởi vì có một số điều bạn cần lưu ý có thể ảnh hưởng đến quyết định của bạn và có một số lưu ý bạn nên biết khi viết mã cho cả hai trường hợp.

Hãy sử dụng List<T>trong thí nghiệm nhỏ của chúng tôi để quan sát hành vi. Đối với thử nghiệm này, tôi đang sử dụng .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Cho phép lặp lại điều này với foreachđầu tiên:

foreach (var name in names)
{
    Console.WriteLine(name);
}

Chúng tôi có thể mở rộng này thành:

using (var enumerator = names.GetEnumerator())
{

}

Với điều tra viên trong tay, nhìn dưới vỏ bọc chúng ta có được:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

Hai điều trở nên rõ ràng ngay lập tức:

  1. Chúng tôi được trả lại một đối tượng có trạng thái với kiến ​​thức sâu sắc về bộ sưu tập cơ bản.
  2. Bản sao của bộ sưu tập là một bản sao nông.

Điều này là tất nhiên trong không có cách nào chủ đề an toàn. Như đã chỉ ra ở trên, thay đổi bộ sưu tập trong khi lặp lại chỉ là mojo xấu.

Nhưng những gì về vấn đề của bộ sưu tập trở nên không hợp lệ trong quá trình lặp lại bằng cách bên ngoài chúng ta mucking với bộ sưu tập trong quá trình lặp lại? Thực tiễn tốt nhất đề xuất phiên bản bộ sưu tập trong các hoạt động và lặp lại, và kiểm tra các phiên bản để phát hiện khi bộ sưu tập cơ bản thay đổi.

Đây là nơi mọi thứ trở nên thực sự u ám. Theo tài liệu của Microsoft:

Nếu các thay đổi được thực hiện cho bộ sưu tập, chẳng hạn như thêm, sửa đổi hoặc xóa các phần tử, hành vi của điều tra viên không được xác định.

Vâng, điều đó có nghĩa là gì? Bằng ví dụ, chỉ vì List<T>thực hiện xử lý ngoại lệ không có nghĩa là tất cả các bộ sưu tập thực hiện IList<T>sẽ làm như vậy. Điều đó dường như là một sự vi phạm rõ ràng về Nguyên tắc thay thế Liskov:

Các đối tượng của một siêu lớp sẽ có thể thay thế bằng các đối tượng của các lớp con của nó mà không phá vỡ ứng dụng.

Một vấn đề khác là điều tra viên phải thực hiện IDisposable- điều đó có nghĩa là một nguồn rò rỉ bộ nhớ tiềm năng khác, không chỉ nếu người gọi bị sai, mà nếu tác giả không thực hiện Disposeđúng mẫu.

Cuối cùng, chúng ta có một vấn đề trọn đời ... điều gì xảy ra nếu iterator hợp lệ, nhưng bộ sưu tập cơ bản không còn nữa? Bây giờ chúng tôi là một ảnh chụp nhanh về những gì ... khi bạn tách rời vòng đời của một bộ sưu tập và các bộ lặp của nó, bạn đang yêu cầu sự cố.

Bây giờ hãy kiểm tra ForEach(x => { }):

names.ForEach(name =>
{

});

Điều này mở rộng đến:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Lưu ý quan trọng là như sau:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Mã này không phân bổ bất kỳ điều tra viên nào (không có gì Dispose) và không tạm dừng trong khi lặp.

Lưu ý rằng điều này cũng thực hiện một bản sao nông của bộ sưu tập cơ bản, nhưng bộ sưu tập hiện đang là một ảnh chụp nhanh. Nếu tác giả không thực hiện chính xác kiểm tra bộ sưu tập thay đổi hoặc sẽ 'cũ', ảnh chụp nhanh vẫn còn hiệu lực.

Điều này không theo bất kỳ cách nào bảo vệ bạn khỏi vấn đề của các vấn đề suốt đời ... nếu bộ sưu tập cơ bản biến mất, bây giờ bạn có một bản sao nông chỉ ra những gì ... nhưng ít nhất bạn không có Dispose vấn đề gì đối phó với các trình vòng lặp mồ côi ...

Vâng, tôi đã nói lặp đi lặp lại ... đôi khi nó có lợi khi có trạng thái. Giả sử bạn muốn duy trì một cái gì đó giống với con trỏ cơ sở dữ liệu ... có thể nhiều foreachkiểu Iterator<T>là cách để đi. Cá nhân tôi không thích phong cách thiết kế này vì có quá nhiều vấn đề suốt đời và bạn dựa vào những ân sủng tốt đẹp của các tác giả của các bộ sưu tập mà bạn đang dựa vào (trừ khi bạn tự viết mọi thứ từ đầu).

Luôn có lựa chọn thứ ba ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Nó không gợi cảm, nhưng nó có răng (xin lỗi Tom Cruise và bộ phim The Firm )

Đó là sự lựa chọn của bạn, nhưng bây giờ bạn biết và nó có thể là một thông tin.


5

Bạn có thể đặt tên cho đại biểu ẩn danh :-)

Và bạn có thể viết thứ hai là:

someList.ForEach(s => s.ToUpper())

Mà tôi thích, và tiết kiệm rất nhiều gõ.

Như Joachim nói, song song dễ áp ​​dụng hơn cho hình thức thứ hai.


2

Đằng sau hậu trường, đại biểu ẩn danh được biến thành một phương thức thực tế để bạn có thể có một số chi phí với lựa chọn thứ hai nếu trình biên dịch không chọn nội tuyến hàm. Ngoài ra, bất kỳ biến cục bộ nào được tham chiếu bởi phần thân của ví dụ đại biểu ẩn danh sẽ thay đổi về bản chất do các thủ thuật trình biên dịch để che giấu thực tế rằng nó được biên dịch sang một phương thức mới. Thông tin thêm ở đây về cách C # thực hiện phép thuật này:

http://bloss.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

Hàm ForEach là thành viên của Danh sách lớp chung.

Tôi đã tạo phần mở rộng sau để tạo lại mã nội bộ:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Vì vậy, kết thúc chúng tôi đang sử dụng một foreach bình thường (hoặc một vòng lặp cho nếu bạn muốn).

Mặt khác, sử dụng hàm ủy nhiệm chỉ là một cách khác để xác định hàm, mã này:

delegate(string s) {
    <process the string>
}

tương đương với:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

hoặc sử dụng biểu thức labda:

(s) => <process the string>

2

Toàn bộ phạm vi ForEach (hàm ủy nhiệm) được coi là một dòng mã duy nhất (gọi hàm) và bạn không thể đặt các điểm dừng hoặc bước vào mã. Nếu xảy ra ngoại lệ chưa xử lý, toàn bộ khối được đánh dấu.


2

List.ForEach () được coi là có nhiều chức năng hơn.

List.ForEach()nói những gì bạn muốn làm. foreach(item in list)cũng nói chính xác cách bạn muốn nó được thực hiện. Điều này để lại List.ForEachtự do thay đổi việc thực hiện phần như thế nào trong tương lai. Ví dụ: phiên bản tương lai giả định của .Net có thể luôn chạyList.ForEach song song, với giả định rằng tại thời điểm này, mọi người đều có một số lõi cpu thường không hoạt động.

Mặt khác, foreach (item in list)cung cấp cho bạn một chút kiểm soát vòng lặp. Ví dụ, bạn biết rằng các mục sẽ được lặp theo một thứ tự tuần tự nào đó và bạn có thể dễ dàng phá vỡ ở giữa nếu một mục đáp ứng một số điều kiện.


Một số nhận xét gần đây về vấn đề này có sẵn ở đây:

https://stackoverflow.com/a/529197/3043


1

Cách thứ hai bạn đã trình bày sử dụng một phương thức mở rộng để thực thi phương thức ủy nhiệm cho từng thành phần trong danh sách.

Theo cách này, bạn có một lệnh gọi đại biểu (= phương thức) khác.

Ngoài ra, có khả năng lặp lại danh sách với một vòng lặp for .


1

Một điều cần cảnh giác là làm thế nào để thoát khỏi phương thức Generic.ForEach - xem cuộc thảo luận này . Mặc dù liên kết dường như nói rằng cách này là nhanh nhất. Không chắc chắn tại sao - bạn nghĩ rằng chúng sẽ tương đương một khi được biên dịch ...


0

Có một cách, tôi đã thực hiện nó trong ứng dụng của mình:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Bạn có thể sử dụng như là mục của bạn từ foreach

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.