Lọc các vòng lặp foreach với điều kiện nơi vs tiếp tục các mệnh đề bảo vệ


24

Tôi đã thấy một số lập trình viên sử dụng điều này:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

thay vì nơi tôi thường sử dụng:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Tôi thậm chí đã nhìn thấy một sự kết hợp của cả hai. Tôi thực sự thích khả năng đọc với 'tiếp tục', đặc biệt với các điều kiện phức tạp hơn. Thậm chí có một sự khác biệt trong hiệu suất? Với một truy vấn cơ sở dữ liệu tôi giả sử sẽ có. Những gì về danh sách thường xuyên?


3
Đối với danh sách thường xuyên, nó có vẻ như tối ưu hóa vi mô.
ngày tận thế

2
@zgnilec: ... nhưng thực sự thì một trong hai biến thể là phiên bản tối ưu hóa? Tôi có ý kiến ​​về điều đó, tất nhiên, nhưng từ việc chỉ nhìn vào mã này, điều này vốn không rõ ràng cho tất cả mọi người.
Doc Brown

2
Tất nhiên tiếp tục sẽ nhanh hơn. Sử dụng linq. Nơi bạn tạo vòng lặp bổ sung.
ngày tận thế

1
@zgnilec - Lý thuyết tốt. Quan tâm để gửi một câu trả lời giải thích lý do tại sao bạn nghĩ rằng? Cả hai câu trả lời hiện đang nói ngược lại.
Bobson

2
... Vì vậy, điểm mấu chốt là: sự khác biệt hiệu năng giữa hai cấu trúc là không thể bỏ qua, và khả năng đọc cũng như khả năng sửa lỗi có thể đạt được cho cả hai. Nó chỉ đơn giản là một vấn đề của hương vị mà bạn thích.
Doc Brown

Câu trả lời:


63

Tôi sẽ coi đây là một nơi thích hợp để sử dụng phân tách lệnh / truy vấn . Ví dụ:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

Điều này cũng cho phép bạn đưa ra một tên tự viết tài liệu tốt cho kết quả truy vấn. Nó cũng giúp bạn nhìn thấy các cơ hội để tái cấu trúc, bởi vì việc tái cấu trúc mã dễ dàng hơn nhiều khi chỉ truy vấn dữ liệu hoặc chỉ làm thay đổi dữ liệu so với mã hỗn hợp cố gắng thực hiện cả hai.

Khi gỡ lỗi, bạn có thể ngắt trước foreachđể nhanh chóng kiểm tra xem nội dung validItemsgiải quyết theo những gì bạn mong đợi. Bạn không cần phải bước vào lambda trừ khi bạn cần. Nếu bạn cần phải bước vào lambda, thì tôi khuyên bạn nên đưa nó vào một chức năng riêng biệt, sau đó bước qua đó.

Có sự khác biệt trong hiệu suất? Nếu truy vấn được hỗ trợ bởi cơ sở dữ liệu, thì phiên bản LINQ có khả năng chạy nhanh hơn, vì truy vấn SQL thể hiệu quả hơn. Nếu đó là LINQ to Object, thì bạn sẽ không thấy bất kỳ sự khác biệt hiệu suất thực sự nào. Như mọi khi, hồ sơ mã của bạn và sửa chữa các nút thắt thực sự được báo cáo, thay vì cố gắng dự đoán trước sự tối ưu hóa.


1
Tại sao một tập dữ liệu cực lớn sẽ tạo ra sự khác biệt? Chỉ vì chi phí rất nhỏ của lambdas cuối cùng sẽ tăng lên?
BlueRaja - Danny Pflughoeft

1
@ BlueRaja-DannyPflughoeft: Có, bạn đúng, ví dụ này không liên quan đến thuật toán phức tạp ngoài mã gốc. Tôi đã loại bỏ cụm từ.
Christian Hayter

Điều này không dẫn đến hai lần lặp lại trong bộ sưu tập? Đương nhiên, cái thứ hai ngắn hơn, chỉ xem xét các mục hợp lệ trong đó, nhưng bạn vẫn cần thực hiện hai lần, một lần để lọc ra các phần tử, lần thứ hai để làm việc với các mục hợp lệ.
Andy

1
@DavidPacker số. Chỉ IEnumerableđược điều khiển bởi foreachvòng lặp.
Benjamin Hodgson

2
@DavidPacker: Đó chính xác là những gì nó làm; hầu hết các phương thức LINQ to Object được triển khai bằng các khối lặp. Mã ví dụ ở trên sẽ lặp qua bộ sưu tập chính xác một lần, thực thi Wherelambda và thân vòng lặp (nếu lambda trả về true) một lần cho mỗi phần tử.
Christian Hayter

7

Tất nhiên, có một sự khác biệt về hiệu suất, .Where()kết quả là một cuộc gọi đại biểu được thực hiện cho mỗi mục duy nhất. Tuy nhiên, tôi sẽ không lo lắng về hiệu suất:

  • Các chu kỳ đồng hồ được sử dụng trong việc gọi một đại biểu là không đáng kể so với các chu kỳ đồng hồ được sử dụng bởi phần còn lại của mã lặp lại trong bộ sưu tập và kiểm tra các điều kiện.

  • Hình phạt về hiệu suất của việc triệu tập một đại biểu là theo thứ tự của một vài chu kỳ đồng hồ, và may mắn thay, chúng ta đã qua những ngày mà chúng ta phải lo lắng về các chu kỳ đồng hồ riêng lẻ.

Nếu vì một lý do nào đó, hiệu suất thực sự quan trọng đối với bạn ở cấp độ chu kỳ đồng hồ, thì hãy sử dụng List<Item>thay vì IList<Item>, để trình biên dịch có thể sử dụng các cuộc gọi trực tiếp (và không thể thực hiện được) thay vì các cuộc gọi ảo và List<T>thực sự là trình lặp của a struct, không phải đóng hộp. Nhưng đó thực sự là những thứ tầm thường.

Một truy vấn cơ sở dữ liệu là một tình huống khác nhau, vì (ít nhất là về lý thuyết) có khả năng gửi bộ lọc đến RDBMS, do đó cải thiện hiệu năng rất nhiều: chỉ các hàng khớp sẽ thực hiện chuyến đi từ RDBMS đến chương trình của bạn. Nhưng vì điều đó tôi nghĩ rằng bạn sẽ phải sử dụng linq, tôi không nghĩ biểu thức này có thể được gửi đến RDBMS như vậy.

Bạn sẽ thực sự thấy được lợi ích của if(x) continue;thời điểm bạn phải gỡ lỗi mã này: Bước đơn trên if()s và continues hoạt động độc đáo; bước đơn vào đại biểu lọc là một nỗi đau.


Đó là khi có điều gì đó không ổn và bạn muốn xem tất cả các mục và kiểm tra trình gỡ lỗi những mục nào có Trường! = Null và mục nào có Trạng thái! = Null; điều này có thể khó thực hiện với foreach ... ở đâu.
gnasher729

Điểm tốt với gỡ lỗi. Bước vào một nơi không tệ trong Visual Studio, nhưng bạn không thể viết lại các biểu thức lambda trong khi gỡ lỗi mà không biên dịch lại và điều này bạn tránh khi sử dụng if(x) continue;.
Paprik

Nói đúng ra, .Wherechỉ được gọi một lần. Những gì được gọi trên mỗi lần lặp là các đại biểu lọc (và MoveNextCurrenttrên các điều tra viên, khi họ không được tối ưu hóa ra)
CodesInChaos

@CodesInChaos tôi phải mất một chút suy nghĩ để hiểu những gì bạn đang nói, nhưng tất nhiên, wh00ps, bạn nói đúng, nói đúng, .Wherechỉ được gọi một lần. Đã sửa nó.
Mike Nakis
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.