Đây có phải là một trường hợp sử dụng hợp lý cho goto trong C?


59

Tôi thực sự ngần ngại khi hỏi điều này, vì tôi không muốn "tranh luận, tranh luận, bỏ phiếu hoặc thảo luận mở rộng" nhưng tôi mới biết về C và muốn hiểu rõ hơn về các mẫu phổ biến được sử dụng trong ngôn ngữ.

Gần đây tôi đã nghe thấy một số khó chịu cho gotolệnh, nhưng gần đây tôi cũng đã tìm thấy một trường hợp sử dụng hợp lý cho nó.

Mã như thế này:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

Nếu phần dọn dẹp hoàn toàn giống nhau, có thể được viết đẹp hơn một chút (ý kiến ​​của tôi?) Như thế này:

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

Đây có phải là trường hợp sử dụng phổ biến hoặc được chấp nhận gototrong C không? Có một cách khác / tốt hơn để làm điều này?



2
Hơn cả lớp phủ đường, trình biên dịch đảm bảo dọn dẹp và kiểm tra lỗi, trong khi ở C, bạn phải thực hiện thủ công, vi phạm DRY và đảm bảo rằng ở một nơi nào đó bạn quá lười để kiểm tra mã lỗi hoặc đi đến nhãn sai hoặc đại loại như thế.
DeadMG

3
@Izkata: Không, vấn đề là phải có các hàm (như hàm hủy) được trình biên dịch gọi ngầm khi ném ngoại lệ.
DeadMG

6
@DeadMG: Sử dụng ngôn ngữ khác không phải lúc nào cũng là một lựa chọn.
Benjamin Kloster

13
Tôi nghĩ với cách đặt tên nhãn phù hợp, ví dụ có thể trông khá tốt:goto hell;
gnat

Câu trả lời:


48

Câu gotolệnh (và các nhãn tương ứng của nó) là một nguyên hàm kiểm soát luồng (cùng với việc thực hiện có điều kiện của một câu lệnh). Do đó, ý tôi là họ ở đó để cho phép bạn xây dựng các mạng điều khiển luồng chương trình. Bạn có thể nghĩ về chúng như mô hình hóa các mũi tên giữa các nút của sơ đồ.

Một số trong số này có thể được tối ưu hóa ngay lập tức, trong đó có một luồng tuyến tính trực tiếp (bạn chỉ cần sử dụng một chuỗi các câu lệnh cơ bản). Các mẫu khác được thay thế tốt nhất bằng các cấu trúc lập trình có cấu trúc, nơi các mẫu này có sẵn; Nếu nó trông giống như một whilevòng lặp, hãy sử dụng một whilevòng lặp , OK? Các mẫu lập trình có cấu trúc chắc chắn ít nhất có khả năng rõ ràng hơn về ý định hơn là một mớ hỗn độn của các gotocâu lệnh.

Tuy nhiên, C không bao gồm tất cả các cấu trúc lập trình có cấu trúc có thể. (Tôi không rõ ràng rằng tất cả những người có liên quan đã được phát hiện; tốc độ khám phá hiện đang chậm, nhưng tôi ngần ngại nói rằng tất cả đã được tìm thấy.) Trong số những người chúng tôi biết, C chắc chắn thiếu try/ catch/ finallycấu trúc (và ngoại lệ quá). Nó cũng thiếu breakvòng lặp đa cấp . Đây là những thứ mà a gotocó thể được sử dụng để thực hiện. Cũng có thể sử dụng các sơ đồ khác để thực hiện những điều này - chúng tôi biết rằng C có đủ bộ khônggotonguyên thủy - nhưng chúng thường liên quan đến việc tạo các biến cờ và các điều kiện vòng lặp hoặc bảo vệ phức tạp hơn nhiều; tăng sự vướng mắc của phân tích kiểm soát với phân tích dữ liệu làm cho chương trình khó hiểu hơn về tổng thể. Nó cũng làm cho trình biên dịch khó tối ưu hóa hơn và CPU thực thi nhanh chóng (hầu hết các cấu trúc điều khiển luồng - và chắc chắn goto - rất rẻ).

Vì vậy, trong khi bạn không nên sử dụng gototrừ khi cần thiết, bạn nên biết rằng nó tồn tại và nó thể cần thiết, và nếu bạn cần nó, bạn không nên cảm thấy quá tệ. Một ví dụ về trường hợp cần thiết là phân bổ tài nguyên khi hàm được gọi trả về một điều kiện lỗi. (Đó là, try/ finally.) Có thể viết mà không gotolàm nhưng việc đó có thể có nhược điểm của riêng nó, chẳng hạn như các vấn đề về duy trì nó. Một ví dụ về trường hợp:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

Mã có thể thậm chí ngắn hơn, nhưng nó đủ để chứng minh điểm.


4
+1: Trong C goto về mặt kỹ thuật không bao giờ là "cần thiết" - luôn có cách để làm điều đó, nó trở nên lộn xộn ..... Đối với một bộ hướng dẫn mạnh mẽ để sử dụng goto, hãy nhìn vào MISRA C.
mattnz

1
Bạn muốn try/catch/finallyđể gotomặc dù chưa phổ biến hơn (vì nó có thể lan rộng khắp nhiều chức năng / modules) hình thức tương tự, các mã spaghetti đó là có thể sử dụng try/catch/finally?
tự kỷ

65

Đúng.

Nó được sử dụng trong, ví dụ, kernel linux. Đây là một email từ cuối chuỗi từ gần một thập kỷ trước , đã gửi thư của tôi:

Từ: Robert Tình yêu
Chủ đề: Re: bất kỳ cơ hội nào của 2.6.0-test *?
Ngày: 12 tháng 1 năm 2003 17:58:06 -0500

Trên Mặt Trời, 2003-01-12 lúc 17:22, Rob Wilkens đã viết:

Tôi nói "vui lòng không sử dụng goto" và thay vào đó có chức năng "cleanup_lock" và thêm vào đó trước tất cả các câu lệnh trả về .. Nó không phải là một gánh nặng. Vâng, nó yêu cầu nhà phát triển làm việc chăm chỉ hơn một chút, nhưng kết quả cuối cùng là mã tốt hơn.

Không, nó là thô và nó làm nở hạt nhân . Nó nội tuyến một loạt rác cho các đường dẫn lỗi N, trái ngược với việc có mã thoát một lần ở cuối. Dấu chân bộ nhớ cache là chìa khóa và bạn chỉ cần giết nó.

Cũng không dễ đọc hơn.

Như là một lý lẽ cuối cùng, nó không cho phép chúng ta làm sạch gió thông thường và thư giãn , tức là

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

Bây giờ dừng lại này.

Robert tình yêu

Điều đó nói rằng, nó đòi hỏi rất nhiều kỷ luật để ngăn bạn tạo mã spaghetti một khi bạn đã quen với việc sử dụng goto, vì vậy trừ khi bạn viết một cái gì đó đòi hỏi tốc độ và dấu chân bộ nhớ thấp (như kernel hoặc hệ thống nhúng), bạn thực sự nên suy nghĩ về nó trước khi bạn viết goto đầu tiên.


21
Lưu ý rằng kernel khác với chương trình không kernel liên quan đến mức độ ưu tiên về tốc độ thô so với khả năng đọc. Nói cách khác, họ đã lập hồ sơ ALREADY và thấy rằng họ cần tối ưu hóa mã của mình để tăng tốc với goto.

11
Sử dụng stack un-gió để xử lý dọn dẹp lỗi mà không thực sự đẩy lên ngăn xếp! Đây là một công dụng tuyệt vời của goto.
mike30

1
@ user1249, Rác, bạn không thể cấu hình mọi ứng dụng {quá khứ, hiện tại, tương lai} sử dụng một đoạn mã {library, kernel}. Bạn chỉ cần nhanh chóng.
Pacerier

1
Không liên quan: Tôi ngạc nhiên về cách mọi người có thể sử dụng danh sách gửi thư để hoàn thành mọi việc, chứ đừng nói đến các dự án lớn như vậy. Nó chỉ là ... nguyên thủy. Làm thế nào để mọi người đến với pháo hoa của tin nhắn?!
Alexander

2
Tôi không quá quan tâm đến điều độ. Nếu ai đó đủ mềm yếu để bị từ chối bởi một số lỗ đít trên internet, dự án của bạn có lẽ tốt hơn nếu không có họ. Tôi quan tâm nhiều hơn đến tính không thực tế của việc theo kịp các tin nhắn đến và làm thế nào bạn có thể có một cuộc thảo luận ngược tự nhiên với rất ít công cụ để theo dõi các trích dẫn, ví dụ.
Alexander

14

Theo tôi, mã bạn đã đăng là một ví dụ về việc sử dụng hợp lệ goto, bởi vì bạn chỉ nhảy xuống và chỉ sử dụng nó như một trình xử lý ngoại lệ nguyên thủy.

Tuy nhiên , vì cuộc tranh luận về goto cũ, các lập trình viên đã tránh được gotokhoảng 40 năm và do đó họ không được sử dụng để đọc mã với goto. Đây là một lý do hợp lệ để tránh goto: nó đơn giản không phải là tiêu chuẩn.

Tôi đã viết lại mã như một thứ dễ đọc hơn bởi các lập trình viên C:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

Ưu điểm của thiết kế này:

  • Hàm thực hiện công việc thực tế không cần quan tâm đến các tác vụ không liên quan đến thuật toán của nó, chẳng hạn như phân bổ dữ liệu.
  • Mã sẽ trông ít xa lạ hơn với các lập trình viên C, vì họ sợ goto và nhãn.
  • Bạn có thể tập trung xử lý lỗi và xử lý lỗi tại cùng một điểm, bên ngoài chức năng thực hiện thuật toán. Nó không có ý nghĩa đối với một chức năng để xử lý kết quả của chính nó.


9

Trong Java, bạn sẽ làm như thế này:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

Tôi sử dụng nó rất nhiều. Nhiều như tôi không thích goto, trong hầu hết các ngôn ngữ kiểu C khác, tôi sử dụng mã của bạn; Không có cách nào khác tốt để làm điều đó. (Nhảy ra khỏi các vòng lặp lồng nhau là một trường hợp tương tự; trong Java tôi sử dụng một nhãn có nhãn breakvà ở mọi nơi khác tôi sử dụng a goto.)


3
Aw đó là một cấu trúc điều khiển gọn gàng.
Bryan Boettcher

4
Điều này thực sự thú vị. Tôi thường nghĩ về việc sử dụng cấu trúc try / Catch / cuối cùng cho điều này trong java (ném ngoại lệ thay vì phá vỡ).
Robz

5
Điều đó thực sự không thể đọc được (ít nhất là đối với tôi). Nếu có, ngoại lệ là tốt hơn nhiều.
m3th0dman

1
@ m3th0dman Tôi đồng ý với bạn trong ví dụ cụ thể này (xử lý lỗi). Nhưng có những trường hợp khác (không đặc biệt) trong đó thành ngữ này có thể có ích.
Konrad Rudolph

1
Các ngoại lệ rất tốn kém, chúng cần tạo ra lỗi, stacktrace và nhiều thứ linh tinh hơn. Phá vỡ nhãn này cho một lối thoát sạch từ một vòng kiểm tra. trừ khi người ta không quan tâm đến bộ nhớ và tốc độ, thì đối với tất cả những gì tôi quan tâm đều sử dụng một ngoại lệ.
Tschallacka

8

Tôi nghĩ rằng đó một trường hợp sử dụng hợp lý, nhưng trong trường hợp "lỗi" không có gì khác hơn một giá trị boolean, có một cách khác để thực hiện những gì bạn muốn:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

Điều này sử dụng đánh giá ngắn mạch của các toán tử boolean. Nếu điều này "tốt hơn", tùy thuộc vào sở thích cá nhân của bạn và cách bạn quen với thành ngữ đó.


1
Vấn đề với điều này là errorgiá trị của nó có thể trở nên vô nghĩa với tất cả các OR.
James

@James: đã chỉnh sửa câu trả lời của tôi do nhận xét của bạn
Doc Brown

1
Điều này là không đủ. Nếu xảy ra lỗi trong chức năng đầu tiên, tôi không muốn thực hiện chức năng thứ hai hoặc thứ ba.
Robz

2
Nếu với đánh giá tay ngắn, bạn có nghĩa là đánh giá ngắn mạch , thì đây chính xác không phải là những gì được thực hiện ở đây do sử dụng bitwise OR thay vì logic OR.
Chris nói phục hồi Monica

1
@ChristianRau: cảm ơn, đã chỉnh sửa câu trả lời của tôi cho phù hợp
Doc Brown

6

Hướng dẫn kiểu linux đưa ra các lý do cụ thể để sử dụng gotocác ví dụ phù hợp với ví dụ của bạn:

https://www.kernel.org/doc/Documentation/ Process /oding-style.rst

Lý do để sử dụng gotos là:

  • tuyên bố vô điều kiện dễ hiểu và làm theo
  • làm tổ được giảm
  • lỗi bằng cách không cập nhật các điểm thoát riêng lẻ khi thực hiện sửa đổi bị ngăn chặn
  • lưu công cụ biên dịch để tối ưu hóa mã dự phòng đi;)

Tuyên bố từ chối trách nhiệm Tôi không được phép chia sẻ công việc của tôi. Các ví dụ ở đây là một chút giả tạo vì vậy hãy chịu đựng với tôi.

Điều này là tốt cho quản lý bộ nhớ. Gần đây tôi đã làm việc với mã có bộ nhớ được cấp phát động (ví dụ: được char *trả về bởi một hàm). Một hàm xem xét một đường dẫn và xác định xem đường dẫn đó có hợp lệ hay không bằng cách phân tích các mã thông báo của đường dẫn:

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

Bây giờ với tôi, đoạn mã sau đẹp hơn và dễ bảo trì hơn nếu bạn cần thêm varNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

Bây giờ mã có tất cả các loại vấn đề khác với nó, cụ thể là N ở đâu đó trên 10 và hàm có hơn 450 dòng, với 10 cấp độ lồng nhau ở một số nơi.

Nhưng tôi đã đề nghị người giám sát của tôi cấu trúc lại nó, điều mà tôi đã làm và bây giờ nó là một loạt các chức năng đều ngắn và tất cả chúng đều có kiểu linux

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

Nếu chúng ta xem xét tương đương mà không có gotos:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

Đối với tôi, trong trường hợp đầu tiên, rõ ràng với tôi rằng nếu hàm đầu tiên quay trở lại NULL, chúng tôi sẽ ra khỏi đây và chúng tôi sẽ quay trở lại 0. Trong trường hợp thứ hai, tôi phải cuộn xuống để xem if có chứa toàn bộ hàm không. Cấp cái đầu tiên chỉ ra điều này cho tôi theo kiểu cách (tên " out") và cái thứ hai thực hiện theo cú pháp. Cái đầu tiên vẫn còn rõ ràng hơn.

Ngoài ra, tôi rất thích có các free()câu lệnh ở cuối hàm. Đó là một phần bởi vì, theo kinh nghiệm của tôi, các free()câu lệnh ở giữa các hàm có mùi khó chịu và cho tôi biết rằng tôi nên tạo một chương trình con. Trong trường hợp này, tôi đã tạo var1trong hàm của mình và không thể free()theo chương trình con, nhưng đó là lý do tại sao goto out_free, kiểu goto out rất thiết thực.

Tôi nghĩ rằng các lập trình viên cần phải được đưa lên để tin rằng đó gotolà xấu xa. Sau đó, khi họ đủ trưởng thành, họ nên duyệt mã nguồn Linux và đọc hướng dẫn kiểu linux.

Tôi nên thêm rằng tôi sử dụng phong cách này rất nhất quán, mọi chức năng đều có int retval, out_freenhãn và nhãn ngoài. Bởi vì sự nhất quán về phong cách, khả năng đọc được cải thiện.

Tiền thưởng: Nghỉ giải lao và tiếp tục

Nói rằng bạn có một vòng lặp while

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

Có những điều khác với mã này, nhưng có một điều là tuyên bố tiếp tục. Tôi muốn viết lại toàn bộ, nhưng tôi được giao nhiệm vụ sửa đổi nó theo một cách nhỏ. Tôi sẽ mất nhiều ngày để cấu trúc lại nó theo cách làm tôi hài lòng, nhưng sự thay đổi thực sự là khoảng nửa ngày làm việc. Vấn đề là ngay cả khi chúng continueta vẫn cần giải phóng var1var2. Tôi đã phải thêm một var3, và nó khiến tôi muốn ngất đi khi phải phản ánh các câu lệnh free ().

Tôi là một thực tập sinh tương đối mới vào thời điểm đó, nhưng tôi đã xem mã nguồn linux để giải trí một thời gian, vì vậy tôi đã hỏi người giám sát của mình nếu tôi có thể sử dụng câu lệnh goto. Anh ấy nói có, và tôi đã làm điều này:

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

Tôi nghĩ tiếp tục là tốt nhất nhưng với tôi họ giống như một goto với một nhãn vô hình. Cùng đi cho nghỉ. Tôi vẫn thích tiếp tục hoặc phá vỡ trừ khi, như trường hợp ở đây, nó buộc bạn phải phản chiếu các sửa đổi ở nhiều nơi.

Và tôi cũng nên thêm rằng việc sử dụng này goto next;next:nhãn không đạt yêu cầu đối với tôi. Chúng chỉ tốt hơn là phản chiếu free()các count++tuyên bố và tuyên bố.

gotoHầu như luôn luôn sai, nhưng người ta phải biết khi nào chúng tốt để sử dụng.

Một điều mà tôi đã không thảo luận là xử lý lỗi đã được bao phủ bởi các câu trả lời khác.

Hiệu suất

Người ta có thể nhìn vào việc thực hiện strtok () http://opensource.apple.com//source/Libc/Libc-167/opes.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

Vui lòng sửa cho tôi nếu tôi sai, nhưng tôi tin rằng cont:nhãn và goto cont;tuyên bố là có hiệu suất (chắc chắn họ không làm cho mã dễ đọc hơn). Chúng có thể được thay thế bằng mã có thể đọc được bằng cách làm

while( isDelim(*s++,delim));

để bỏ qua các dấu phân cách. Nhưng để nhanh nhất có thể và tránh các cuộc gọi chức năng không cần thiết, họ làm theo cách này.

Tôi đọc bài báo của Dijkstra và tôi thấy nó khá bí truyền.

google "dijkstra goto tuyên bố có hại" vì tôi không đủ uy tín để đăng hơn 2 liên kết.

Tôi đã xem nó được trích dẫn là một lý do không sử dụng goto và đọc nó đã không thay đổi bất cứ điều gì cho đến khi việc sử dụng goto của tôi được che giấu.

Phụ lục :

Tôi đã đưa ra một quy tắc gọn gàng trong khi suy nghĩ về tất cả những điều này về tiếp tục và phá vỡ.

  • Nếu trong một vòng lặp while, bạn có một tiếp tục, thì phần thân của vòng lặp while sẽ là một hàm và tiếp tục là một câu lệnh return.
  • Nếu trong một vòng lặp while, bạn có một câu lệnh break, thì chính vòng lặp while sẽ là một hàm và break sẽ trở thành một câu lệnh return.
  • Nếu bạn có cả hai, thì có thể có gì đó không ổn.

Không phải lúc nào cũng có thể do các vấn đề về phạm vi nhưng tôi đã thấy rằng việc này giúp cho việc lập luận về mã của tôi dễ dàng hơn nhiều. Tôi đã nhận thấy rằng bất cứ khi nào một vòng lặp nghỉ ngơi hoặc tiếp tục, nó mang lại cho tôi một cảm giác tồi tệ.


2
+1 nhưng tôi có thể không đồng ý ở một điểm không? "Tôi nghĩ rằng các lập trình viên cần phải được đưa lên để tin rằng goto là xấu xa." Có thể là như vậy, nhưng lần đầu tiên tôi học lập trình trong BASIC, với số dòng và GOTO, không có trình soạn thảo văn bản, vào năm 1975. Tôi đã gặp lập trình có cấu trúc mười năm sau đó, sau đó tôi phải mất một tháng để tự mình ngừng sử dụng GOTO bất kỳ áp lực để dừng lại. Ngày nay, tôi sử dụng GOTO một vài lần một năm vì nhiều lý do, nhưng nó không xuất hiện nhiều. Không được đưa lên để tin rằng GOTO là xấu xa đã làm tôi không có hại gì mà tôi biết, và nó thậm chí có thể đã làm một số điều tốt. Đó chỉ là tôi.
THB

1
Tôi nghĩ bạn đúng về điều đó. Tôi đã nảy ra ý tưởng rằng GOTO không được sử dụng và do tình cờ thuần túy, tôi đã duyệt mã nguồn Linux vào thời điểm tôi đang làm việc với mã có các chức năng này với nhiều điểm thoát với bộ nhớ miễn phí. Nếu không, tôi sẽ không bao giờ biết về các kỹ thuật này.
Philippe Carphin

1
@thb Ngoài ra, câu chuyện vui, tôi đã hỏi người giám sát của mình vào lúc đó, với tư cách là một thực tập sinh được phép sử dụng GOTO và tôi đã đảm bảo rằng tôi đã giải thích với anh ta rằng tôi sẽ sử dụng chúng theo cách cụ thể như cách nó được sử dụng trong Nhân Linux và anh ta nói "Ok, điều đó có ý nghĩa, và đồng thời, tôi không biết bạn có thể sử dụng GOTO trong C".
Philippe Carphin

1
@thb Tôi không biết liệu có tốt để đi vào vòng lặp (thay vì phá vỡ các vòng lặp) như thế này không? Chà, đó là một ví dụ tồi, nhưng tôi thấy rằng quicksort với các câu lệnh goto (ví dụ 7a) trên Lập trình có cấu trúc của Knuth đi đến các Tuyên bố không dễ hiểu lắm.
Yai0Phah

@ Yai0Phah Tôi sẽ giải thích quan điểm của mình nhưng lời giải thích của tôi không làm giảm ví dụ tốt đẹp của bạn 7a! Tôi tán thành ví dụ. Tuy nhiên, các sinh viên năm hai hách dịch thích giảng bài cho mọi người về goto. Thật khó để tìm thấy một cách sử dụng goto thực tế từ năm 1985 gây ra rắc rối đáng kể, trong khi người ta có thể tìm thấy những hình ảnh vô hại làm giảm bớt công việc của lập trình viên. Goto vì vậy hiếm khi phát sinh trong lập trình hiện đại, dù sao đi nữa, khi nó phát sinh, lời khuyên của tôi là, nếu bạn muốn sử dụng nó, thì có lẽ bạn chỉ nên sử dụng nó. Goto là tốt. Vấn đề chính với goto là một số người tin rằng goto phản cảm làm cho họ trông thông minh.
THB

5

Cá nhân tôi muốn cấu trúc lại nó giống như thế này:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

Tuy nhiên, điều đó sẽ được thúc đẩy nhiều hơn bằng cách tránh việc lồng sâu hơn là tránh goto (IMO là một vấn đề tồi tệ hơn với mẫu mã đầu tiên), và tất nhiên sẽ phụ thuộc vào CleanupAfterError có thể nằm ngoài phạm vi (trong trường hợp này là "params" có thể là một cấu trúc có chứa một số bộ nhớ được phân bổ mà bạn cần giải phóng, một TẬP TIN * mà bạn cần phải đóng hoặc bất cứ thứ gì).

Một lợi thế lớn mà tôi thấy với cách tiếp cận này là nó dễ dàng hơn và dễ dàng hơn để đặt một bước bổ sung giả định trong tương lai giữa, giả sử, FTCF2 và FTCF3 (hoặc loại bỏ một bước hiện tại), vì vậy nó cho vay tốt hơn để duy trì (và người kế thừa mã của tôi không muốn làm tôi thất vọng!) - bỏ qua một bên, phiên bản lồng nhau thiếu điều đó.


1
Tôi đã không nêu điều này trong câu hỏi của mình, nhưng có thể các FTCF KHÔNG có cùng tham số, làm cho mẫu này phức tạp hơn một chút. Cảm ơn mặc dù.
Robz

3

Hãy xem hướng dẫn mã hóa MISRA (Hiệp hội độ tin cậy phần mềm công nghiệp động cơ) cho phép goto theo các tiêu chí nghiêm ngặt (mà ví dụ của bạn đáp ứng)

Nơi tôi làm việc cùng một mã sẽ được viết - không cần goto - Tránh tranh luận tôn giáo không cần thiết về chúng là một điểm cộng lớn trong bất kỳ nhà phần mềm nào.

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

hoặc cho "goto in drag" - thứ gì đó thậm chí còn tinh ranh hơn goto, nhưng lại xoay quanh "No goto Ever !!!" cắm trại) "Chắc chắn là nó ổn, không sử dụng Goto" ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

Nếu các hàm có cùng loại tham số, đặt chúng vào bảng và sử dụng vòng lặp -


2
Các hướng dẫn MISRA-C: 2004 hiện tại không cho phép goto dưới mọi hình thức (xem quy tắc 14.4). Lưu ý rằng ủy ban MISRA luôn bối rối về vấn đề này, họ không biết nên đứng trên bàn chân nào. Đầu tiên, họ vô điều kiện cấm sử dụng goto, tiếp tục, v.v. Nhưng trong dự thảo cho MISRA 2011 sắp tới, họ muốn cho phép họ một lần nữa. Là một sidenote, xin lưu ý rằng MISRA cấm chuyển nhượng bên trong if-statement, vì những lý do rất chính đáng vì nó nguy hiểm hơn nhiều so với bất kỳ việc sử dụng goto nào.

1
Từ góc độ phân tích, việc thêm cờ vào chương trình tương đương với việc sao chép tất cả mã mã trong phạm vi cờ, mỗi if(flag)bản sao đều có nhánh "nếu" và mỗi câu lệnh tương ứng trong bản sao khác sẽ lấy " khác ". Các hành động thiết lập và xóa cờ thực sự là "gotos" nhảy giữa hai phiên bản mã đó. Đôi khi việc sử dụng cờ sạch hơn bất kỳ giải pháp thay thế nào, nhưng thêm cờ để lưu một gotomục tiêu không phải là một sự đánh đổi tốt.
supercat

1

Tôi cũng sử dụng gotonếu tin do/while/continue/breaktặc thay thế sẽ ít đọc hơn .

gotos có lợi thế là mục tiêu của họ có tên và họ đọc goto something;. Điều này có thể dễ đọc hơn breakhoặc continuenếu bạn không thực sự dừng lại hoặc tiếp tục một cái gì đó.


4
Bất cứ nơi nào bên trong một do ... while(0)hoặc một cấu trúc khác không phải là một vòng lặp thực sự mà là một nỗ lực hạn chế để ngăn chặn việc sử dụng a goto.
aib

1
À, cảm ơn, tôi không biết thương hiệu đặc biệt này "Tại sao ai đó lại làm thế?!" cấu trúc chưa.
Benjamin Kloster

2
Thông thường, tin tặc do / while / continue / break chỉ trở nên không thể đọc được khi mô-đun chứa nó quá dài ở vị trí đầu tiên.
John R. Strohm

2
Tôi không thể tìm thấy bất cứ điều gì trong đây như là một biện minh để sử dụng goto. Phá vỡ và tiếp tục có một hậu quả rõ ràng. đi đến nơi? Nhãn ở đâu? Break và goto cho bạn biết chính xác bước tiếp theo là gì và gần đó.
Rig

1
Nhãn tất nhiên phải được nhìn thấy từ bên trong vòng lặp. Tôi đồng ý với phần dài của phần bình luận của @John R. Strohm. Và quan điểm của bạn, được dịch thành tin tặc vòng lặp, trở thành "Thoát ra khỏi cái gì? Đây không phải là một vòng lặp!". Trong mọi trường hợp, điều này đang trở thành điều mà OP sợ nó có thể xảy ra, vì vậy tôi đang từ bỏ cuộc thảo luận.
aib

-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:

Nếu chỉ có một vòng lặp, breakhoạt động chính xác như thế goto, mặc dù không có sự kỳ thị.
9000

6
-1: Đầu tiên, x và y nằm ngoài phạm vi tìm thấy:, vì vậy điều này không giúp ích gì cho bạn. Thứ hai, với mã như được viết, thực tế là bạn đã tìm thấy: không có nghĩa là bạn đã tìm thấy những gì bạn đang tìm kiếm.
John R. Strohm

Đó là bởi vì đây là ví dụ nhỏ nhất mà tôi có thể nghĩ ra cho trường hợp thoát ra khỏi nhiều vòng lặp. Xin vui lòng chỉnh sửa nó cho một nhãn tốt hơn hoặc kiểm tra thực hiện.
aib

1
Nhưng cũng nên nhớ rằng các hàm C không nhất thiết không có tác dụng phụ.
aib

1
@ JohnR.Strohm Điều đó không có ý nghĩa gì nhiều ... Nhãn 'tìm thấy' được sử dụng để phá vỡ vòng lặp, không phải để kiểm tra các biến. Nếu tôi muốn kiểm tra các biến tôi có thể làm một cái gì đó như thế này: for (int y = 0; y <height; ++ y) {for (int x = 0; x <width; ++ x) {if (find (find (find (find x, y)) {doSomeT BreathWith (x, y); tìm thấy goto; }}} đã tìm thấy:
YoYoYonnY

-1

Sẽ luôn có những trại nói một cách là chấp nhận được và một cách khác thì không. Các công ty tôi từng làm việc đã cau mày hoặc không khuyến khích sử dụng goto. Cá nhân, tôi không thể nghĩ đến bất cứ lúc nào tôi đã sử dụng một cái, nhưng điều đó không có nghĩa là chúng xấu , chỉ là một cách làm khác.

Trong C, tôi thường làm như sau:

  • Kiểm tra các điều kiện có thể ngăn chặn xử lý (đầu vào xấu, v.v.) và "trả lại"
  • Thực hiện tất cả các bước yêu cầu phân bổ tài nguyên (ví dụ: mallocs)
  • Thực hiện xử lý, trong đó nhiều bước kiểm tra thành công
  • Giải phóng bất kỳ tài nguyên nào, nếu được phân bổ thành công
  • Trả lại bất kỳ kết quả

Để xử lý, sử dụng ví dụ goto của bạn, tôi sẽ làm điều này:

error = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

Không có lồng nhau và bên trong mệnh đề if, bạn có thể thực hiện bất kỳ báo cáo lỗi nào, nếu bước tạo ra lỗi. Vì vậy, nó không phải "tệ" hơn phương pháp sử dụng gotos.

Tôi vẫn chưa chạy qua một trường hợp ai đó có gotos không thể thực hiện bằng phương pháp khác và chỉ có thể đọc / hiểu được và đó là chìa khóa, IMHO.

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.