while (true) và loop-break - chống mẫu?


32

Hãy xem xét các mã sau đây:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Giả sử rằng quá trình này bao gồm số bước hữu hạn nhưng phụ thuộc đầu vào; vòng lặp được thiết kế để tự chấm dứt do thuật toán và không được thiết kế để chạy vô thời hạn (cho đến khi bị hủy bởi một sự kiện bên ngoài). Bởi vì thử nghiệm để xem vòng lặp có nên kết thúc ở giữa một tập hợp các bước hợp lý hay không, bản thân vòng lặp while hiện không kiểm tra bất cứ điều gì có ý nghĩa; thay vào đó, kiểm tra được thực hiện tại vị trí "thích hợp" trong thuật toán khái niệm.

Tôi đã nói rằng đây là mã xấu, bởi vì nó dễ bị lỗi hơn do điều kiện kết thúc không được kiểm tra bởi cấu trúc vòng lặp. Khó khăn hơn để tìm ra cách bạn thoát khỏi vòng lặp và có thể mời các lỗi vì điều kiện phá vỡ có thể được bỏ qua hoặc bỏ qua vô tình đưa ra các thay đổi trong tương lai.

Bây giờ, mã có thể được cấu trúc như sau:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

Tuy nhiên, điều này sao chép một cuộc gọi đến một phương thức trong mã, vi phạm DRY; nếu TransformInSomeWaysau đó được thay thế bằng một số phương thức khác, cả hai cuộc gọi sẽ phải được tìm thấy và thay đổi (và thực tế là có hai cuộc gọi có thể ít rõ ràng hơn trong một đoạn mã phức tạp hơn).

Bạn cũng có thể viết nó như sau:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... nhưng bây giờ bạn có một biến có mục đích duy nhất là chuyển dịch kiểm tra điều kiện sang cấu trúc vòng lặp và cũng phải được kiểm tra nhiều lần để cung cấp hành vi giống như logic ban đầu.

Về phần tôi, tôi nói rằng với thuật toán mà mã này thực hiện trong thế giới thực, mã gốc là thứ dễ đọc nhất. Nếu bạn đã tự mình trải qua nó, đây là cách bạn nghĩ về nó, và vì vậy nó sẽ trực quan với những người quen thuộc với thuật toán.

Vậy, cái nào "tốt hơn"? tốt hơn là giao trách nhiệm kiểm tra điều kiện cho vòng lặp while bằng cách cấu trúc logic xung quanh vòng lặp? Hoặc tốt hơn là cấu trúc logic theo cách "tự nhiên" như được chỉ ra bởi các yêu cầu hoặc mô tả khái niệm về thuật toán, mặc dù điều đó có thể có nghĩa là bỏ qua các khả năng tích hợp của vòng lặp?


12
Có thể, nhưng mặc dù các mẫu mã, câu hỏi đang được hỏi là IMO mang tính khái niệm hơn, phù hợp hơn với lĩnh vực SE này. Mô hình hoặc mô hình chống là gì thường được thảo luận ở đây và đó là câu hỏi thực sự; cấu trúc một vòng lặp để chạy vô tận và sau đó thoát ra khỏi nó là một điều xấu?
KeithS

3
Cấu do ... untiltrúc này cũng hữu ích cho các loại vòng lặp này vì chúng không phải được "mồi".
Blrfl

2
Trường hợp vòng lặp rưỡi vẫn không có câu trả lời thực sự tốt.
Loren Pechtel

1
"Tuy nhiên, điều này sao chép một cuộc gọi đến một phương thức trong mã, vi phạm DRY" Tôi không đồng ý với khẳng định đó và nó có vẻ như là một sự hiểu lầm về nguyên tắc.
Andy

1
Ngoài tôi thích phong cách C cho (;;) có nghĩa rõ ràng là "Tôi có một vòng lặp và tôi không kiểm tra trong câu lệnh vòng lặp", cách tiếp cận đầu tiên của bạn là hoàn toàn đúng. Bạn có một vòng lặp. Có một số việc phải làm trước khi bạn có thể quyết định có thoát hay không. Sau đó, bạn thoát nếu một số điều kiện được đáp ứng. Sau đó, bạn làm công việc thực tế, và bắt đầu lại tất cả. Điều đó hoàn toàn tốt, dễ hiểu, hợp lý, không dư thừa và dễ dàng để làm đúng. Biến thể thứ hai chỉ là khủng khiếp, trong khi biến thể thứ ba chỉ là vô nghĩa; chuyển sang khủng khiếp nếu phần còn lại của vòng lặp đi vào nếu rất dài.
gnasher729

Câu trả lời:


14

Gắn bó với giải pháp đầu tiên của bạn. Các vòng lặp có thể trở nên rất liên quan và một hoặc nhiều lần phá sạch có thể dễ dàng hơn đối với bạn, bất kỳ ai khác nhìn vào mã của bạn, trình tối ưu hóa và hiệu suất của chương trình so với các câu lệnh if và các biến được thêm vào. Cách tiếp cận thông thường của tôi là thiết lập một vòng lặp với thứ gì đó như "while (true)" và lấy mã hóa tốt nhất có thể. Thường thì tôi có thể và làm sau đó thay thế (các) ngắt bằng điều khiển vòng lặp tiêu chuẩn; Rốt cuộc, hầu hết các vòng đều khá đơn giản. Điều kiện kết thúc phải được kiểm tra bởi cấu trúc vòng lặp vì những lý do bạn đề cập, nhưng không phải trả thêm các biến mới, thêm câu lệnh if và mã lặp lại, tất cả đều dễ bị lỗi hơn .


"if-statement và lặp lại mã, tất cả chúng đều dễ bị lỗi hơn." - vâng khi thử những người tôi gọi nếu là "lỗi tương lai"
Pat

13

Đó chỉ là một vấn đề có ý nghĩa nếu thân vòng lặp không tầm thường. Nếu cơ thể quá nhỏ, thì việc phân tích nó như thế này là chuyện nhỏ, và không có vấn đề gì cả.


7

Tôi gặp khó khăn khi tìm giải pháp khác tốt hơn giải pháp bị phá vỡ. Chà, tôi thích cách Ada cho phép làm

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

nhưng giải pháp phá vỡ là kết xuất sạch nhất.

POV của tôi là khi thiếu cấu trúc điều khiển, giải pháp sạch nhất là mô phỏng nó với các cấu trúc điều khiển khác, không sử dụng luồng dữ liệu. (Và tương tự, khi tôi gặp vấn đề về luồng dữ liệu để thích các giải pháp luồng dữ liệu thuần túy hơn là giải pháp liên quan đến cấu trúc điều khiển *).

Khó khăn hơn để tìm ra cách bạn thoát khỏi vòng lặp,

Đừng nhầm lẫn, cuộc thảo luận sẽ không diễn ra nếu khó khăn không giống nhau: điều kiện là như nhau và có mặt ở cùng một nơi.

Mà trong số

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

làm cho cấu trúc dự định tốt hơn? Tôi đang bỏ phiếu cho người đầu tiên, bạn không phải kiểm tra xem các điều kiện có phù hợp không. (Nhưng xem vết lõm của tôi).


1
Ồ, cái nhìn, một ngôn ngữ mà không hỗ trợ trực tiếp vòng giữa lối ra! :)
mjfgates

Tôi thích quan điểm của bạn về việc mô phỏng các cấu trúc điều khiển bị thiếu với các cấu trúc điều khiển. Đó có lẽ là một câu trả lời tốt cho các câu hỏi liên quan đến GOTO. Người ta nên sử dụng các cấu trúc điều khiển của ngôn ngữ bất cứ khi nào chúng phù hợp với các yêu cầu ngữ nghĩa, nhưng nếu thuật toán yêu cầu thứ khác, người ta nên sử dụng cấu trúc điều khiển theo yêu cầu của thuật toán.
supercat

3

while (true) và break là một thành ngữ phổ biến. Đó là cách chuẩn để thực hiện các vòng lặp giữa lối ra trong các ngôn ngữ giống như C. Thêm một biến để lưu điều kiện thoát và một if () xung quanh nửa sau của vòng lặp là một lựa chọn hợp lý. Một số cửa hàng có thể Tiêu chuẩn hóa chính thức cái này hay cái kia, nhưng không cái nào tốt hơn; chúng là tương đương

Một lập trình viên giỏi làm việc trong các ngôn ngữ này sẽ có thể biết những gì đang diễn ra trong nháy mắt nếu anh ta hoặc cô ta nhìn thấy một trong hai ngôn ngữ đó. Nó có thể làm cho một câu hỏi phỏng vấn nhỏ tốt; đưa cho ai đó hai vòng lặp, một vòng sử dụng mỗi thành ngữ và hỏi họ xem sự khác biệt là gì (câu trả lời đúng: "không có gì", có thể là một sự bổ sung "nhưng tôi thường sử dụng RATNG".


2

Lựa chọn thay thế của bạn không tệ như bạn nghĩ. Mã tốt nên:

  1. Rõ ràng chỉ ra với tên / bình luận biến tốt trong những điều kiện vòng lặp nên được chấm dứt. (Không nên ẩn điều kiện phá vỡ)

  2. Rõ ràng cho biết thứ tự thực hiện các hoạt động là gì. Đây là nơi đặt thao tác thứ 3 tùy chọn ( DoSomethingElseTo(input)) bên trong if là một ý tưởng tốt.

Điều đó nói rằng, thật dễ dàng để cho sự hoàn hảo, bản năng đánh bóng đồng thau tiêu tốn rất nhiều thời gian. Tôi sẽ chỉ điều chỉnh nếu như đã đề cập ở trên; nhận xét mã và di chuyển trên.


Tôi nghĩ rằng sự hoàn hảo, bản năng đánh bóng đồng thau tiết kiệm thời gian trong thời gian dài. Hy vọng rằng, với thực tiễn, bạn phát triển các thói quen như vậy để mã của bạn hoàn hảo và đồng thau của nó được đánh bóng mà không tốn nhiều thời gian. Nhưng không giống như xây dựng vật lý, phần mềm có thể tồn tại mãi mãi và được sử dụng ở vô số nơi. Khoản tiết kiệm từ mã thậm chí nhanh hơn 0,00000101%, đáng tin cậy hơn, có thể sử dụng và có thể bảo trì có thể lớn. (Tất nhiên, hầu hết các mã nhiều-đánh bóng của tôi là trong ngôn ngữ dài lỗi thời hoặc kiếm tiền cho người khác những ngày này, nhưng nó đã giúp tôi phát triển những thói quen tốt.)
RalphChapin

Vâng, tôi không thể gọi cho bạn sai :-) Đây một câu hỏi về ý kiến ​​và nếu các kỹ năng tốt xuất phát từ đồng thau: tuyệt vời.
Pat

Tôi nghĩ rằng đó là một khả năng để xem xét, dù sao. Tôi chắc chắn sẽ không muốn cung cấp bất kỳ đảm bảo. Nó làm cho tôi cảm thấy tốt hơn khi nghĩ rằng đánh bóng đồng của tôi đã cải thiện các kỹ năng của tôi, vì vậy tôi có thể bị định kiến ​​về điểm này.
RalphChapin

2

Mọi người nói rằng đó là thực tế xấu để sử dụng while (true), và rất nhiều thời gian họ đúng. Nếu whilecâu lệnh của bạn chứa nhiều mã phức tạp và không rõ khi nào bạn thoát ra khỏi if, thì bạn sẽ gặp khó khăn trong việc duy trì mã và một lỗi đơn giản có thể gây ra một vòng lặp vô hạn.

Trong ví dụ của bạn, bạn đúng khi sử dụng while(true)vì mã của bạn rõ ràng và dễ hiểu. Không có điểm nào tạo ra mã không hiệu quả và khó đọc hơn, đơn giản là vì một số người cho rằng nó luôn luôn sử dụng không tốt while(true).

Tôi đã phải đối mặt với một vấn đề tương tự:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

Sử dụng do ... while(true)để tránh isset($array][$key]bị gọi một cách không cần thiết hai lần, mã nó ngắn hơn, sạch hơn và dễ đọc hơn. Hoàn toàn không có rủi ro về lỗi vòng lặp vô hạn được giới thiệu else break;ở cuối.

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Thật tốt khi làm theo các thực hành tốt và tránh các thực hành xấu nhưng luôn có ngoại lệ và điều quan trọng là phải giữ một tâm trí cởi mở và nhận ra những ngoại lệ đó.


0

Nếu một luồng điều khiển cụ thể được thể hiện một cách tự nhiên như một câu lệnh ngắt, thì về cơ bản không bao giờ là lựa chọn tốt hơn để sử dụng các cơ chế điều khiển luồng khác để mô phỏng câu lệnh ngắt.

(lưu ý rằng điều này bao gồm hủy bỏ một phần vòng lặp và trượt thân vòng lặp xuống để vòng lặp bắt đầu trùng với thử nghiệm có điều kiện)

Nếu bạn có thể sắp xếp lại vòng lặp của mình để luồng điều khiển được thể hiện một cách tự nhiên bằng cách kiểm tra có điều kiện ở đầu vòng lặp, thật tuyệt. Nó thậm chí có thể đáng để dành thời gian suy nghĩ về việc liệu điều đó là có thể. Nhưng không, đừng cố lắp một cái chốt vuông vào một cái lỗ tròn.


0

Bạn cũng có thể đi với điều này:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Hoặc ít gây nhầm lẫn:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}

1
Mặc dù một số người thích nó, tôi khẳng định rằng điều kiện vòng lặp thường được dành riêng cho các bài kiểm tra có điều kiện, thay vì các câu lệnh làm công cụ.

5
Biểu hiện dấu phẩy trong điều kiện vòng lặp ... Thật khủng khiếp. Bạn quá thông minh. Và nó chỉ hoạt động trong trường hợp tầm thường này trong đó TransformInSomeWay có thể được biểu thị dưới dạng một biểu thức.
gnasher729

0

Khi sự phức tạp của logic trở nên khó sử dụng thì việc sử dụng một vòng lặp không giới hạn với các ngắt vòng giữa để điều khiển luồng thực sự có ý nghĩa nhất. Tôi có một dự án với một số mã trông như sau. (Lưu ý ngoại lệ ở cuối vòng lặp.)

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Để thực hiện logic này mà không phá vỡ vòng lặp không giới hạn, tôi sẽ kết thúc bằng một cái gì đó như sau:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Vì vậy, ngoại lệ bây giờ được ném trong một phương thức trợ giúp. Ngoài ra nó có khá nhiều if ... elselồng. Tôi không hài lòng với phương pháp này. Vì vậy, tôi nghĩ rằng mô hình đầu tiên là tốt nhất.


Thật vậy, tôi hỏi câu hỏi này trên SO và bị bắn rơi trong biển lửa ... stackoverflow.com/questions/47253486/...
intrepidis
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.