Sử dụng hợp lệ goto để quản lý lỗi trong C?


92

Câu hỏi này thực sự là kết quả của một cuộc thảo luận thú vị trên trang programme.reddit.com cách đây không lâu. Về cơ bản, nó tóm tắt thành đoạn mã sau:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Việc sử dụng gotoở đây dường như là cách tốt nhất để đi, dẫn đến mã sạch nhất và hiệu quả nhất trong tất cả các khả năng, hoặc ít nhất là theo tôi. Trích dẫn Steve McConnell trong Code Complete :

Goto hữu ích trong một quy trình phân bổ tài nguyên, thực hiện các hoạt động trên các tài nguyên đó và sau đó phân bổ tài nguyên. Với goto, bạn có thể xóa trong một phần của mã. Goto làm giảm khả năng bạn quên phân bổ tài nguyên ở mỗi nơi bạn phát hiện lỗi.

Một hỗ trợ khác cho phương pháp này đến từ cuốn sách Trình điều khiển thiết bị Linux , trong phần này .

Bạn nghĩ sao? Trường hợp này có hợp lệ sử dụng gototrong C không? Bạn có muốn các phương pháp khác tạo ra mã phức tạp hơn và / hoặc kém hiệu quả hơn, nhưng tránh goto?



@Eli: Tại sao bạn không xóa các thẻ và đặt hàm (cleanup_3 ();) trong ngoặc đơn của if?

@Akito: ý bạn là gì? Bạn có thể đăng đề xuất của mình như một câu trả lời với một mẫu mã hoàn chỉnh không?
Eli Bendersky

@EliBendersky: Vui lòng xem câu trả lời của tôi.

Một trong những điều tôi ghét nhất từ Visual Basic (VBS và VB6 bao gồm) là on error goto errorhệ thống xử lý lỗi :)
Manu343726

Câu trả lời:


61

FWIF, tôi thấy thành ngữ xử lý lỗi mà bạn đưa ra trong ví dụ của câu hỏi dễ đọc và dễ hiểu hơn bất kỳ phương án nào được đưa ra trong các câu trả lời cho đến nay. gotoNhìn chung, mặc dù là một ý tưởng tồi nhưng nó có thể hữu ích cho việc xử lý lỗi khi được thực hiện một cách đơn giản và thống nhất. Trong tình huống này, mặc dù đó là một goto, nó được sử dụng theo cách được xác định rõ ràng và ít nhiều có cấu trúc.


1
Anh ta không thể chỉ xóa những thẻ đó và đặt các chức năng trực tiếp vào nếu được? Nó sẽ dễ đọc hơn sao?

8
@StartupCrazy, tôi biết điều này đã lâu đời, nhưng vì lợi ích của các bài đăng trên trang này, tôi sẽ chỉ ra rằng không có anh ấy không thể. Nếu anh ta gặp lỗi tại goto error3 trong mã của anh ta, anh ta sẽ chạy dọn dẹp 1 2 và 3, trong giải pháp được yêu cầu của bạn, anh ta sẽ chỉ chạy dọn dẹp 3. Anh ta có thể lồng mọi thứ, nhưng đó sẽ chỉ là mũi tên phản vật chất, điều mà các lập trình viên nên tránh .
gbtimmon

17

Theo nguyên tắc chung, tránh sử dụng goto là một ý tưởng hay, nhưng những hành vi lạm dụng phổ biến khi Dijkstra lần đầu tiên viết 'GOTO được coi là có hại' thậm chí không vượt qua tâm trí của hầu hết mọi người ngày nay.

Những gì bạn phác thảo là một giải pháp tổng quát cho vấn đề xử lý lỗi - với tôi, điều đó là ổn miễn là nó được sử dụng cẩn thận.

Ví dụ cụ thể của bạn có thể được đơn giản hóa như sau (bước 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Tiếp tục quá trình:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

Tôi tin rằng đây là mã gốc. Điều này trông đặc biệt rõ ràng vì bản thân mã ban đầu rất sạch sẽ và được tổ chức tốt. Thông thường, các đoạn mã không gọn gàng như vậy (mặc dù tôi chấp nhận một lập luận rằng chúng phải như vậy); ví dụ: thường có nhiều trạng thái để chuyển cho các quy trình khởi tạo (thiết lập) hơn được hiển thị và do đó cũng có nhiều trạng thái hơn để chuyển cho các quy trình dọn dẹp.


23
Đúng, giải pháp lồng nhau là một trong những giải pháp thay thế khả thi. Thật không may, nó trở nên kém khả thi hơn khi mức độ làm tổ ngày càng sâu.
Eli Bendersky

4
@eliben: Đồng ý - nhưng lồng ghép sâu hơn có thể (có thể là) một dấu hiệu cho thấy bạn cần giới thiệu nhiều chức năng hơn hoặc yêu cầu các bước chuẩn bị thực hiện nhiều hơn, hoặc cấu trúc lại mã của bạn. Tôi có thể tranh luận rằng mỗi chức năng chuẩn bị nên thực hiện thiết lập của chúng, gọi chức năng tiếp theo trong chuỗi và tự dọn dẹp chúng. Nó bản địa hóa công việc đó - bạn thậm chí có thể tiết kiệm trên ba chức năng dọn dẹp. Nó cũng phụ thuộc một phần vào việc liệu bất kỳ chức năng thiết lập hoặc dọn dẹp nào được sử dụng (có thể sử dụng) trong bất kỳ trình tự gọi nào khác hay không.
Jonathan Leffler

6
Thật không may, điều này không hoạt động với các vòng lặp - nếu một lỗi xảy ra bên trong một vòng lặp, thì goto sẽ gọn gàng hơn nhiều so với việc thay thế việc đặt và kiểm tra cờ và câu lệnh 'break' (chỉ là những gotos được ngụy trang khéo léo).
Adam Rosenfield

1
@Smith, Giống như lái xe mà không có áo chống đạn.
strager

6
Tôi biết mình đang tìm kiếm ở đây, nhưng tôi thấy lời khuyên này khá kém - nên tránh kiểu chống mũi tên .
KingRadical

16

Tôi ngạc nhiên là không ai đề xuất phương án thay thế này, vì vậy mặc dù câu hỏi đã xuất hiện một thời gian, tôi sẽ thêm nó vào: một cách tốt để giải quyết vấn đề này là sử dụng các biến để theo dõi trạng thái hiện tại. Đây là một kỹ thuật có thể được sử dụng cho dù có gotođược sử dụng để đến mã dọn dẹp hay không. Giống như bất kỳ kỹ thuật mã hóa nào, nó có ưu và nhược điểm và sẽ không phù hợp với mọi tình huống, nhưng nếu bạn đang chọn một phong cách thì đáng để cân nhắc - đặc biệt nếu bạn muốn tránh gotomà không kết thúc với các ifs lồng nhau sâu sắc .

Ý tưởng cơ bản là, đối với mọi hành động dọn dẹp có thể cần thực hiện, có một biến từ giá trị của nó mà chúng ta có thể cho biết liệu hoạt động dọn dẹp có cần thực hiện hay không.

Tôi sẽ hiển thị gotophiên bản trước, vì nó gần với mã trong câu hỏi ban đầu hơn.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Một ưu điểm của điều này so với một số kỹ thuật khác là, nếu thứ tự của các hàm khởi tạo bị thay đổi, việc dọn dẹp chính xác sẽ vẫn xảy ra - ví dụ: sử dụng switchphương pháp được mô tả trong một câu trả lời khác, nếu thứ tự khởi tạo thay đổi, thì switchphải được chỉnh sửa rất cẩn thận để tránh cố gắng xóa một cái gì đó không thực sự được khởi tạo ngay từ đầu.

Bây giờ, một số người có thể tranh luận rằng phương pháp này thêm rất nhiều biến phụ - và thực sự trong trường hợp này, điều đó đúng - nhưng trong thực tế, một biến hiện có đã theo dõi hoặc có thể được thực hiện để theo dõi, trạng thái cần thiết. Ví dụ: nếu prepare_stuff()thực sự là một lệnh gọi đến malloc()hoặc đến open(), thì biến giữ con trỏ trả về hoặc bộ mô tả tệp có thể được sử dụng - ví dụ:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

Bây giờ, nếu chúng tôi theo dõi thêm trạng thái lỗi với một biến, chúng tôi có thể tránh gotohoàn toàn và vẫn dọn dẹp một cách chính xác, mà không có thụt lề ngày càng sâu hơn khi chúng tôi cần khởi tạo nhiều hơn:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Một lần nữa, có những lời chỉ trích tiềm ẩn về điều này:

  • Không phải tất cả những "nếu" của hiệu suất làm tổn thương? Không - bởi vì trong trường hợp thành công, bạn phải thực hiện tất cả các kiểm tra (nếu không, bạn sẽ không kiểm tra tất cả các trường hợp lỗi); và trong trường hợp không thành công, hầu hết các trình biên dịch sẽ tối ưu hóa chuỗi if (oksofar)kiểm tra không thành công cho một lần chuyển đến mã dọn dẹp (GCC chắc chắn có) - và trong mọi trường hợp, trường hợp lỗi thường ít quan trọng hơn đối với hiệu suất.
  • Đây không phải là thêm một biến khác? Trong trường hợp này là có, nhưng thường thì return_valuebiến có thể được sử dụng để đóng vai trò oksofarở đây. Nếu bạn cấu trúc các hàm của mình để trả về lỗi một cách nhất quán, bạn thậm chí có thể tránh lỗi thứ hai iftrong mỗi trường hợp:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }

    Một trong những lợi thế của việc viết mã như vậy là tính nhất quán có nghĩa là bất kỳ nơi nào mà lập trình viên ban đầu đã quên kiểm tra giá trị trả về sẽ nhô ra như một ngón tay cái đau, khiến việc tìm thấy (một loại) lỗi dễ dàng hơn nhiều.

Vì vậy - đây là (chưa) một phong cách nữa có thể được sử dụng để giải quyết vấn đề này. Được sử dụng đúng cách, nó cho phép tạo ra mã rất rõ ràng, nhất quán - và giống như bất kỳ kỹ thuật nào, nếu dùng sai nó có thể tạo ra mã dài dòng và khó hiểu :-)


2
Có vẻ như bạn đến bữa tiệc muộn, nhưng tôi chắc chắn thích câu trả lời!

Linus có thể sẽ từ chối mã của bạn blog.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: Nếu anh ấy muốn, đó là đặc quyền của anh ấy - nhưng tôi không chắc, dựa trên bài viết bạn đã liên kết, đó là trường hợp: lưu ý rằng theo kiểu tôi đang mô tả, (1) các điều kiện không lồng vào nhau sâu tùy ý và (2) không có câu lệnh "nếu" bổ sung nào so với những gì bạn cần (giả sử bạn sẽ kiểm tra tất cả các mã trả lại).
psmears

Rất nhiều điều này thay vì giải pháp goto khủng khiếp và thậm chí còn tồi tệ hơn của giải pháp phản vật chất!
The Paramagnetic Croissant,

8

Vấn đề với gototừ khóa chủ yếu bị hiểu nhầm. Nó không phải là điều ác. Bạn chỉ cần biết về các đường dẫn điều khiển bổ sung mà bạn tạo với mọi goto. Rất khó để suy luận về mã của bạn và do đó tính hợp lệ của nó.

FWIW, nếu bạn tra cứu các hướng dẫn của developer.apple.com, họ sẽ sử dụng phương pháp goto để xử lý lỗi.

Chúng tôi không sử dụng gotos. Tầm quan trọng cao hơn được đặt trên các giá trị trả về. Xử lý ngoại lệ được thực hiện thông qua setjmp/longjmp- bất cứ điều gì bạn có thể.


8
Mặc dù tôi chắc chắn đã sử dụng setjmp / longjmp trong một số trường hợp mà nó được gọi, tôi sẽ coi nó thậm chí còn "tệ hơn" so với goto (mà tôi cũng sử dụng, hơi ít dè dặt hơn, khi nó được gọi). Lần duy nhất tôi sử dụng setjmp / longjmp là khi (1) Mục tiêu sẽ tắt mọi thứ theo cách không bị ảnh hưởng bởi trạng thái hiện tại của nó hoặc (2) Mục tiêu sẽ khởi động lại mọi thứ được kiểm soát bên trong khối setjmp / longjmp được bảo vệ theo cách độc lập với trạng thái hiện tại của nó.
supercat

4

Không có gì sai về mặt đạo đức về câu lệnh goto hơn là có điều gì đó sai về mặt đạo đức với các con trỏ (void) *.

Tất cả là ở cách bạn sử dụng công cụ. Trong trường hợp (tầm thường) mà bạn đã trình bày, một câu lệnh tình huống có thể đạt được cùng một logic, mặc dù có nhiều chi phí hơn. Câu hỏi thực sự là, "yêu cầu tốc độ của tôi là gì?"

goto rất nhanh, đặc biệt nếu bạn cẩn thận để đảm bảo rằng nó biên dịch thành một bước nhảy ngắn. Hoàn hảo cho các ứng dụng có tốc độ cao. Đối với các ứng dụng khác, có thể hợp lý khi sử dụng trường hợp if / else + để bảo trì.

Hãy nhớ rằng: goto không giết ứng dụng, nhà phát triển giết ứng dụng.

CẬP NHẬT: Đây là ví dụ trường hợp

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

1
Bạn có thể trình bày phương án thay thế 'trường hợp' không? Ngoài ra, tôi sẽ tranh luận rằng điều này khác với void *, điều này được yêu cầu đối với bất kỳ sự trừu tượng hóa cấu trúc dữ liệu nghiêm trọng nào trong C. Tôi không nghĩ rằng có ai đó nghiêm túc phản đối void * và bạn sẽ không tìm thấy một cơ sở mã lớn nào nếu không có nó.
Eli Bendersky

Re: void *, đó chính xác là quan điểm của tôi, không có gì sai về mặt đạo đức cả. Ví dụ về switch / case bên dưới. int foo (int bar) {int return_value = 0; int fail_value = 0; if (! do_something (bar)) {fail_value = 1; } else if (! init_stuff (bar)) {fail_value = 2; } else if (standard_stuff (bar)) {{return_value = do_the_thing (bar); dọn dẹp_3 (); } switch (fail_value) {case 2: cleanup_2 (); phá vỡ ; case 1: cleanup_1 (); phá vỡ ; default: nghỉ; }}
webmarc

5
@webmarc, xin lỗi nhưng điều này thật kinh khủng! Bạn vừa mới mô phỏng hoàn toàn goto với các nhãn - phát minh ra các giá trị không mô tả của riêng bạn cho các nhãn và triển khai goto với switch / case. Failure_value = 1 rõ ràng hơn "goto cleanup_something"?
Eli Bendersky

4
Tôi cảm thấy như bạn sắp đặt cho tôi ở đây ... câu hỏi của bạn là một ý kiến, và những gì tôi muốn. Tuy nhiên, khi tôi đưa ra câu trả lời của mình, nó thật kinh khủng. :-( Đối với khiếu nại của bạn về các tên nhãn, chúng chỉ mang tính mô tả như phần còn lại của ví dụ: cleanup_1, foo, bar. Tại sao bạn lại tấn công các tên nhãn khi câu hỏi đó thậm chí còn không hợp pháp?
webmarc

1
Tôi không có ý định "sắp đặt" và gây ra bất kỳ loại cảm giác tiêu cực nào, xin lỗi vì điều đó! Có vẻ như cách tiếp cận mới của bạn chỉ nhắm mục tiêu vào 'loại bỏ goto' mà không thêm bất kỳ sự rõ ràng nào. Nó giống như thể bạn đã hoàn thành lại những gì goto làm, chỉ sử dụng thêm mã ít rõ ràng hơn. IMHO này không phải là một điều tốt để làm - loại bỏ goto chỉ vì lợi ích của nó.
Eli Bendersky

2

GOTO rất hữu ích. Đó là thứ mà bộ xử lý của bạn có thể làm và đây là lý do tại sao bạn nên có quyền truy cập vào nó.

Đôi khi bạn muốn thêm một chút gì đó vào chức năng của mình và goto đơn cho phép bạn thực hiện điều đó một cách dễ dàng. Nó có thể tiết kiệm thời gian ..


3
Bạn không cần quyền truy cập vào mọi thứ mà bộ xử lý của bạn có thể làm. Hầu hết thời gian goto là khó hiểu hơn so với thay thế.
David Thornley

@DavidThornley: Vâng, bạn làm cần truy cập vào mọi điều duy nhất xử lý của bạn có thể làm, nếu không, bạn đang lãng phí bộ vi xử lý của bạn. Goto là hướng dẫn tốt nhất trong lập trình.
Ron Maimon

1

Nói chung, tôi coi thực tế là một đoạn mã có thể được viết rõ ràng nhất bằng cách sử dụng gotonhư một dấu hiệu cho thấy dòng chương trình có thể phức tạp hơn mong muốn chung. Việc kết hợp các cấu trúc chương trình khác theo những cách kỳ lạ để tránh việc sử dụng gotosẽ cố gắng điều trị triệu chứng hơn là bệnh. Ví dụ cụ thể của bạn có thể không quá khó thực hiện nếu không có goto:

  làm {
    .. thiết lập thing1 sẽ chỉ cần dọn dẹp trong trường hợp thoát sớm
    if (lỗi) break;
    làm
    {
      .. thiết lập thing2 sẽ cần dọn dẹp trong trường hợp thoát sớm
      if (lỗi) break;
      // ***** XEM VĂN BẢN LIÊN QUAN ĐẾN DÒNG NÀY
    } trong khi (0);
    .. thu dọn điều2;
  } while (0);
  .. thu dọn điều1;

nhưng nếu việc dọn dẹp chỉ được thực hiện khi chức năng bị lỗi, gototrường hợp này có thể được xử lý bằng cách đặt returnngay trước nhãn đích đầu tiên. Đoạn mã trên sẽ yêu cầu thêm một returnở dòng được đánh dấu bằng *****.

Trong kịch bản "dọn dẹp ngay cả trong trường hợp bình thường", tôi sẽ coi việc sử dụng gotonhư là rõ ràng hơn so với do/ while(0)cấu trúc, trong số những thứ khác bởi vì bản thân các nhãn đích thực tế kêu "HÃY NHÌN TÔI" nhiều hơn so với breakdo/ while(0)cấu trúc. Đối với trường hợp "chỉ dọn dẹp nếu có lỗi", returncâu lệnh cuối cùng phải ở nơi tồi tệ nhất có thể từ quan điểm dễ đọc (câu lệnh trả về thường phải ở đầu một hàm hoặc ở vị trí khác "trông như thế nào" kết thúc); có returnnhãn ngay trước khi nhãn mục tiêu đáp ứng tiêu chuẩn đó dễ dàng hơn nhiều so với việc có nhãn ngay trước khi kết thúc "vòng lặp".

BTW, một tình huống mà đôi khi tôi sử dụng gotođể xử lý lỗi là trong một switchcâu lệnh, khi mã cho nhiều trường hợp chia sẻ cùng một mã lỗi. Mặc dù trình biên dịch của tôi thường đủ thông minh để nhận ra rằng nhiều trường hợp kết thúc bằng cùng một mã, tôi nghĩ rõ ràng hơn khi nói:

 REPARSE_PACKET:
  chuyển đổi (gói [0])
  {
    trường hợp PKT_THIS_OPERATION:
      nếu (điều kiện vấn đề)
        goto PACKET_ERROR;
      ... xử lý NÀY_OPERATION
      phá vỡ;
    trường hợp PKT_THAT_OPERATION:
      nếu (điều kiện vấn đề)
        goto PACKET_ERROR;
      ... xử lý THAT_OPERATION
      phá vỡ;
    ...
    trường hợp PKT_PROCESS_CONDITIONALLY
      if (pack_length <9)
        goto PACKET_ERROR;
      if (điều kiện gói liên quan đến gói [4])
      {
        độ dài gói - = 5;
        memmove (gói tin, gói tin + 5, độ dài gói tin);
        goto REPARSE_PACKET;
      }
      khác
      {
        gói [0] = PKT_CONDITION_SKIPPED;
        pack [4] = pack_length;
        độ dài gói = 5;
        pack_status = READY_TO_SEND;
      }
      phá vỡ;
    ...
    mặc định:
    {
     PACKET_ERROR:
      pack_error_count ++;
      độ dài gói = 4;
      gói [0] = PKT_ERROR;
      pack_status = READY_TO_SEND;
      phá vỡ;
    }
  }   

Mặc dù người ta có thể thay thế các gotocâu lệnh bằng {handle_error(); break;}và mặc dù người ta có thể sử dụng vòng lặp do/ while(0)cùng với continueđể xử lý gói thực thi có điều kiện được bao bọc, tôi thực sự không nghĩ điều đó rõ ràng hơn việc sử dụng a goto. Hơn nữa, mặc dù có thể sao chép mã từ PACKET_ERRORmọi nơi goto PACKET_ERRORđược sử dụng và trong khi trình biên dịch có thể viết ra mã trùng lặp một lần và thay thế hầu hết các lần xuất hiện bằng một lần chuyển đến bản sao được chia sẻ đó, thì việc sử dụng mã này gotogiúp bạn dễ dàng nhận thấy các địa điểm hơn mà thiết lập gói tin khác một chút (ví dụ: nếu lệnh "thực thi có điều kiện" quyết định không thực thi).


1

Cá nhân tôi là một tín đồ của "Sức mạnh của Mười - 10 Quy tắc Viết Quy tắc Quan trọng về An toàn" .

Tôi sẽ bao gồm một đoạn trích nhỏ từ văn bản đó minh họa những gì tôi tin là một ý tưởng hay về goto.


Quy tắc: Hạn chế tất cả mã trong các cấu trúc luồng điều khiển rất đơn giản - không sử dụng câu lệnh goto, cấu trúc setjmp hoặc longjmp và đệ quy trực tiếp hoặc gián tiếp.

Cơ sở lý luận: Quy trình kiểm soát đơn giản hơn chuyển thành khả năng xác minh mạnh hơn và thường dẫn đến độ rõ ràng của mã được cải thiện. Việc loại bỏ đệ quy có lẽ là bất ngờ lớn nhất ở đây. Tuy nhiên, nếu không có đệ quy, chúng tôi được đảm bảo có một biểu đồ gọi hàm xoay vòng, có thể được khai thác bởi trình phân tích mã và có thể trực tiếp giúp chứng minh rằng tất cả các thực thi cần được giới hạn trên thực tế đều bị giới hạn. (Lưu ý rằng quy tắc này không yêu cầu tất cả các hàm phải có một điểm trả về duy nhất - mặc dù điều này cũng thường đơn giản hóa quy trình điều khiển. Tuy nhiên, có đủ trường hợp, trong đó trả về lỗi sớm là giải pháp đơn giản hơn.)


Việc cấm sử dụng goto có vẻ không tốt nhưng:

Nếu ban đầu các quy tắc có vẻ hà khắc, hãy nhớ rằng chúng nhằm mục đích giúp bạn có thể kiểm tra mã mà theo nghĩa đen, cuộc sống của bạn có thể phụ thuộc vào tính đúng đắn của nó: mã được sử dụng để điều khiển chiếc máy bay bạn bay, nhà máy điện hạt nhân một vài dặm từ nơi bạn sinh sống, hoặc tàu vũ trụ mang phi hành gia lên quỹ đạo. Các quy tắc hoạt động giống như việc thắt dây an toàn trong xe hơi của bạn: ban đầu chúng có thể hơi khó chịu, nhưng sau một thời gian, việc sử dụng chúng trở thành bản chất thứ hai và việc không sử dụng chúng trở thành điều không tưởng.


22
Vấn đề với điều này là cách thông thường để loại bỏ hoàn toàn gotolà sử dụng một số tập hợp các boolean "thông minh" trong ifs hoặc vòng lặp lồng nhau sâu. Điều đó thực sự không giúp ích gì. Có thể các công cụ của bạn sẽ kiểm tra nó tốt hơn, nhưng bạn sẽ không và bạn quan trọng hơn.
Donal Fellows

1

Tôi đồng ý rằng dọn dẹp goto theo thứ tự ngược lại được đưa ra trong câu hỏi là cách dọn dẹp mọi thứ sạch sẽ nhất trong hầu hết các chức năng. Nhưng tôi cũng muốn chỉ ra rằng đôi khi, bạn muốn chức năng của mình được dọn dẹp. Trong những trường hợp này, tôi sử dụng biến thể sau thành ngữ if (0) {label:} để đi đến đúng điểm của quá trình dọn dẹp:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

Có vẻ với tôi rằng cleanup_3nên làm sạch nó, sau đó gọi cleanup_2. Tương tự, cleanup_2nên thực hiện dọn dẹp, sau đó gọi dọn dẹp_1. Có vẻ như bất cứ khi nào bạn làm cleanup_[n], điều đó cleanup_[n-1]là bắt buộc, do đó, nó phải là trách nhiệm của phương thức (ví dụ, vì vậy, cleanup_3không bao giờ có thể được gọi nếu không gọicleanup_2 và có thể gây ra rò rỉ.)

Với cách tiếp cận đó, thay vì gotos, bạn chỉ cần gọi quy trình dọn dẹp, sau đó quay lại.

Cách gototiếp cận không sai hoặc không tốt , chỉ cần lưu ý rằng nó không nhất thiết phải là cách tiếp cận "sạch sẽ nhất" (IMHO).

Nếu bạn đang tìm kiếm hiệu suất tối ưu, thì tôi cho rằng goto giải pháp đó là tốt nhất. Tuy nhiên, tôi chỉ mong rằng nó có liên quan trong một số ít ứng dụng, quan trọng về hiệu suất (ví dụ: trình điều khiển thiết bị, thiết bị nhúng, v.v.). Nếu không, đó là một tối ưu hóa vi mô có mức độ ưu tiên thấp hơn độ rõ ràng của mã.


4
Điều đó sẽ không cắt giảm - việc dọn dẹp dành riêng cho các tài nguyên, được phân bổ theo thứ tự chỉ trong quy trình này. Ở những nơi khác, chúng không có liên quan, vì vậy việc gọi cái này từ cái kia không có ý nghĩa.
Eli Bendersky

0

Tôi nghĩ rằng câu hỏi ở đây là ngụy biện đối với mã đã cho.

Xem xét:

  1. do_something (), init_stuff () và ready_stuff () xuất hiện để biết liệu chúng có bị lỗi hay không, vì chúng trả về false hoặc nil trong trường hợp đó.
  2. Trách nhiệm thiết lập trạng thái dường như là trách nhiệm của các hàm đó, vì không có trạng thái nào được thiết lập trực tiếp trong foo ().

Do đó: do_something (), init_stuff () và ready_stuff () nên tự dọn dẹp . Có một hàm cleanup_1 () riêng biệt để dọn dẹp sau do_something () phá vỡ triết lý đóng gói. Đó là thiết kế tồi.

Nếu họ đã tự dọn dẹp, thì foo () trở nên khá đơn giản.

Mặt khác. Nếu foo () thực sự tạo ra trạng thái riêng cần được chia nhỏ, thì goto sẽ phù hợp.


0

Đây là những gì tôi thích:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

Tuy nhiên, cuộc thảo luận cũ .... còn việc sử dụng "arrow anti pattern" và đóng gói sau mỗi cấp lồng nhau trong một hàm nội tuyến tĩnh thì sao? Mã trông sạch sẽ, nó tối ưu (khi tối ưu hóa được bật) và không có goto nào được sử dụng. Nói tóm lại, chia để trị. Dưới đây là một ví dụ:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

Về không gian, chúng tôi đang tạo ba lần biến trong ngăn xếp, điều này không tốt, nhưng điều này sẽ biến mất khi biên dịch với -O2, xóa biến khỏi ngăn xếp và sử dụng một thanh ghi trong ví dụ đơn giản này. Những gì tôi nhận được từ khối bên trên gcc -S -O2 test.clà bên dưới:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

Tôi thích sử dụng kỹ thuật được mô tả trong ví dụ sau ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

nguồn: http://blog.staila.com/?p=114


2
mã cờ và mã chống mũi tên (cả hai đều được hiển thị trong ví dụ của bạn) đều là những thứ không cần thiết làm phức tạp mã. Không có bất kỳ lời biện minh nào ngoài "goto là ác" để sử dụng chúng.
KingRadical

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.