Foreach-loop với break / return so với while-loop với bất biến rõ ràng và điều kiện hậu


17

Đây là cách phổ biến nhất (dường như đối với tôi) để kiểm tra xem một giá trị có nằm trong một mảng không:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Tuy nhiên, trong một cuốn sách tôi đã đọc cách đây nhiều năm, có lẽ, Wirth hoặc Dijkstra, người ta nói rằng phong cách này tốt hơn (khi so sánh với một vòng lặp trong khi có lối ra bên trong):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

Bằng cách này, điều kiện thoát bổ sung trở thành một phần rõ ràng của bất biến vòng lặp, không có điều kiện ẩn và thoát bên trong vòng lặp, mọi thứ rõ ràng hơn và nhiều hơn theo cách lập trình có cấu trúc. Tôi thường thích mẫu sau này bất cứ khi nào có thể và sử dụng for-loop để chỉ lặp từ ađến b.

Nhưng tôi không thể nói rằng phiên bản đầu tiên ít rõ ràng hơn. Có lẽ nó thậm chí còn rõ ràng và dễ hiểu hơn, ít nhất là cho những người mới bắt đầu. Vì vậy, tôi vẫn đang tự hỏi mình câu hỏi cái nào tốt hơn?

Có lẽ ai đó có thể đưa ra một lý do tốt để ủng hộ một trong những phương pháp?

Cập nhật: Đây không phải là câu hỏi về nhiều điểm trả về hàm, lambdas hoặc tìm một phần tử trong một mảng mỗi se. Đó là về cách viết các vòng lặp với các bất biến phức tạp hơn bất đẳng thức đơn lẻ.

Cập nhật: OK, tôi thấy quan điểm của những người trả lời và nhận xét: Tôi đã trộn lẫn trong vòng lặp foreach ở đây, bản thân nó đã rõ ràng và dễ đọc hơn nhiều so với vòng lặp while. Tôi không nên làm điều đó. Nhưng đây cũng là một câu hỏi thú vị, vì vậy hãy để nó như vậy: vòng lặp foreach và một điều kiện bổ sung bên trong, hoặc vòng lặp while với bất biến vòng lặp rõ ràng và điều kiện hậu sau. Có vẻ như vòng lặp foreach với một điều kiện và thoát / ngắt là chiến thắng. Tôi sẽ tạo một câu hỏi bổ sung mà không có vòng lặp foreach (cho danh sách được liên kết).


2
Các ví dụ mã được trích dẫn ở đây trộn lẫn một số vấn đề khác nhau. Trả về sớm & nhiều (đối với tôi đi theo kích thước của phương thức (không hiển thị)), tìm kiếm mảng (yêu cầu một cuộc thảo luận liên quan đến lambdas), foreach so với lập chỉ mục trực tiếp ... Câu hỏi này sẽ rõ ràng và dễ dàng hơn trả lời nếu nó chỉ tập trung vào một trong những vấn đề này tại một thời điểm.
Erik Eidt


1
Tôi biết đó là những ví dụ, nhưng có những ngôn ngữ có API để xử lý chính xác trường hợp sử dụng đó. Tức làcollection.contains(foo)
Berin Loritsch

2
Bạn có thể muốn tìm cuốn sách và đọc lại ngay bây giờ để xem những gì nó thực sự nói.
Thorbjørn Ravn Andersen

1
"Tốt hơn" là một từ rất chủ quan. Điều đó nói rằng, người ta có thể nói trong nháy mắt những gì phiên bản đầu tiên đang làm. Phiên bản thứ hai thực hiện chính xác điều tương tự cần có sự xem xét kỹ lưỡng.
David Hammen

Câu trả lời:


19

Tôi nghĩ đối với các vòng lặp đơn giản, chẳng hạn như các vòng lặp, cú pháp tiêu chuẩn đầu tiên rõ ràng hơn nhiều. Một số người coi nhiều lợi nhuận gây nhầm lẫn hoặc mùi mã, nhưng đối với một đoạn mã nhỏ này, tôi không tin đây là vấn đề thực sự.

Nó gây tranh cãi hơn một chút cho các vòng lặp phức tạp hơn. Nếu nội dung của vòng lặp không thể vừa với màn hình của bạn và có một số kết quả trả về trong vòng lặp, có một lập luận được đưa ra là nhiều điểm thoát có thể làm cho mã khó bảo trì hơn. Ví dụ: nếu bạn phải đảm bảo một số phương thức bảo trì trạng thái đã chạy trước khi thoát khỏi chức năng, sẽ rất dễ bỏ lỡ việc thêm nó vào một trong các câu lệnh trả về và bạn sẽ gây ra lỗi. Nếu tất cả các điều kiện kết thúc có thể được kiểm tra trong một vòng lặp while, bạn chỉ có một điểm thoát và có thể thêm mã này sau nó.

Điều đó nói rằng, với các vòng lặp đặc biệt là tốt để thử và đưa càng nhiều logic càng tốt vào các phương thức riêng biệt. Điều này tránh được rất nhiều trường hợp phương pháp thứ hai sẽ có lợi thế. Các vòng lặp tinh gọn với logic tách biệt rõ ràng sẽ quan trọng hơn những kiểu bạn sử dụng. Ngoài ra, nếu hầu hết cơ sở mã của ứng dụng của bạn đang sử dụng một kiểu, bạn nên giữ nguyên kiểu đó.


56

Điều này thật dễ dàng.

Hầu như không có gì quan trọng hơn sự rõ ràng cho người đọc. Các biến thể đầu tiên tôi tìm thấy vô cùng đơn giản và rõ ràng.

Phiên bản 'cải tiến' thứ hai, tôi đã phải đọc nhiều lần và đảm bảo tất cả các điều kiện cạnh đều đúng.

Có ZERO DOUBT là phong cách mã hóa tốt hơn (lần đầu tiên là tốt hơn nhiều).

Bây giờ - những gì rõ ràng với mọi người có thể khác nhau từ người này sang người khác. Tôi không chắc chắn có bất kỳ tiêu chuẩn khách quan nào cho điều đó (mặc dù việc đăng lên một diễn đàn như thế này và nhận được nhiều đầu vào của mọi người có thể giúp ích).

Tuy nhiên, trong trường hợp cụ thể này, tôi có thể cho bạn biết lý do tại sao thuật toán đầu tiên rõ ràng hơn: Tôi biết C ++ lặp qua một cú pháp container trông như thế nào và làm gì. Tôi đã nội hóa nó. Ai đó UNFAMILIAR (cú pháp mới của nó) với cú pháp đó có thể thích biến thể thứ hai.

Nhưng một khi bạn biết và hiểu cú pháp mới đó, đó là một khái niệm cơ bản bạn chỉ có thể sử dụng. Với cách tiếp cận vòng lặp (giây), bạn phải kiểm tra cẩn thận rằng người dùng có kiểm tra ĐÚNG cho tất cả các điều kiện cạnh để lặp trên toàn bộ mảng (ví dụ: ít hơn thay cho chỉ số ít hoặc bằng, được sử dụng để kiểm tra và để lập chỉ mục vv).


4
Mới là tương đối, vì nó đã có trong tiêu chuẩn năm 2011. Ngoài ra, bản demo thứ hai rõ ràng không phải là C ++.
Ded repeatator

Một giải pháp thay thế nếu bạn muốn sử dụng một điểm thoát duy nhất sẽ được để thiết lập một lá cờ longerLength = true, sau đó return longerLength.
Cullub

@Ded repeatator Tại sao không phải là bản demo thứ hai C ++? Tôi không thấy lý do tại sao không hoặc tôi đang thiếu một cái gì đó rõ ràng?
Rakete1111

2
@ Rakete1111 Mảng thô không có bất kỳ thuộc tính nào như length. Nếu nó thực sự được khai báo là một mảng và không phải là một con trỏ, chúng có thể sử dụng sizeofhoặc nếu đó là một std::arrayhàm thành viên chính xác size()thì không có thuộc lengthtính.
IllusiveBrian

@IllusiveBrian: sizeofsẽ được tính bằng byte ... Chung chung nhất kể từ C ++ 17 là std::size().
Ded repeatator

9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[V]] mọi thứ rõ ràng hơnnhiều hơn theo cách lập trình có cấu trúc.

Không hẳn. Biến itồn tại bên ngoài vòng lặp while ở đây và do đó là một phần của phạm vi bên ngoài, trong khi (ý định chơi chữ) xcủa for-loop chỉ tồn tại trong phạm vi của vòng lặp. Phạm vi là một cách rất quan trọng để giới thiệu cấu trúc để lập trình.



1
@ruakh Tôi không chắc lấy gì từ nhận xét của bạn. Nó xuất hiện dưới dạng hơi thụ động, như thể câu trả lời của tôi phản đối những gì được viết trên trang wiki. Xin hãy giải thích.
null

"Lập trình có cấu trúc" là một thuật ngữ nghệ thuật có ý nghĩa cụ thể và OP chính xác một cách khách quan rằng phiên bản # 2 tuân thủ các quy tắc của lập trình có cấu trúc trong khi phiên bản # 1 thì không. Từ câu trả lời của bạn, có vẻ như bạn không quen thuộc với thuật ngữ nghệ thuật và đã diễn giải thuật ngữ này theo nghĩa đen. Tôi không chắc tại sao bình luận của tôi lại xuất hiện dưới dạng thụ động; Tôi có nghĩa là nó chỉ đơn giản là thông tin.
ruakh

@ruakh Tôi không đồng ý rằng phiên bản # 2 phù hợp với các quy tắc hơn ở mọi khía cạnh và giải thích điều đó trong câu trả lời của tôi.
null

Bạn nói "Tôi không đồng ý" như thể đó là một điều chủ quan, nhưng thực tế không phải vậy. Trở về từ bên trong một vòng lặp là một sự vi phạm phân loại các quy tắc của lập trình có cấu trúc. Tôi chắc chắn rằng nhiều người đam mê lập trình có cấu trúc là người hâm mộ của các biến có phạm vi tối thiểu, nhưng nếu bạn giảm phạm vi của biến bằng cách rời khỏi lập trình có cấu trúc, thì bạn đã rời khỏi lập trình có cấu trúc, thời gian và giảm phạm vi của biến không hoàn tác cái đó.
ruakh

2

Hai vòng có ngữ nghĩa khác nhau:

  • Vòng lặp đầu tiên chỉ trả lời một câu hỏi có / không đơn giản: "Mảng có chứa đối tượng tôi đang tìm không?" Nó làm như vậy một cách ngắn gọn nhất có thể.

  • Vòng lặp thứ hai trả lời câu hỏi: "Nếu mảng chứa đối tượng tôi đang tìm kiếm, chỉ số của trận đấu đầu tiên là gì?" Một lần nữa, nó làm như vậy một cách ngắn gọn nhất có thể.

Vì câu trả lời cho câu hỏi thứ hai không cung cấp nhiều thông tin hơn câu trả lời cho câu hỏi thứ nhất, bạn có thể chọn trả lời câu hỏi thứ hai và sau đó rút ra câu trả lời của câu hỏi đầu tiên. Đó là những gì dòng return i < array.length;làm, dù sao.

Tôi tin rằng tốt nhất là chỉ sử dụng công cụ phù hợp với mục đích trừ khi bạn có thể sử dụng lại một công cụ linh hoạt hơn đã có sẵn. I E:

  • Sử dụng biến thể đầu tiên của vòng lặp là tốt.
  • Thay đổi biến thể đầu tiên để chỉ đặt một boolbiến và phá vỡ cũng tốt. (Tránh returncâu lệnh thứ hai , câu trả lời có sẵn trong một biến thay vì trả về hàm.)
  • Sử dụng std::findlà tốt (sử dụng lại mã!).
  • Tuy nhiên, mã hóa rõ ràng một tìm thấy và sau đó giảm câu trả lời thành boolkhông.

Sẽ rất tuyệt nếu những người downvoters để lại bình luận ...
cmaster - phục hồi monica vào

2

Tôi sẽ đề xuất một lựa chọn thứ ba hoàn toàn:

return array.find(value);

Có nhiều lý do khác nhau để lặp lại trên một mảng: Kiểm tra xem có tồn tại một giá trị cụ thể không, biến đổi mảng thành một mảng khác, tính toán một giá trị tổng hợp, lọc một số giá trị ra khỏi mảng ... Nếu bạn sử dụng một vòng lặp đơn giản cho vòng lặp, thì không rõ ràng trong nháy mắt cụ thể làm thế nào vòng lặp for đang được sử dụng. Tuy nhiên, hầu hết các ngôn ngữ hiện đại đều có API phong phú trên cấu trúc dữ liệu mảng của chúng làm cho những ý định khác nhau này rất rõ ràng.

So sánh chuyển đổi một mảng thành một mảng khác với một vòng lặp for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

và sử dụng maphàm kiểu JavaScript :

array.map((value) => value * 2);

Hoặc tóm tắt một mảng:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

đấu với:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Mất bao lâu để bạn hiểu điều này làm gì?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

đấu với

array.filter((value) => (value >= 0));

Trong cả ba trường hợp, trong khi vòng lặp for chắc chắn có thể đọc được, bạn phải dành một vài phút để tìm hiểu cách vòng lặp for đang được sử dụng và kiểm tra xem tất cả các bộ đếm và điều kiện thoát là chính xác. Các hàm kiểu lambda hiện đại làm cho mục đích của các vòng lặp cực kỳ rõ ràng và bạn biết chắc chắn rằng các hàm API được gọi được triển khai chính xác.

Hầu hết các ngôn ngữ hiện đại, bao gồm JavaScript , Ruby , C #Java , sử dụng kiểu tương tác chức năng này với các mảng và các bộ sưu tập tương tự.

Nói chung, trong khi tôi không nghĩ việc sử dụng các vòng lặp là nhất thiết sai và đó là vấn đề sở thích cá nhân, tôi đã thấy mình rất thích sử dụng phong cách làm việc với mảng này. Điều này đặc biệt vì sự rõ ràng tăng lên trong việc xác định những gì mỗi vòng lặp đang làm. Nếu ngôn ngữ của bạn có các tính năng hoặc công cụ tương tự trong các thư viện tiêu chuẩn, tôi khuyên bạn cũng nên xem xét áp dụng phong cách này!


2
Khuyến nghị array.findđặt ra câu hỏi, khi đó chúng ta phải thảo luận về cách tốt nhất để thực hiện array.find. Trừ khi bạn đang sử dụng phần cứng với một findhoạt động tích hợp, chúng ta phải viết một vòng lặp ở đó.
Barmar

2
@Barmar tôi không đồng ý. Như tôi đã chỉ ra trong câu trả lời của mình, rất nhiều ngôn ngữ được sử dụng nhiều cung cấp các chức năng như findtrong các thư viện tiêu chuẩn của họ. Không còn nghi ngờ gì nữa, các thư viện này triển khai findvà họ hàng của nó sử dụng cho các vòng lặp, nhưng đó là chức năng tốt: nó trừu tượng hóa các chi tiết kỹ thuật khỏi người tiêu dùng của chức năng, cho phép lập trình viên không cần phải suy nghĩ về các chi tiết đó. Vì vậy, mặc dù findcó khả năng được triển khai với một vòng lặp for, nó vẫn giúp làm cho mã dễ đọc hơn và vì nó thường xuyên trong thư viện tiêu chuẩn, sử dụng nó không có thêm chi phí hay rủi ro có ý nghĩa.
Kevin - Hồi phục lại

4
Nhưng một kỹ sư phần mềm phải thực hiện các thư viện này. Không phải các nguyên tắc kỹ thuật phần mềm tương tự áp dụng cho các tác giả thư viện là lập trình viên ứng dụng? Câu hỏi là về cách viết các vòng lặp nói chung, không phải là cách tốt nhất để tìm kiếm một phần tử mảng trong một ngôn ngữ cụ thể
Barmar

4
Nói cách khác, tìm kiếm một phần tử mảng chỉ là một ví dụ đơn giản mà anh ta đã sử dụng để thể hiện các kỹ thuật lặp khác nhau.
Barmar

-2

Tất cả tập trung vào chính xác những gì có nghĩa là 'tốt hơn'. Đối với các lập trình viên thực tế, nó thường có nghĩa là hiệu quả - tức là trong trường hợp này, thoát trực tiếp khỏi vòng lặp sẽ tránh một so sánh thêm và trả về hằng số Boolean tránh so sánh trùng lặp; Điều này tiết kiệm chu kỳ. Dijkstra quan tâm nhiều hơn đến việc tạo mã dễ chứng minh chính xác hơn . [có vẻ như đối với tôi rằng giáo dục CS ở Châu Âu coi "việc chứng minh tính đúng đắn" nghiêm trọng hơn nhiều so với giáo dục CS ở Mỹ, nơi các lực lượng kinh tế có xu hướng chi phối thực hành mã hóa]


3
PMar, hiệu suất-khôn ngoan cả hai vòng tương đương nhau khá nhiều - cả hai đều có hai so sánh.
Danila Piatov

1
Nếu một người thực sự quan tâm đến hiệu suất, người ta sẽ sử dụng thuật toán nhanh hơn. ví dụ: sắp xếp mảng và thực hiện tìm kiếm nhị phân hoặc sử dụng Hashtable.
user949300

Danila - bạn không biết cấu trúc dữ liệu nào đằng sau điều này. Một iterator luôn luôn nhanh. Truy cập được lập chỉ mục có thể là thời gian tuyến tính và độ dài thậm chí không cần tồn tại.
gnasher729
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.