Cách cấu trúc một vòng lặp lặp lại cho đến khi thành công và xử lý các thất bại


8

Tôi là một lập trình viên tự học. Tôi bắt đầu lập trình khoảng 1,5 năm trước. Bây giờ tôi đã bắt đầu có các lớp lập trình ở trường. Chúng tôi đã có các lớp lập trình trong 1/2 năm và sẽ có thêm 1/2 ngay bây giờ.

Trong các lớp học, chúng tôi đang học lập trình trong C ++ (đây là ngôn ngữ mà tôi đã biết sử dụng khá tốt trước khi chúng tôi bắt đầu).

Tôi không gặp khó khăn gì trong lớp học này nhưng có một vấn đề tái diễn mà tôi chưa thể tìm ra giải pháp rõ ràng.

Vấn đề là như thế này (trong Pseudocode):

 do something
 if something failed:
     handle the error
     try something (line 1) again
 else:
     we are done!

Đây là một ví dụ trong C ++

Mã nhắc người dùng nhập một số và làm như vậy cho đến khi đầu vào hợp lệ. Nó sử dụng cin.fail()để kiểm tra nếu đầu vào không hợp lệ. Khi cin.fail()truetôi phải gọi cin.clear()cin.ignore()để có thể tiếp tục nhận được ý kiến từ các dòng suối.

Tôi biết rằng mã này không kiểm tra EOF. Các chương trình chúng tôi đã viết không mong muốn làm điều đó.

Đây là cách tôi viết mã trong một trong những bài tập của mình ở trường:

for (;;) {
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        continue;
    }
    break;
}

Giáo viên của tôi nói rằng tôi không nên sử dụng breakcontinuenhư thế này. Ông đề nghị rằng tôi nên sử dụng một vòng lặp thường xuyên whilehoặc do ... whilethay thế.

Dường như với tôi rằng sử dụng breakcontinuelà cách đơn giản nhất để thể hiện loại vòng lặp này. Tôi thực sự đã nghĩ về nó trong một thời gian dài nhưng không thực sự đưa ra một giải pháp rõ ràng hơn.

Tôi nghĩ rằng anh ấy muốn tôi làm một cái gì đó như thế này:

do {
    cout << ": ";
    cin >> input;
    bool fail = cin.fail();
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (fail);

Đối với tôi phiên bản này có vẻ phức tạp hơn rất nhiều kể từ bây giờ chúng tôi cũng có biến được gọi failđể theo dõi và kiểm tra lỗi đầu vào được thực hiện hai lần thay vì chỉ một lần.

Tôi cũng hình dung rằng tôi có thể viết mã như thế này (lạm dụng đánh giá ngắn mạch):

do {
    cout << ": ";
    cin >> input;
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (cin.fail() && (cin.clear(), cin.ignore(512, '\n', true);

Phiên bản này hoạt động chính xác như những người khác. Nó không sử dụng breakhoặc continuecin.fail()thử nghiệm chỉ được thực hiện một lần. Tuy nhiên, dường như tôi không đúng khi lạm dụng "quy tắc đánh giá ngắn mạch" như thế này. Tôi cũng không nghĩ giáo viên của mình sẽ thích nó.

Vấn đề này không chỉ áp dụng cho cin.fail()việc kiểm tra. Tôi đã sử dụng breakcontinuethích điều này cho nhiều trường hợp khác liên quan đến việc lặp lại một bộ mã cho đến khi một điều kiện được đáp ứng trong đó điều gì đó cũng phải được thực hiện nếu điều kiện không được đáp ứng (như gọi cin.clear()cin.ignore(...)từ cin.fail()ví dụ).

Tôi đã tiếp tục sử dụng breakcontinuetrong suốt khóa học và bây giờ giáo viên của tôi đã ngừng phàn nàn về nó.

Ý kiến ​​của bạn về điều này là gì?

Bạn có nghĩ rằng giáo viên của tôi là đúng?

Bạn có biết một cách tốt hơn để đại diện cho loại vấn đề này?


6
Bạn không lặp đi lặp lại mãi mãi, vậy tại sao bạn lại sử dụng thứ gì đó ám chỉ rằng bạn đang lặp đi lặp lại mãi mãi? Một vòng lặp mãi mãi không phải luôn luôn là một lựa chọn tồi (mặc dù nó thường là vậy) trong trường hợp này rõ ràng đó là một lựa chọn tồi vì nó truyền đạt ý định sai của mã. Phương thức thứ hai gần với mục đích của mã hơn, vì vậy nó tốt hơn. Mã trong đoạn mã thứ hai có thể được làm cho gọn gàng hơn và dễ đọc hơn với một số sắp xếp lại tối thiểu.
Dunk

3
@Dunk Điều đó có vẻ hơi giáo điều. Thật sự rất hiếm khi cần phải lặp đi lặp lại mãi mãi, ngoại trừ ở cấp cao nhất của các chương trình tương tác. Tôi thấy while (true)và ngay lập tức giả sử có một break, returnhoặc thrownơi nào đó trong đó. Giới thiệu một biến phụ cần được theo dõi chỉ để né tránh breakhoặc continuelà phản tác dụng. Tôi tranh luận vấn đề với mã ban đầu là vòng lặp không bao giờ hoàn thành việc lặp lại bình thường và bạn cần biết có một continuebên trong ifđể biết rằng breaksẽ không được thực hiện.
Doval

1
Có quá sớm để bạn học xử lý ngoại lệ?
Mawg nói rằng phục hồi Monica

2
@Dunk Bạn có thể biết ngay những gì đã được dự định , nhưng bạn sẽ phải tạm dừng và suy nghĩ kỹ hơn để kiểm tra xem nó có đúng không , điều này tệ hơn. dataIsValidlà có thể thay đổi, vì vậy để biết rằng vòng lặp kết thúc chính xác, tôi cần kiểm tra xem nó có được đặt khi dữ liệu hợp lệ không và cũng không được đặt ở bất kỳ điểm nào sau đó. Trong một vòng lặp của biểu mẫu do { ... if (!expr) { break; } ... } while (true);tôi biết rằng nếu exprđánh giá false, vòng lặp sẽ bị phá vỡ mà không cần phải nhìn vào phần còn lại của thân vòng lặp.
Doval

1
@Doval: Tôi cũng không biết bạn đọc mã như thế nào, nhưng tôi chắc chắn không đọc từng dòng khi tôi đang cố gắng tìm hiểu xem nó đang làm gì hoặc lỗi tôi đang tìm kiếm ở đâu. Nếu tôi thấy vòng lặp while, thì rõ ràng (từ ví dụ của tôi) rằng vòng lặp đang nhận dữ liệu hợp lệ. Tôi không quan tâm làm thế nào nó được thực hiện, tôi có thể bỏ qua mọi thứ khác trong vòng lặp trừ khi điều đó xảy ra vì lý do nào đó tôi nhận được dữ liệu không hợp lệ. Sau đó, tôi sẽ xem xét mã bên trong vòng lặp. Ý tưởng của bạn yêu cầu tôi xem xét tất cả các mã trong vòng lặp để tìm ra rằng nó không quan tâm đến tôi.
Dunk

Câu trả lời:


15

Tôi sẽ viết câu lệnh if hơi khác một chút, vì vậy nó được thực hiện khi đầu vào thành công.

for (;;) {
    cout << ": ";
    if (cin >> input)
        break;
    cin.clear();
    cin.ignore(512, '\n');
}

Nó cũng ngắn hơn.

Điều này gợi ý một cách ngắn hơn mà giáo viên của bạn có thể thích:

cout << ": ";
while (!(cin >> input)) {
    cin.clear();
    cin.ignore(512, '\n');
    cout << ": ";
}

Ý tưởng tốt đảo ngược các điều kiện như vậy để tôi không yêu cầu breakở cuối. Điều đó một mình làm cho mã rất nhiều dễ đọc hơn. Tôi cũng không biết điều đó !(cin >> input)là hợp lệ. Cảm ơn vì đã cho tôi biết!
wefwefa3

8
Vấn đề với giải pháp thứ hai là nó sao chép dòng cout << ": ";, do đó vi phạm nguyên tắc DRY. Đối với mã trong thế giới thực, điều này sẽ không xảy ra, vì nó dễ bị lỗi (khi bạn phải thay đổi phần đó sau, bạn sẽ phải đảm bảo rằng bạn không quên thay đổi hai dòng theo cách tương tự bây giờ). Neverthelss tôi đã cho bạn +1 cho phiên bản đầu tiên của bạn, đây là ví dụ về cách sử dụng breakđúng cách.
Doc Brown

2
Cảm giác này giống như bạn nhặt lông, xem xét dòng trùng lặp rõ ràng là một chút UI nhàm chán. Bạn có thể loại bỏ cout << ": "hoàn toàn đầu tiên và không có gì sẽ phá vỡ.
jdevlin

2
@codingthewheel: bạn đã đúng, ví dụ này quá tầm thường để chứng minh những rủi ro tiềm ẩn trong việc bỏ qua pricinple DRY. Nhưng hãy tưởng tượng một tình huống như vậy trong mã thế giới thực, nơi việc giới thiệu các lỗi vô tình có thể có hậu quả thực sự - sau đó bạn có thể nghĩ khác về nó. Đó là lý do tại sao sau ba thập kỷ lập trình, tôi có thói quen không bao giờ giải quyết vấn đề thoát vòng lặp như vậy bằng cách sao chép phần đầu tiên chỉ vì "một whilelệnh trông đẹp hơn".
Doc Brown

1
@DocBrown Sự trùng lặp của cout chủ yếu là một tạo tác của ví dụ. Trong thực tế, hai câu lệnh cout sẽ khác nhau vì câu lệnh thứ hai sẽ chứa thông báo lỗi. Ví dụ: "Vui lòng nhập <foo>:""Lỗi! Vui lòng nhập lại <foo>:" .
Sjoerd

15

Những gì bạn phải phấn đấu là tránh các vòng lặp thô .

Chuyển logic phức tạp thành một hàm trợ giúp và đột nhiên mọi thứ rõ ràng hơn nhiều:

bool getValidUserInput(string & input)
{
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        return false;
    }
    return true;
}

int main() {
    string input;
    while (!getValidUserInput(input)) { 
        // We wait for a valid user input...
    }
}

1
Đây là khá nhiều những gì tôi sẽ đề nghị. Tái cấu trúc nó để tách đầu vào từ quyết định lấy đầu vào.
Rob K

4
Tôi đồng ý rằng đây là một giải pháp tốt đẹp. Nó trở thành sự thay thế vượt trội khi getValidUserInputđược gọi nhiều lần và không chỉ một lần. Tôi cho bạn +1 vì điều đó nhưng tôi sẽ chấp nhận câu trả lời của Sjoerd vì tôi thấy rằng nó trả lời câu hỏi của tôi tốt hơn.
wefwefa3

@ user3787875: chính xác những gì tôi nghĩ khi tôi đọc hai câu trả lời - lựa chọn tốt.
Doc Brown

Đây là bước tiếp theo hợp lý sau khi viết lại trong câu trả lời của tôi.
Sjoerd

9

Nó không quá for(;;)tệ đến thế. Nó chỉ không rõ ràng như các mẫu như:

while (cin.fail()) {
    ...
}

Hoặc như Sjoerd đã đặt nó:

while (!(cin >> input)) {
    ...
}

Chúng ta hãy xem đối tượng chính của công cụ này là những lập trình viên đồng nghiệp của bạn, bao gồm cả phiên bản tương lai của chính bạn, người không còn nhớ tại sao bạn lại mắc kẹt vào cuối như thế, hoặc tiếp tục nghỉ giải lao. Mẫu này từ ví dụ của bạn:

for (;;) {
    ...
    if (blah) {
        continue;
    }
    break;
}

... đòi hỏi một vài giây suy nghĩ thêm để mô phỏng so với các mẫu khác. Đó không phải là một quy tắc khó và nhanh, nhưng việc nhảy câu lệnh break với tiếp tục cảm thấy lộn xộn, hoặc thông minh mà không hữu ích, và đặt câu lệnh break ở cuối vòng lặp, ngay cả khi nó hoạt động, là không bình thường. Thông thường cả hai breakcontinueđược sử dụng để sơ tán sớm vòng lặp hoặc lặp đi lặp lại của vòng lặp, vì vậy nhìn thấy một trong hai cuối cùng cảm thấy một chút kỳ lạ ngay cả khi nó không.

Khác hơn thế, thứ tốt!


Câu trả lời của bạn có vẻ tốt - nhưng chỉ trong một cái nhìn đầu tiên. Trong thực tế, nó che giấu vấn đề cố hữu mà chúng ta có thể thấy trong @Sjoerd trong s answer: using a khi lặp theo cách này có thể dẫn đến sao chép mã không cần thiết cho ví dụ đã cho. Và đó là một vấn đề mà IMHO ít nhất cũng quan trọng để giải quyết như khả năng đọc tổng thể của vòng lặp.
Doc Brown

Thư giãn đi, bác sĩ. OP đã yêu cầu một số lời khuyên / phản hồi cơ bản về cấu trúc vòng lặp của anh ấy, tôi đã cho anh ấy giải thích tiêu chuẩn. Bạn có thể tranh luận về các cách hiểu khác - cách tiếp cận tiêu chuẩn không phải lúc nào cũng là cách tiếp cận chặt chẽ nhất về mặt toán học - nhưng đặc biệt là khi người hướng dẫn của mình nghiêng về việc sử dụng while, tôi nghĩ rằng "chỉ trong một cái nhìn đầu tiên" là không chính đáng .
jdevlin

Đừng hiểu sai ý tôi, điều này không liên quan gì đến việc "nghiêm khắc về mặt toán học" - đây là tất cả về việc viết mã có thể tiến hóa - mã trong đó việc thêm các tính năng mới hoặc thay đổi các tính năng hiện có có nguy cơ giới thiệu các lỗi nhỏ nhất. Trong công việc của tôi, đó thực sự không phải là một vấn đề học thuật.
Doc Brown

Tôi muốn thêm rằng tôi đồng ý chủ yếu cho câu trả lời của bạn. Tôi chỉ muốn chỉ ra rằng sự đơn giản hóa của bạn ngay từ đầu có thể ẩn giấu một vấn đề thường xuyên. Khi tôi có cơ hội sử dụng "while" mà không có bất kỳ sự sao chép mã nào, tôi sẽ không ngần ngại sử dụng nó.
Doc Brown

1
Đồng ý và đôi khi một hoặc hai dòng không cần thiết trước vòng lặp là giá chúng ta phải trả cho whilecú pháp nạp trước khai báo đó . Tôi không muốn làm một cái gì đó trước vòng lặp cũng được thực hiện bên trong vòng lặp, sau đó một lần nữa, tôi cũng không thích một vòng lặp liên kết với nhau trong các nút thắt khi cố gắng cấu trúc lại tiền tố. Vì vậy, một chút của một hành động cân bằng một trong hai cách.
jdevlin

3

Người hướng dẫn logic của tôi ở trường luôn nói, và dồn nó vào não tôi, rằng chỉ nên có một lối vào và một lối ra cho các vòng lặp. Nếu không, bạn bắt đầu nhận mã spaghetti và không biết mã đang đi đâu trong quá trình gỡ lỗi.

Trong cuộc sống thực, tôi cảm thấy rằng khả năng đọc mã là thực sự quan trọng để nếu người khác cần khắc phục hoặc gỡ lỗi một vấn đề với mã của bạn, họ sẽ dễ dàng thực hiện.

Cũng như nếu đó là một vấn đề về hiệu suất bởi vì bạn đang chạy qua giao diện này hàng triệu lần thì vòng lặp for có thể nhanh hơn. Kiểm tra sẽ trả lời câu hỏi đó.

Tôi không biết C ++ nhưng tôi hoàn toàn hiểu hai ví dụ mã đầu tiên của bạn. Tôi giả sử rằng tiếp tục đi đến cuối vòng lặp for và sau đó lặp lại nó. Ví dụ trong khi tôi hiểu hoàn toàn nhưng đối với tôi nó dễ hiểu hơn ví dụ cho (;;) của bạn. Cái thứ ba tôi sẽ phải làm một số công việc suy nghĩ để tìm ra nó.

Vì vậy, đối với tôi, nó dễ đọc hơn (không biết c ++) và những gì người hướng dẫn logic của tôi nói rằng tôi sẽ sử dụng vòng lặp while.


4
Vì vậy, người hướng dẫn của bạn chống lại việc sử dụng ngoại lệ? Và về sớm? Và những điều tương tự? Được sử dụng đúng cách, tất cả đều làm cho mã dễ đọc hơn ...
Ded repeatator

1
Cụm từ chính trong câu elip của bạn là "được sử dụng đúng cách". Kinh nghiệm cay đắng của tôi đối với tôi chỉ ra rằng chết tiệt gần NOBODY trong cây vợt điên này biết cách sử dụng những thứ đó "đúng cách", cho hầu hết mọi định nghĩa hợp lý về "đúng" (hoặc "có thể đọc được", cho vấn đề đó).
John R. Strohm

2
Hãy nhớ rằng chúng ta đang nói về một lớp học mà học sinh là người mới bắt đầu. Chắc chắn, đào tạo họ để tránh những điều gây ra vấn đề phổ biến trong khi họ là người mới bắt đầu (trước khi họ hiểu những hàm ý và có thể lý do về những gì sẽ là một ngoại lệ cho quy tắc) là một ý tưởng tốt.
dùng1118321

2
Giáo điều "một lối vào - một lần thoát" là IMHO một quan điểm cổ xưa, có lẽ là "phát minh" bởi Edward Dijkstra tại thời điểm mà mọi người đang làm rối tung các GOTO bằng các ngôn ngữ như Basic, Pascal và COBOL. Tôi hoàn toàn đồng ý với những gì Ded repeatator đã viết ở trên. Hơn nữa, hai ví dụ đầu tiên của OP thực sự chỉ chứa một mục nhập và một lối thoát, tuy nhiên khả năng đọc là tầm thường.
Doc Brown

4
Đồng ý với @DocBrown. "Một lối vào một lối ra" trong các cơ sở sản xuất hiện đại có xu hướng là một công thức cho hành trình mã và khuyến khích lồng sâu các cấu trúc điều khiển.
jdevlin

-2

Đối với tôi, bản dịch C thẳng về phía trước nhất của mã giả là

do
    {
    success = something();
    if (success == FALSE)
        {
        handle_the_error();
        }
    } while (success == FALSE)
\\ moving on ...

tôi không hiểu tại sao bản dịch rõ ràng này là một vấn đề.

có lẽ đây:

while (!something())
    {
    handle_the_error();
    }

Điều đó có vẻ đơn giản hơn.

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.