Có bao giờ thuận lợi khi sử dụng 'goto' trong ngôn ngữ hỗ trợ các vòng lặp và chức năng không? Nếu vậy, tại sao?


201

Tôi từ lâu đã có ấn tượng rằng gotokhông bao giờ nên được sử dụng nếu có thể. Trong khi đọc libavcodec (được viết bằng C) vào ngày khác, tôi nhận thấy nhiều cách sử dụng nó. Có bao giờ thuận lợi để sử dụng gototrong một ngôn ngữ hỗ trợ các vòng lặp và chức năng? Nếu vậy, tại sao?

Câu trả lời:


242

Có một vài lý do để sử dụng câu lệnh "goto" mà tôi biết (một số người đã nói về điều này rồi):

Thoát khỏi chức năng

Thông thường trong một chức năng, bạn có thể phân bổ tài nguyên và cần thoát ra ở nhiều nơi. Các lập trình viên có thể đơn giản hóa mã của họ bằng cách đặt mã dọn dẹp tài nguyên ở cuối hàm và tất cả "điểm thoát" của hàm sẽ lấy nhãn làm sạch. Bằng cách này, bạn không phải viết mã dọn dẹp tại mọi "điểm thoát" của chức năng.

Thoát khỏi các vòng lặp lồng nhau

Nếu bạn đang ở trong một vòng lặp lồng nhau và cần phải thoát ra khỏi tất cả các vòng lặp, một goto có thể làm cho việc này đơn giản và đơn giản hơn nhiều so với các câu lệnh và kiểm tra if.

Cải thiện hiệu suất cấp thấp

Điều này chỉ hợp lệ trong mã quan trọng hoàn hảo, nhưng các câu lệnh goto thực thi rất nhanh và có thể giúp bạn tăng tốc khi di chuyển qua một hàm. Tuy nhiên, đây là con dao hai lưỡi, bởi vì trình biên dịch thường không thể tối ưu hóa mã có chứa gotos.

Lưu ý rằng trong tất cả các ví dụ này, gotos bị giới hạn trong phạm vi của một chức năng.


15
Cách đúng để thoát các vòng lặp lồng nhau là cấu trúc lại vòng lặp bên trong thành một phương thức riêng biệt.
jason

129
@Jason - Bah. Đó là một tải bò. Thay thế gotobằng returnchỉ là ngớ ngẩn. Nó không "tái cấu trúc" bất cứ thứ gì, nó chỉ là "đổi tên" để những người lớn lên trong một gotomôi trường bị áp bức (tức là tất cả chúng ta) cảm thấy tốt hơn khi sử dụng số tiền về mặt đạo đức cho a goto. Tôi rất thích nhìn thấy vòng lặp nơi tôi sử dụng nó và nhìn thấy một chút goto, mà bản thân nó chỉ là một công cụ , hơn là thấy ai đó đã di chuyển vòng lặp ở đâu đó không liên quan chỉ để tránh a goto.
Chris Lutz

18
Có những vị trí mà gotos rất quan trọng: ví dụ, một môi trường C ++ ngoại lệ. Trong nguồn Silverlight, chúng tôi có hàng chục nghìn (hoặc nhiều hơn) các câu lệnh goto cho chức năng an toàn tồn tại thông qua việc sử dụng macro - các codec và thư viện phương tiện chính thường hoạt động thông qua các giá trị trả về và không bao giờ có ngoại lệ, và rất khó để kết hợp các cơ chế xử lý lỗi này trong một cách biểu diễn duy nhất.
Jeff Wilcox

79
Nó đáng chú ý là tất cả break, continue, returnvề cơ bản goto, chỉ trong bao bì đẹp.
el.pescado

16
Tôi không thấy làm thế nào do{....}while(0)được coi là một ý tưởng tốt hơn goto, ngoại trừ thực tế nó hoạt động trong Java.
Danh sách Jeremy

906

Mọi người chống lại goto, trực tiếp hoặc gián tiếp, bài viết hại được coi là có hại của Edsger Dijkstra để chứng minh vị trí của họ. Quá tệ bài viết của Dijkstra hầu như không liên quan gì đến cách gotosử dụng các câu lệnh ngày nay và do đó những gì bài báo nói có rất ít khả năng áp dụng cho bối cảnh lập trình hiện đại. Cácgoto meme không tập trung vào một tôn giáo, ngay đến kinh sách của nó được chỉ định từ trên cao, các linh mục cao cấp của nó và sự xấu hổ (hoặc tệ hơn) của những kẻ dị giáo nhận thức.

Hãy đặt bài viết của Dijkstra vào bối cảnh để làm sáng tỏ một chút về chủ đề này.

Khi Dijkstra viết bài báo của mình, các ngôn ngữ phổ biến thời bấy giờ là những ngôn ngữ thủ tục không có cấu trúc như BASIC, FORTRAN (các phương ngữ trước đó) và các ngôn ngữ lắp ráp khác nhau. Điều này khá phổ biến đối với những người sử dụng các ngôn ngữ cấp cao hơn để nhảy khắp cơ sở mã của họ trong các luồng thực thi bị xoắn, méo mó dẫn đến thuật ngữ "mã spaghetti". Bạn có thể thấy điều này bằng cách nhảy vào trò chơi Trek kinh điển được viết bởi Mike Mayfield và cố gắng tìm ra cách mọi thứ hoạt động. Hãy dành một vài phút để xem qua.

ĐÂY là "việc sử dụng không bị hạn chế trong tuyên bố" mà Dijkstra đã chống lại trong bài báo của mình vào năm 1968. ĐÂY là môi trường mà anh ta sống trong đó đã khiến anh ta viết bài báo đó. Khả năng nhảy bất cứ nơi nào bạn thích trong mã của bạn tại bất kỳ điểm nào bạn thích là những gì anh ta đang chỉ trích và yêu cầu dừng lại. So sánh điều đó với các năng lực thiếu máu của gotoC hoặc các ngôn ngữ hiện đại hơn như vậy chỉ đơn giản là có khả năng.

Tôi đã có thể nghe thấy tiếng hô vang của những người sùng bái khi họ đối mặt với những kẻ dị giáo. "Nhưng," họ sẽ hô vang, "bạn có thể tạo mã rất khó đọc bằng gotoC." Ồ vâng? Bạn có thể làm cho mã rất khó đọc mà không có goto. Giống như cái này:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Không phải gototrong tầm nhìn, vì vậy nó phải dễ đọc, phải không? Thế còn cái này thì như thế nào:

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

Không gotocó ở đó. Do đó nó phải được đọc.

Quan điểm của tôi với những ví dụ này là gì? Đó không phải là các tính năng ngôn ngữ làm cho mã không thể đọc được, không thể nhầm lẫn. Đó không phải là cú pháp làm điều đó. Đó là những lập trình viên tồi gây ra điều này. Và các lập trình viên xấu, như bạn có thể thấy trong mục trên, có thể làm cho bất kỳ tính năng ngôn ngữ nào không thể đọc được và không thể sử dụng được. Giống như các forvòng lặp ở đó. (Bạn có thể thấy chúng, phải không?)

Bây giờ để công bằng, một số cấu trúc ngôn ngữ dễ lạm dụng hơn những ngôn ngữ khác. Tuy nhiên, nếu bạn là một lập trình viên C, tôi sẽ xem xét kỹ hơn khoảng 50% việc sử dụng từ #definelâu trước khi tôi tham gia một cuộc thập tự chinh chống lại goto!

Vì vậy, đối với những người đã bận tâm đọc đến nay, có một số điểm chính cần lưu ý.

  1. Giấy Dijkstra trên gotobáo cáo được viết cho một môi trường lập trình ở đâu gotođược một rất nhiều tiềm năng hơn gây tổn hại hơn là trong hầu hết các ngôn ngữ hiện đại mà không phải là một nhà lắp ráp.
  2. Tự động vứt bỏ tất cả các mục đích sử dụng gotovì điều này là hợp lý như nói rằng "Tôi đã cố gắng vui vẻ một lần nhưng không thích nó vì vậy bây giờ tôi chống lại nó".
  3. Có những cách sử dụng hợp pháp của các câu lệnh hiện đại (thiếu máu) gototrong mã không thể được thay thế đầy đủ bằng các cấu trúc khác.
  4. Tất nhiên, có những sử dụng bất hợp pháp của cùng một tuyên bố.
  5. Cũng có những cách sử dụng bất hợp pháp các tuyên bố kiểm soát hiện đại như godo"gớm ghiếc" trong đó một dovòng lặp luôn luôn sai được sử dụng breakthay cho a goto. Đây thường là tồi tệ hơn sử dụng hợp lý goto.

42
@pocjoc: Viết tối ưu hóa đệ quy đuôi trong C chẳng hạn. Điều này xuất hiện trong việc thực hiện các phiên dịch / thời gian chạy ngôn ngữ chức năng, nhưng là đại diện của một loại vấn đề thú vị. Hầu hết các trình biên dịch được viết bằng C sử dụng goto vì lý do này. Dĩ nhiên, việc sử dụng nó không xảy ra trong hầu hết các tình huống, nhưng trong các tình huống, nó được gọi là rất có ý nghĩa để sử dụng.
zxq9

66
Tại sao mọi người lại nói về bài báo "Đi đến bị coi là có hại" của Dijkstra mà không đề cập đến câu trả lời của Knuth cho nó, "Lập trình có cấu trúc đi đến câu lệnh"?
Oblomov

22
@ Nietzche-jou "đây là một người đàn ông rơm khá trắng trợn [...]" Anh ta mỉa mai sử dụng một sự phân đôi giả để cho thấy tại sao sự kỳ thị là phi logic. Một Strawman là một ngụy biện logic, trong đó bạn cố tình xuyên tạc vị trí của đối thủ để làm cho có vẻ như vị trí của họ dễ dàng bị đánh bại. Vd: "Người vô thần chỉ là người vô thần vì họ ghét Chúa". Một sự phân đôi giả là khi bạn cho rằng cực trị ngược lại là đúng khi có ít nhất một khả năng bổ sung tồn tại (tức là đen và trắng). Ví dụ: "Chúa ghét những người đồng tính luyến ái, do đó, anh ấy yêu những người dị tính."
Braden hay nhất

27
@JLRishe Bạn đang đọc quá sâu vào một thứ thậm chí không có ở đó. Nó chỉ là một ngụy biện logic thực sự nếu người sử dụng nó thành thật tin rằng nó có ý nghĩa logic. Nhưng anh ta nói điều đó một cách mỉa mai, rõ ràng cho thấy rằng anh ta biết điều đó thật lố bịch, và đây là cách anh ta thể hiện nó. Anh ta không "dùng nó để biện minh cho quan điểm của mình"; anh ta đang sử dụng nó để cho thấy tại sao thật nực cười khi nói rằng gotos tự động biến mã thành spaghetti. Tức là bạn vẫn có thể viết mã shitty mà không cần gotos. ĐÓ là quan điểm của anh ấy. Và sự phân đôi giả dối mỉa mai là phương pháp của ông để minh họa nó một cách hài hước.
Braden hay nhất

32
-1 vì đã đưa ra một lập luận rất lớn tại sao nhận xét của Dijkstra không được áp dụng, trong khi quên cho thấy những lợi thế gotothực sự là gì (đó là câu hỏi được đăng)
Maarten Bodewes

154

Tuân thủ các thực hành tốt nhất một cách mù quáng không phải là một thực hành tốt nhất. Ý tưởng tránh các gotocâu lệnh là hình thức kiểm soát dòng chính của một người là để tránh tạo ra mã spaghetti không thể đọc được. Nếu được sử dụng một cách tiết kiệm ở đúng nơi, đôi khi chúng có thể là cách đơn giản nhất, rõ ràng nhất để thể hiện một ý tưởng. Walter Bright, người tạo ra trình biên dịch Zortech C ++ và ngôn ngữ lập trình D, sử dụng chúng thường xuyên, nhưng thận trọng. Ngay cả vớigoto tuyên bố, mã của anh ấy vẫn hoàn toàn có thể đọc được.

Điểm mấu chốt: Tránh gotođể tránh gotolà vô nghĩa. Những gì bạn thực sự muốn tránh là sản xuất mã không thể đọc được. Nếu gotomã -laden của bạn có thể đọc được, thì không có gì sai với nó.


36

gotolàm cho lý do về chương trình chảy khó 1 (hay còn gọi là mã spaghetti của Spon),goto thường chỉ được sử dụng để bù cho các tính năng bị thiếu: Việc sử dụng gotocó thể thực sự được chấp nhận, nhưng chỉ khi ngôn ngữ không cung cấp biến thể có cấu trúc chặt chẽ hơn để có được cùng một mục tiêu. Lấy ví dụ về nghi ngờ:

Quy tắc với goto mà chúng tôi sử dụng là goto vẫn ổn khi nhảy về phía trước một điểm dọn dẹp duy nhất trong một chức năng.

Điều này đúng - nhưng chỉ khi ngôn ngữ không cho phép xử lý ngoại lệ có cấu trúc với mã dọn dẹp (chẳng hạn như RAII hoặc finally ), hoạt động tương tự tốt hơn (vì nó được xây dựng đặc biệt để thực hiện) hoặc khi có lý do chính đáng sử dụng xử lý ngoại lệ có cấu trúc (nhưng bạn sẽ không bao giờ gặp trường hợp này ngoại trừ ở mức rất thấp).

Trong hầu hết các ngôn ngữ khác, việc sử dụng duy nhất được chấp nhận gotolà thoát khỏi các vòng lặp lồng nhau. Và thậm chí ở đó, hầu như luôn luôn tốt hơn để nâng vòng lặp bên ngoài thành một phương thức riêng và sử dụngreturn thay thế.

Ngoài ra, đó gotolà một dấu hiệu cho thấy không đủ suy nghĩ đã đi vào đoạn mã cụ thể.


1 ngôn ngữ hiện đại hỗ trợ gotothực hiện một số hạn chế (ví dụ:goto có thể không nhảy vào hoặc ra khỏi chức năng) nhưng về cơ bản vấn đề vẫn như cũ.

Ngẫu nhiên, điều tương tự tất nhiên cũng đúng với các tính năng ngôn ngữ khác, đáng chú ý nhất là ngoại lệ. Và thường có các quy tắc nghiêm ngặt tại chỗ chỉ sử dụng các tính năng này khi được chỉ định, chẳng hạn như quy tắc không sử dụng ngoại lệ để kiểm soát luồng chương trình không đặc biệt.


4
Chỉ tò mò ở đây, nhưng những gì về trường hợp sử dụng gotos để dọn mã. Theo tôi, bằng cách dọn dẹp, không chỉ giải quyết bộ nhớ, mà còn nói rằng ghi nhật ký lỗi. Tôi đã đọc qua một loạt các bài đăng, và rõ ràng, không ai viết mã in nhật ký .. hmm?!
Shiva

37
"Về cơ bản, do bản chất khiếm khuyết của goto (và tôi tin rằng điều này không gây tranh cãi)" -1 và tôi đã dừng đọc ở đó.
o0 '.

14
Lời khuyên chống lại goto xuất phát từ thời đại lập trình có cấu trúc, nơi mà việc trở về sớm cũng bị coi là xấu xa. Di chuyển các vòng lặp lồng nhau vào một hàm trao đổi một "cái ác" cho cái khác và tạo ra một hàm không có lý do thực sự để tồn tại (bên ngoài "nó cho phép tôi tránh sử dụng goto!"). Đúng là goto là công cụ mạnh nhất để sản xuất mã spaghetti nếu được sử dụng mà không bị ràng buộc, nhưng đây là một ứng dụng hoàn hảo cho nó; nó đơn giản hóa mã. Knuth lập luận cho việc sử dụng này và Dijkstra nói "Đừng rơi vào cái bẫy tin rằng tôi cực kỳ giáo điều về goto".
Bùn

5
finally? Vì vậy, sử dụng ngoại lệ cho những thứ khác ngoài xử lý lỗi là tốt nhưng sử dụng gotolà xấu? Tôi nghĩ rằng các ngoại lệ được đặt tên khá thông minh.
Christian

3
@Mud: trong các trường hợp mà tôi gặp phải (hàng ngày), tạo ra một chức năng "không có lý do thực sự để tồn tại" giải pháp tốt nhất. Lý do: nó loại bỏ chi tiết khỏi chức năng cấp cao nhất, sao cho kết quả có luồng điều khiển dễ đọc. Cá nhân tôi không gặp phải tình huống trong đó một goto tạo ra mã dễ đọc nhất. Mặt khác, tôi sử dụng nhiều trả về, trong các hàm đơn giản, trong đó người khác có thể thích lấy một điểm thoát duy nhất. Tôi rất thích xem các ví dụ ngược trong đó goto dễ đọc hơn là tái cấu trúc thành các hàm được đặt tên tốt.
ToolmakerSteve

35

Chà, có một thứ luôn tệ hơn goto's ; sử dụng lạ các toán tử dòng chương trình khác để tránh một goto:

Ví dụ:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

2
do{}while(false)Tôi nghĩ có thể được coi là thành ngữ. Bạn không được phép không đồng ý: D
Thomas Eding

37
@trinithis: Nếu đó là "thành ngữ", đó chỉ là do sự sùng bái chống goto. Nếu bạn nhìn kỹ vào nó, bạn sẽ nhận ra đó chỉ là một cách nói goto after_do_block;mà không thực sự nói điều đó. Nếu không ... một "vòng lặp" chạy chính xác một lần? Tôi gọi đó là lạm dụng các cấu trúc kiểm soát.
cHao

5
@ThomasEding Eding Có một ngoại lệ đối với quan điểm của bạn. Nếu bạn đã từng thực hiện một số lập trình C / C ++ và phải sử dụng #define, bạn sẽ biết, đó là {} trong khi (0) là một trong những tiêu chuẩn để đóng gói nhiều dòng mã. Ví dụ: #define do {memcpy (a, b, 1); một cái gì đó ++;} while (0) an toàn hơn và tốt hơn #define memcpy (a, b, 1); một cái gì đó ++
Ignas2526

10
@ Ignas2526 Bạn đã chỉ ra rất độc đáo về việc #definerất nhiều, tệ hơn nhiều lần so với việc sử dụng gotomột lần trong một thời gian: D
Luaan

3
+1 cho một danh sách tốt những cách mà mọi người cố gắng tránh gotos, đến mức cực đoan; Tôi đã thấy đề cập đến các kỹ thuật như vậy ở nơi khác, nhưng đây là một danh sách tuyệt vời, cô đọng. Suy ngẫm tại sao, trong 15 năm qua, nhóm của tôi chưa bao giờ cần bất kỳ kỹ thuật nào trong số đó, cũng không sử dụng gotos. Ngay cả trong một ứng dụng máy khách / máy chủ với hàng trăm ngàn dòng mã, bởi nhiều lập trình viên. C #, java, PHP, python, javascript. Mã máy khách, máy chủ và ứng dụng. Không khoe khoang, cũng không thịnh vượng cho một vị trí, chỉ thực sự tò mò tại sao một số người gặp phải tình huống cầu xin gotos là giải pháp rõ ràng nhất, và những người khác thì không ...
ToolmakerSteve

28

Trong câu lệnh chuyển đổi C # không cho phép rơi qua . Vì vậy, goto được sử dụng để chuyển điều khiển sang nhãn trường hợp chuyển đổi cụ thể hoặc mặc định nhãn .

Ví dụ:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

Chỉnh sửa: Có một ngoại lệ đối với quy tắc "không rơi". Fall-through được cho phép nếu một tuyên bố trường hợp không có mã.


3
Chuyển đổi dự phòng được hỗ trợ trong .NET 2.0 - msdn.microsoft.com/en-us/l
Library / 06tc147t (VS.80) .aspx

9
Chỉ khi trường hợp không có cơ thể mã. Nếu nó có mã thì bạn phải sử dụng từ khóa goto.
Matthew Whited

26
Câu trả lời này rất buồn cười - C # đã loại bỏ thông qua vì nhiều người cho rằng nó có hại và ví dụ này sử dụng goto (cũng được xem là có hại bởi nhiều người) để làm sống lại hành vi ban đầu, được cho là có hại, NHƯNG kết quả tổng thể thực sự ít gây hại hơn ( bởi vì mã làm cho nó rõ ràng rằng sự sụp đổ là có chủ ý!).
thomasrutter

9
Chỉ vì một từ khóa được viết bằng các chữ cái GOTO không làm cho nó trở thành một goto. Trường hợp trình bày này không phải là một goto. Đây là một câu lệnh chuyển đổi xây dựng cho dự phòng. Sau đó, một lần nữa, tôi không thực sự biết C # rất tốt, vì vậy tôi có thể sai.
Thomas Eding

1
Chà, trong trường hợp này, nó hơi nhiều hơn một chút (vì bạn có thể nói goto case 5:khi bạn ở trong trường hợp 1). Có vẻ như câu trả lời của Konrad Rudolph là chính xác ở đây: gotođang bù cho một tính năng bị thiếu (và không rõ ràng hơn tính năng thực sự). Nếu những gì chúng ta thực sự muốn là thông qua, có lẽ mặc định tốt nhất sẽ không phải là thông qua, nhưng một cái gì đó muốn continueyêu cầu nó một cách rõ ràng.
David Stone

14

#ifdef TONGUE_IN_CHEEK

Perl có một gotocho phép bạn thực hiện các cuộc gọi đuôi của người nghèo. :-P

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Được rồi, vì vậy mà không có gì để làm với C goto. Nghiêm trọng hơn, tôi đồng ý với các ý kiến ​​khác về việc sử dụng gotođể dọn dẹp hoặc triển khai thiết bị của Duff hoặc tương tự. Đó là tất cả về việc sử dụng, không lạm dụng.

(Nhận xét tương tự có thể áp dụng cho longjmp, ngoại lệ call/ccvà tương tự --- chúng có những cách sử dụng hợp pháp, nhưng có thể dễ dàng bị lạm dụng. Ví dụ, ném một ngoại lệ hoàn toàn để thoát khỏi cấu trúc kiểm soát được lồng sâu, trong những trường hợp hoàn toàn không đặc biệt .)


Tôi nghĩ đây là lý do duy nhất để sử dụng goto trong Perl.
Brad Gilbert

12

Tôi đã viết hơn một vài dòng ngôn ngữ lắp ráp trong những năm qua. Cuối cùng, mọi ngôn ngữ cấp cao biên dịch thành gotos. Được rồi, gọi chúng là "cành" hoặc "nhảy" hoặc bất cứ thứ gì khác, nhưng chúng là hình ảnh. Bất cứ ai cũng có thể viết trình biên dịch goto-less?

Bây giờ chắc chắn, bạn có thể chỉ ra cho một lập trình viên Fortran, C hoặc BASIC rằng chạy bạo loạn với gotos là một công thức cho món bolognaise spaghetti. Tuy nhiên, câu trả lời không phải là tránh chúng, mà là sử dụng chúng cẩn thận.

Một con dao có thể được sử dụng để chuẩn bị thức ăn, giải thoát ai đó hoặc giết chết ai đó. Chúng ta có làm mà không có dao thông qua sợ sau này? Tương tự như goto: sử dụng bất cẩn nó cản trở, sử dụng cẩn thận nó giúp.


1
Có lẽ bạn muốn đọc lý do tại sao tôi tin rằng điều này về cơ bản là sai tại stackoverflow.com/questions/46586/ mẹo
Konrad Rudolph

15
Bất cứ ai tiến bộ "tất cả đều biên dịch thành JMP!" đối số về cơ bản không hiểu điểm đằng sau lập trình bằng ngôn ngữ cấp cao hơn.
Nietzche-jou

7
Tất cả những gì bạn thực sự cần là trừ và chi nhánh. Mọi thứ khác là để thuận tiện hoặc hiệu suất.
David Stone

8

Hãy xem khi nào nên sử dụng Goto khi lập trình trong C :

Mặc dù việc sử dụng goto hầu như luôn luôn là thực hành lập trình tồi (chắc chắn bạn có thể tìm thấy cách làm XYZ tốt hơn), nhưng có những lúc nó thực sự không phải là một lựa chọn tồi. Một số thậm chí có thể lập luận rằng, khi nó hữu ích, đó là sự lựa chọn tốt nhất.

Hầu hết những gì tôi phải nói về goto thực sự chỉ áp dụng cho C. Nếu bạn đang sử dụng C ++, không có lý do chính đáng nào để sử dụng goto thay cho ngoại lệ. Tuy nhiên, trong C, bạn không có sức mạnh của cơ chế xử lý ngoại lệ, vì vậy nếu bạn muốn tách riêng việc xử lý lỗi khỏi phần còn lại của logic chương trình và bạn muốn tránh viết lại mã nhiều lần trong toàn bộ mã của mình, sau đó goto có thể là một lựa chọn tốt.

Ý tôi là sao Bạn có thể có một số mã trông như thế này:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

Điều này là tốt cho đến khi bạn nhận ra rằng bạn cần thay đổi mã dọn dẹp của bạn. Sau đó, bạn phải trải qua và thực hiện 4 thay đổi. Bây giờ, bạn có thể quyết định rằng bạn chỉ có thể gói gọn tất cả việc dọn dẹp vào một chức năng duy nhất; Đó không phải là một ý tưởng tồi. Nhưng điều đó có nghĩa là bạn sẽ cần cẩn thận với các con trỏ - nếu bạn có kế hoạch giải phóng một con trỏ trong chức năng dọn dẹp của mình, không có cách nào để đặt nó sau đó trỏ đến NULL trừ khi bạn chuyển con trỏ đến một con trỏ. Trong nhiều trường hợp, dù sao bạn cũng sẽ không sử dụng con trỏ đó nữa, vì vậy đó có thể không phải là mối quan tâm chính. Mặt khác, nếu bạn thêm vào một con trỏ mới, xử lý tệp hoặc những thứ khác cần dọn dẹp, thì bạn sẽ cần phải thay đổi chức năng dọn dẹp của mình một lần nữa; và sau đó bạn sẽ cần thay đổi các đối số cho chức năng đó.

Bằng cách sử dụng goto, nó sẽ được

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

Lợi ích ở đây là mã theo sau của bạn có quyền truy cập vào mọi thứ cần thiết để thực hiện dọn dẹp và bạn đã quản lý để giảm số lượng điểm thay đổi đáng kể. Một lợi ích khác là bạn đã đi từ việc có nhiều điểm thoát cho chức năng của mình thành một điểm; không có cơ hội bạn sẽ vô tình quay trở lại từ chức năng mà không dọn dẹp.

Hơn nữa, vì goto chỉ được sử dụng để nhảy đến một điểm duy nhất, nên không phải là bạn đang tạo ra một khối mã spaghetti nhảy qua lại trong một nỗ lực để mô phỏng các cuộc gọi chức năng. Thay vào đó, goto thực sự giúp viết mã có cấu trúc hơn.


Trong một từ, gotonên luôn luôn được sử dụng một cách tiết kiệm, và như là phương sách cuối cùng - nhưng có một thời gian và một nơi dành cho nó. Câu hỏi không phải là "bạn có phải sử dụng nó không" mà là "nó có phải là lựa chọn tốt nhất" để sử dụng nó không.


7

Một trong những lý do goto là xấu, bên cạnh phong cách mã hóa là bạn có thể sử dụng nó để tạo các vòng lặp chồng chéo , nhưng không lồng nhau :

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

Điều này sẽ tạo ra cấu trúc kiểm soát luồng kỳ lạ, nhưng có thể hợp pháp trong đó một chuỗi như (a, b, c, b, a, b, a, b, ...) là có thể, khiến tin tặc biên dịch không hài lòng. Rõ ràng có một số thủ thuật tối ưu hóa thông minh dựa trên loại cấu trúc này không xảy ra. (Tôi nên kiểm tra bản sao của cuốn sách rồng ...) Kết quả của việc này có thể (sử dụng một số trình biên dịch) là các tối ưu hóa khác không được thực hiện cho mã có chứa gotos.

Nó có thể hữu ích nếu bạn biết điều đó, "ồ, nhân tiện", tình cờ thuyết phục trình biên dịch phát ra mã nhanh hơn. Cá nhân, tôi muốn thử giải thích với trình biên dịch về những gì có thể xảy ra và những gì không phải trước khi sử dụng một thủ thuật như goto, nhưng có thể nói, tôi cũng có thể thử gototrước khi hack trình biên dịch chương trình .


3
Chà ... đưa tôi trở lại thời kỳ lập trình FORTRAN trong một công ty thông tin tài chính. Năm 2007
Marcin

5
Bất kỳ cấu trúc ngôn ngữ nào cũng có thể bị lạm dụng theo những cách khiến nó không thể đọc được hoặc hoạt động kém.
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI

@ CHỈ: Điểm không phải là về khả năng đọc, hoặc hiệu suất kém, mà là các giả định và đảm bảo về biểu đồ kiểm soát dòng chảy. Bất kỳ lạm dụng goto sẽ được tăng hiệu suất (hoặc khả năng đọc).
Anders Eurenius

3
Tôi muốn tranh luận một trong những lý do gotohữu ích là nó cho phép bạn xây dựng các vòng như thế này, mà sẽ đòi hỏi một loạt các vặn vẹo logic khác. Tôi muốn tranh luận thêm rằng nếu trình tối ưu hóa không biết cách viết lại cái này thì tốt . Một vòng lặp như thế này không nên được thực hiện cho hiệu suất hoặc khả năng đọc, nhưng bởi vì đó chính xác là thứ tự mà mọi thứ cần phải xảy ra. Trong trường hợp đó, tôi sẽ không đặc biệt muốn trình tối ưu hóa xoay quanh nó.
cHao

1
... một bản dịch đơn giản của thuật toán cần thiết sang mã dựa trên GOTO có thể dễ dàng xác nhận hơn nhiều so với một mớ hỗn độn các biến cờ và các cấu trúc lặp bị lạm dụng.
supercat

7

Tôi thấy buồn cười khi một số người sẽ đưa ra một danh sách các trường hợp mà goto được chấp nhận, nói rằng tất cả các cách sử dụng khác là không thể chấp nhận được. Bạn có thực sự nghĩ rằng bạn biết mọi trường hợp trong đó goto là lựa chọn tốt nhất để thể hiện một thuật toán?

Để minh họa, tôi sẽ cho bạn một ví dụ mà chưa có ai ở đây thể hiện:

Hôm nay tôi đã viết mã để chèn một phần tử trong bảng băm. Bảng băm là một bộ đệm của các tính toán trước đó có thể được ghi đè theo ý muốn (ảnh hưởng đến hiệu suất nhưng không chính xác).

Mỗi nhóm của bảng băm có 4 vị trí và tôi có một loạt các tiêu chí để quyết định phần tử nào sẽ ghi đè khi một nhóm đầy. Ngay bây giờ điều này có nghĩa là thực hiện tối đa ba lần đi qua một cái xô, như thế này:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

Bây giờ nếu tôi không sử dụng goto, mã này sẽ trông như thế nào?

Một cái gì đó như thế này:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

Nó sẽ trông tệ hơn và tệ hơn nếu thêm nhiều lượt đi, trong khi phiên bản với goto luôn giữ cùng mức thụt đầu dòng và tránh sử dụng giả nếu các câu lệnh có kết quả được ngụ ý bởi việc thực hiện vòng lặp trước.

Vì vậy, có một trường hợp khác mà goto làm cho mã sạch hơn và dễ viết và dễ hiểu hơn ... Tôi chắc chắn còn nhiều điều nữa, vì vậy đừng giả vờ biết tất cả các trường hợp mà goto hữu ích, loại bỏ bất kỳ trường hợp tốt nào bạn có thể Tôi không nghĩ đến.


1
Trong ví dụ bạn đã đưa ra, tôi muốn cấu trúc lại khá đáng kể. Nói chung, tôi cố gắng tránh các bình luận một lớp nói rằng đoạn mã tiếp theo đang làm gì. Thay vào đó, tôi chia nó thành chức năng riêng có tên tương tự như nhận xét. Nếu bạn thực hiện một phép chuyển đổi như vậy, thì hàm này sẽ cung cấp một cái nhìn tổng quan ở mức độ cao về chức năng đang làm gì và mỗi hàm mới sẽ cho biết cách bạn thực hiện từng bước. Tôi cảm thấy rằng điều quan trọng hơn nhiều so với bất kỳ sự phản đối nào gotolà có mỗi chức năng ở cùng một mức độ trừu tượng. Điều đó tránh được gotolà một phần thưởng.
David Stone

2
Bạn chưa thực sự giải thích làm thế nào việc thêm nhiều chức năng thoát khỏi goto, nhiều mức thụt đầu dòng và giả mạo nếu các câu lệnh ...
Ricardo

Nó sẽ trông giống như thế này, sử dụng ký hiệu container tiêu chuẩn: container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();Tôi thấy rằng loại lập trình đó dễ đọc hơn nhiều. Mỗi hàm trong trường hợp này có thể được viết dưới dạng một hàm thuần túy, dễ hiểu hơn nhiều. Hàm chính bây giờ giải thích những gì mã làm chỉ bằng tên của các hàm, và sau đó nếu bạn muốn, bạn có thể xem định nghĩa của chúng để tìm hiểu làm thế nào nó hoạt động.
David Stone

3
Thực tế là bất cứ ai cũng phải đưa ra các ví dụ, về việc các thuật toán nhất định nên sử dụng goto một cách tự nhiên - là một sự phản ánh đáng buồn về cách suy nghĩ thuật toán ít diễn ra ngày hôm nay !! Tất nhiên, ví dụ của @ Ricardo là (một trong nhiều) ví dụ hoàn hảo về nơi goto thanh lịch và rõ ràng.
Fattie

6

Quy tắc với goto mà chúng tôi sử dụng là goto vẫn ổn khi nhảy về phía trước một điểm dọn dẹp duy nhất trong một chức năng. Trong các chức năng thực sự phức tạp, chúng tôi thư giãn quy tắc đó để cho phép các bước nhảy khác về phía trước. Trong cả hai trường hợp, chúng tôi đều tránh lồng nhau sâu nếu các câu lệnh thường xảy ra khi kiểm tra mã lỗi, giúp dễ đọc và duy trì.


1
Tôi có thể thấy một cái gì đó hữu ích trong một ngôn ngữ như C. Tuy nhiên, khi bạn có sức mạnh của các hàm tạo / hàm hủy C ++, điều đó thường không hữu ích lắm.
David Stone

1
"Trong các hàm thực sự phức tạp, chúng tôi nới lỏng quy tắc đó để cho phép các bước nhảy khác tiến lên" Không có ví dụ, nghe có vẻ như, nếu bạn đang thực hiện các hàm phức tạp thậm chí còn phức tạp hơn bằng cách sử dụng các bước nhảy. Sẽ không phải là cách tiếp cận "tốt hơn" để tái cấu trúc và phân chia các chức năng phức tạp đó?
MikeMB

1
Đây là mục đích cuối cùng mà tôi đã sử dụng goto. Tôi không bỏ lỡ goto trong Java, vì nó đã thử - cuối cùng cũng có thể làm công việc tương tự.
Patricia Shanahan

5

Cuộc thảo luận chu đáo và kỹ lưỡng nhất về các tuyên bố goto, sử dụng hợp pháp của chúng và các cấu trúc thay thế có thể được sử dụng thay cho "các tuyên bố goto có đạo đức" nhưng có thể bị lạm dụng dễ dàng như các tuyên bố goto, là bài viết của Donald Knuth " Lập trình có cấu trúc với các tuyên bố goto " , trong Khảo sát tính toán tháng 12 năm 1974 (tập 6, số 4. trang 261 - 301).

Không có gì đáng ngạc nhiên, một số khía cạnh của bài báo 39 tuổi này đã được đề xuất: Sức mạnh xử lý của đơn đặt hàng làm cho một số cải tiến hiệu suất của Knuth không được chú ý đối với các vấn đề có kích thước vừa phải và các cấu trúc ngôn ngữ lập trình mới đã được phát minh kể từ đó. .


3

Trong mô-đun Perl, đôi khi bạn muốn tạo chương trình con hoặc đóng khi đang di chuyển. Vấn đề là, một khi bạn đã tạo ra chương trình con, làm thế nào để bạn có được nó. Bạn chỉ có thể gọi nó, nhưng sau đó nếu chương trình con sử dụng caller()nó sẽ không hữu ích như nó có thể. Đó là nơi goto &subroutinebiến thể có thể hữu ích.

Đây là một ví dụ nhanh:

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

Bạn cũng có thể sử dụng hình thức này gotođể cung cấp một hình thức tối ưu hóa cuộc gọi đuôi thô sơ.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

(Trong Perl 5 phiên bản 16 sẽ được viết tốt hơn goto __SUB__;)

Có một mô-đun sẽ nhập một công cụ tailsửa đổi và một mô-đun sẽ nhập recurnếu bạn không thích sử dụng hình thức này goto.

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

Hầu hết các lý do khác để sử dụng gotođược thực hiện tốt hơn với các từ khóa khác.

Giống như redoing một chút mã:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

Hoặc đi đến lastmột chút mã từ nhiều nơi:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

2

Nếu vậy, tại sao?

C không có ngắt đa cấp / có nhãn và không phải tất cả các luồng điều khiển đều có thể được mô hình hóa dễ dàng với các nguyên hàm lặp và quyết định của C. gotos đi một chặng đường dài để khắc phục những sai sót.

Đôi khi, rõ ràng hơn là sử dụng một biến cờ nào đó để tạo ra một loại phá vỡ đa cấp giả, nhưng nó không phải lúc nào cũng vượt trội hơn goto (ít nhất là một goto cho phép người ta dễ dàng xác định nơi điều khiển đi đến, không giống như biến cờ ) và đôi khi bạn chỉ đơn giản là không muốn trả giá hiệu suất của cờ / các hình thức khác để tránh goto.

libavcodec là một đoạn mã nhạy cảm về hiệu năng. Biểu hiện trực tiếp của luồng điều khiển có lẽ là ưu tiên, bởi vì nó sẽ có xu hướng chạy tốt hơn.


2

Cũng như không ai từng thực hiện câu lệnh "COME TỪ" ....


8
Hoặc trong C ++, C #, Java, JS, Python, Ruby .... v.v ... Chỉ có họ gọi nó là "ngoại lệ".
cHao

2

Tôi thấy việc sử dụng do {} while (false) hoàn toàn nổi loạn. Có thể hiểu được có thể thuyết phục tôi rằng nó là cần thiết trong một số trường hợp kỳ lạ, nhưng không bao giờ rằng đó là mã hợp lý sạch.

Nếu bạn phải thực hiện một số vòng lặp như vậy, tại sao không làm cho sự phụ thuộc vào biến cờ rõ ràng?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

Nên không phải là /*empty*/được stepfailed = 1? Trong mọi trường hợp, làm thế nào là tốt hơn a do{}while(0)? Trong cả hai, bạn cần phải breakra khỏi nó (hoặc trong của bạn stepfailed = 1; continue;). Có vẻ không cần thiết với tôi.
Thomas Eding

2

1) Việc sử dụng goto phổ biến nhất mà tôi biết là mô phỏng xử lý ngoại lệ trong các ngôn ngữ không cung cấp nó, cụ thể là bằng C. (Mã được cung cấp bởi Nucle ở trên chỉ là vậy.) Hãy nhìn vào mã nguồn Linux và bạn ' sẽ thấy một bazillion gotos được sử dụng theo cách đó; có khoảng 100.000 gotos trong mã Linux theo một cuộc khảo sát nhanh được thực hiện vào năm 2013: http://blog.regehr.org/archives/894 . Việc sử dụng Goto thậm chí còn được đề cập trong hướng dẫn về phong cách mã hóa Linux: https://www.kernel.org/doc/Documentation/CodingStyle . Giống như lập trình hướng đối tượng được mô phỏng bằng cách sử dụng các cấu trúc được điền với các con trỏ hàm, goto có vị trí của nó trong lập trình C. Vậy ai đúng: Dijkstra hoặc Linus (và tất cả các lập trình viên nhân Linux)? Đó là lý thuyết so với thực hành về cơ bản.

Tuy nhiên, có một Gotcha thông thường vì không có hỗ trợ và kiểm tra mức trình biên dịch cho các cấu trúc / mẫu chung: dễ sử dụng sai và giới thiệu các lỗi mà không cần kiểm tra thời gian biên dịch. Windows và Visual C ++ nhưng ở chế độ C cung cấp xử lý ngoại lệ thông qua SEH / VEH vì lý do này: các ngoại lệ rất hữu ích ngay cả bên ngoài các ngôn ngữ OOP, tức là trong ngôn ngữ thủ tục. Nhưng trình biên dịch không thể luôn lưu thịt xông khói của bạn, ngay cả khi nó cung cấp hỗ trợ cú pháp cho các ngoại lệ trong ngôn ngữ. Hãy xem xét ví dụ về trường hợp sau, lỗi "goto fail" nổi tiếng của Apple SSL, lỗi này chỉ nhân đôi một goto với hậu quả tai hại ( https://www.imperialviolet.org/2014/02/22/applebug.html ):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

Bạn có thể có chính xác cùng một lỗi bằng cách sử dụng các ngoại lệ được hỗ trợ bởi trình biên dịch, ví dụ như trong C ++:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

Nhưng cả hai biến thể của lỗi đều có thể tránh được nếu trình biên dịch phân tích và cảnh báo bạn về mã không thể truy cập được. Ví dụ, biên dịch với Visual C ++ ở mức cảnh báo / W4 tìm thấy lỗi trong cả hai trường hợp. Ví dụ, Java cấm mã không thể truy cập (nơi có thể tìm thấy mã!) Vì một lý do khá chính đáng: đó có thể là một lỗi trong mã trung bình của Joe. Miễn là cấu trúc goto không cho phép các mục tiêu mà trình biên dịch không thể dễ dàng tìm ra, như gotos cho các địa chỉ được tính toán (**), trình biên dịch không tìm thấy mã không thể truy cập trong hàm với gotos so với sử dụng Dijkstra mã được phê duyệt.

(**) Chú thích: Có thể sử dụng số dòng Gotos để tính toán trong một số phiên bản Cơ bản, ví dụ GOTO 10 * x trong đó x là một biến. Khá khó hiểu, trong Fortran "goto tính toán" đề cập đến một cấu trúc tương đương với câu lệnh chuyển đổi trong C. Tiêu chuẩn C không cho phép gotos tính toán trong ngôn ngữ, mà chỉ gotos cho nhãn được khai báo tĩnh / tổng hợp. Tuy nhiên, GNU C có một phần mở rộng để lấy địa chỉ của nhãn (toán tử đơn nguyên, tiền tố &&) và cũng cho phép một goto đến một biến có kiểu void *. Xem https://gcc.gnu.org/onlinesocs/gcc/Labels-as-Values.html để biết thêm về chủ đề phụ tối nghĩa này. Phần còn lại của bài đăng này không liên quan đến tính năng GNU C tối nghĩa đó.

Gotos tiêu chuẩn C (tức là không được tính toán) thường không phải là lý do tại sao mã không thể truy cập không thể được tìm thấy tại thời điểm biên dịch. Lý do thông thường là mã logic như sau. Được

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

Trình biên dịch cũng khó tìm được mã không thể truy cập trong bất kỳ cấu trúc nào trong 3 cấu trúc sau:

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(Xin lỗi vì phong cách mã hóa liên quan đến cú đúp của tôi, nhưng tôi đã cố gắng giữ các ví dụ nhỏ gọn nhất có thể.)

Visual C ++ / W4 (ngay cả với / Ox) không tìm thấy mã không thể truy cập trong bất kỳ mã nào trong số này và vì có thể bạn biết rằng vấn đề tìm mã không thể truy cập nói chung là không thể giải quyết được. (Nếu bạn không tin tôi về điều đó: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf )

Là một vấn đề liên quan, goto C có thể được sử dụng để mô phỏng các ngoại lệ chỉ bên trong cơ thể của một chức năng. Thư viện C tiêu chuẩn cung cấp một cặp hàm setjmp () và longjmp () để mô phỏng các ngoại lệ / ngoại lệ không cục bộ, nhưng chúng có một số nhược điểm nghiêm trọng so với những ngôn ngữ khác cung cấp. Bài viết Wikipedia http://en.wikipedia.org/wiki/setjmp.h giải thích khá rõ vấn đề sau này. Cặp chức năng này cũng hoạt động trên Windows ( http://msdn.microsoft.com/en-us/l Library / yz2ez4as.aspx), nhưng hầu như không ai sử dụng chúng ở đó vì SEH / VEH vượt trội. Ngay cả trên Unix, tôi nghĩ setjmp và longjmp rất hiếm khi được sử dụng.

2) Tôi nghĩ rằng việc sử dụng goto phổ biến thứ hai trong C là triển khai phá vỡ đa cấp hoặc tiếp tục đa cấp, đây cũng là một trường hợp sử dụng khá không gây tranh cãi. Hãy nhớ lại rằng Java không cho phép nhãn goto, nhưng cho phép phá vỡ nhãn hoặc tiếp tục nhãn. Theo http://www.oracle.com/technetwork/java/simple-142616.html , đây thực sự là trường hợp sử dụng phổ biến nhất của gotos trong C (90% họ nói), nhưng theo kinh nghiệm chủ quan của tôi, mã hệ thống có xu hướng để sử dụng gotos để xử lý lỗi thường xuyên hơn. Có lẽ trong mã khoa học hoặc nơi HĐH cung cấp xử lý ngoại lệ (Windows) thì lối thoát đa cấp là trường hợp sử dụng chi phối. Họ không thực sự đưa ra bất kỳ chi tiết nào về bối cảnh khảo sát của họ.

Chỉnh sửa để thêm: hóa ra hai mẫu sử dụng này được tìm thấy trong sách C của Kernighan và Ritchie, khoảng trang 60 (tùy thuộc vào phiên bản). Một điều lưu ý là cả hai trường hợp sử dụng chỉ liên quan đến gotos phía trước. Và hóa ra phiên bản MISRA C 2012 (không giống như phiên bản 2004) hiện cho phép gotos, miễn là chúng chỉ là phiên bản chuyển tiếp.


Đúng. "Con voi trong phòng" là hạt nhân Linux, vì lợi ích, như một ví dụ về cơ sở mã quan trọng thế giới - được tải bằng goto. Tất nhiên là thế rồi. Chắc chắn. "Chống goto-meme" chỉ là một sự tò mò từ nhiều thập kỷ trước. KHÓA HỌC, có một số điều trong lập trình (đáng chú ý là "thống kê", thực sự là "toàn cầu" và ví dụ "otherif") có thể bị lạm dụng bởi những người không chuyên nghiệp. Vì vậy, nếu bạn là em họ của bạn đang học chương trình 2, bạn nói với họ "oh đừng sử dụng toàn cầu" và "không bao giờ sử dụng otherif".
Fattie

Lỗi goto fail không liên quan gì đến goto. Vấn đề được gây ra bởi câu lệnh if không có dấu ngoặc sau đó. Chỉ cần bất kỳ bản sao kê nào được dán hai lần sẽ gây ra vấn đề. Tôi coi loại trần trụi đó nếu có hại hơn nhiều so với goto.
muusbolla

2

Một số người nói không có lý do cho goto trong C ++. Một số người nói rằng trong 99% trường hợp có những lựa chọn thay thế tốt hơn. Đây không phải là lý luận, chỉ là ấn tượng phi lý. Đây là một ví dụ chắc chắn trong đó goto dẫn đến một mã đẹp, một cái gì đó như vòng lặp do-while nâng cao:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

So sánh nó với mã goto-free:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

Tôi thấy những khác biệt này:

  • {}khối lồng nhau là cần thiết (mặc dù do {...} whiletrông quen thuộc hơn)
  • loopbiến phụ là cần thiết, được sử dụng ở bốn nơi
  • mất nhiều thời gian hơn để đọc và hiểu công việc với loop
  • những loopkhông giữ bất kỳ dữ liệu, nó chỉ kiểm soát dòng chảy của việc thực hiện, đó là ít hiểu hơn nhãn đơn giản

Có một ví dụ khác

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

Bây giờ chúng ta hãy thoát khỏi goto "ác":

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

Bạn thấy đó là cùng một kiểu sử dụng goto, nó có cấu trúc tốt và nó không chuyển tiếp goto như nhiều cách quảng bá như cách duy nhất được đề xuất. Chắc chắn bạn muốn tránh mã "thông minh" như thế này:

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

Vấn đề là goto có thể dễ dàng bị sử dụng sai, nhưng bản thân goto không đáng trách. Lưu ý rằng nhãn có phạm vi chức năng trong C ++, vì vậy nó không gây ô nhiễm phạm vi toàn cầu như trong lắp ráp thuần túy, trong đó các vòng lặp chồng chéo có vị trí của nó và rất phổ biến - như trong mã sau đây cho 8051, trong đó hiển thị 7sejection được kết nối với P1. Chương trình vòng lặp sét xung quanh:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

Có một lợi thế khác: goto có thể phục vụ như các vòng lặp, điều kiện và các luồng khác:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

Hoặc bạn có thể sử dụng goto tương đương với thụt lề, vì vậy bạn không cần bình luận nếu bạn chọn tên nhãn một cách khôn ngoan:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;

1

Trong Perl, sử dụng nhãn để "goto" từ một vòng lặp - sử dụng câu lệnh "cuối cùng", tương tự như ngắt.

Điều này cho phép kiểm soát tốt hơn các vòng lặp lồng nhau.

Nhãn goto truyền thống cũng được hỗ trợ, nhưng tôi không chắc có quá nhiều trường hợp trong đó đây là cách duy nhất để đạt được những gì bạn muốn - chương trình con và vòng lặp nên đủ cho hầu hết các trường hợp.


Tôi nghĩ rằng hình thức goto duy nhất mà bạn từng sử dụng trong Perl là goto &subroutine. Mà khởi động chương trình con với @_ hiện tại, trong khi thay thế chương trình con hiện tại trong ngăn xếp.
Brad Gilbert

1

Vấn đề với 'goto' và đối số quan trọng nhất của phong trào 'lập trình không có goto' là, nếu bạn sử dụng nó quá thường xuyên, mã của bạn, mặc dù nó có thể hoạt động chính xác, trở nên không thể đọc được, không thể hiểu được, không thể xem được, v.v. các trường hợp 'goto' dẫn đến mã spaghetti. Cá nhân, tôi không thể nghĩ ra bất kỳ lý do chính đáng nào về lý do tại sao tôi sẽ sử dụng 'goto'.


11
Nói "Tôi không thể nghĩ ra bất kỳ lý do nào" trong tranh luận của bạn, chính thức là en.wikipedia.org/wiki/Argument_from_ignorance , mặc dù tôi thích thuật ngữ "bằng chứng thiếu trí tưởng tượng".
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI

2
@ CHỈ CẦN HOẠT ĐỘNG CỦA TÔI: Đó chỉ là một ngụy biện logic nếu nó được sử dụng sau đại học. Từ quan điểm của một nhà thiết kế ngôn ngữ, nó có thể là một đối số hợp lệ để cân nhắc chi phí bao gồm một tính năng ( goto). Cách sử dụng của @ cschol tương tự nhau: Mặc dù có thể không thiết kế ngôn ngữ ngay bây giờ, nhưng về cơ bản, anh ấy đang đánh giá nỗ lực của người thiết kế.
Konrad Rudolph

1
@KonradRudolph: IMHO, có một ngôn ngữ cho phép gotongoại trừ trong bối cảnh nơi nó sẽ đưa các biến vào tồn tại là rẻ hơn so với việc cố gắng hỗ trợ mọi loại cấu trúc điều khiển mà ai đó có thể cần. Viết mã với gotocó thể không tốt bằng sử dụng một số cấu trúc khác, nhưng có thể viết mã như vậy gotosẽ giúp tránh "lỗ hổng trong tính biểu cảm" - các cấu trúc mà ngôn ngữ không có khả năng viết mã hiệu quả.
supercat

1
@supercat Tôi e rằng chúng ta đến từ các trường thiết kế ngôn ngữ khác nhau. Tôi phản đối các ngôn ngữ được thể hiện tối đa ở mức giá dễ hiểu (hoặc chính xác). Tôi muốn có một ngôn ngữ hạn chế hơn là một ngôn ngữ cho phép.
Konrad Rudolph

1
@thb Có tất nhiên tôi làm. Nó thường làm cho mã khó đọc hơn nhiều và lý do về. Hầu như mỗi khi ai đó đăng mã chứa gototrên trang web xem xét mã, loại bỏ gotorất nhiều đơn giản hóa logic của mã.
Konrad Rudolph

1

Tất nhiên, GOTO có thể được sử dụng, nhưng có một điều quan trọng hơn kiểu mã hoặc nếu mã có hoặc không đọc được mà bạn phải có khi sử dụng: mã bên trong có thể không mạnh như bạn nghĩ .

Chẳng hạn, hãy xem hai đoạn mã sau:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

Mã tương đương với GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

Điều đầu tiên chúng tôi nghĩ là kết quả của cả hai bit mã sẽ là "Giá trị của A: 0" (tất nhiên chúng tôi cho rằng một thực thi không có sự song song)

Điều đó không đúng: trong mẫu đầu tiên, A sẽ luôn là 0, nhưng trong mẫu thứ hai (với câu lệnh GOTO) A có thể không bằng 0. Tại sao?

Lý do là vì từ một điểm khác của chương trình tôi có thể chèn a GOTO FINALmà không kiểm soát giá trị của A.

Ví dụ này rất rõ ràng, nhưng khi các chương trình trở nên phức tạp hơn, khó khăn trong việc nhìn thấy những thứ đó tăng lên.

Tài liệu liên quan có thể được tìm thấy trong bài báo nổi tiếng từ ông Dijkstra "Một trường hợp chống lại tuyên bố GO TO"


6
Điều này thường xảy ra trong BASIC trường học cũ. Tuy nhiên, trong các biến thể hiện đại, bạn không được phép nhảy vào giữa một chức năng khác ... hoặc trong nhiều trường hợp, thậm chí qua quá trình khai báo một biến. Về cơ bản (chơi chữ không có ý định), các ngôn ngữ hiện đại phần lớn đã loại bỏ được các GOTO "không kiểm soát" mà Dijkstra đang nói đến ... và cách duy nhất để sử dụng nó khi anh ta đang tranh cãi, là phạm một số tội lỗi ghê tởm khác. :)
cHao

1

Tôi sử dụng goto trong trường hợp sau: khi cần quay trở lại từ chức năng ở những nơi khác nhau và trước khi trả lại một số việc chưa được khởi tạo cần phải được thực hiện:

phiên bản không phải là goto:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

phiên bản goto:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

Phiên bản thứ hai giúp dễ dàng hơn, khi bạn cần thay đổi một cái gì đó trong các câu lệnh thỏa thuận (mỗi câu được sử dụng một lần trong mã) và giảm cơ hội bỏ qua bất kỳ trong số chúng, khi thêm một nhánh mới. Di chuyển chúng trong một chức năng sẽ không giúp ích gì ở đây, bởi vì việc phân bổ có thể được thực hiện ở các "cấp độ" khác nhau.


3
Đây là lý do tại sao chúng tôi có finallycác khối trong C #
John Saunders

^ như @JohnSaunders nói. Đây là một ví dụ về "sử dụng goto vì ngôn ngữ thiếu cấu trúc điều khiển thích hợp". TUY NHIÊN nó là một mùi mã để yêu cầu NHIỀU điểm goto gần trở lại. Có một phong cách lập trình an toàn hơn (dễ dàng hơn để không làm hỏng) không yêu cầu gotos, hoạt động tốt ngay cả trong các ngôn ngữ mà không "cuối cùng": thiết kế các cuộc gọi "dọn dẹp" đó để chúng vô hại khi không làm sạch lên là bắt buộc Yếu tố tất cả mọi thứ trừ việc dọn dẹp thành một phương thức, trong đó sử dụng thiết kế nhiều trả về. Gọi phương thức đó, sau đó thực hiện các cuộc gọi dọn dẹp.
ToolmakerSteve

Lưu ý rằng cách tiếp cận tôi mô tả yêu cầu thêm một mức gọi phương thức (nhưng chỉ trong một ngôn ngữ thiếu finally). Để thay thế, sử dụng gotos, nhưng đến một điểm thoát chung , luôn luôn làm sạch tất cả . Nhưng mỗi phương pháp dọn dẹp có thể xử lý một giá trị là null hoặc đã sạch hoặc được bảo vệ bằng một thử nghiệm có điều kiện, do đó bỏ qua khi không phù hợp.
ToolmakerSteve

@ToolmakerSteve đây không phải là mùi mã; thực tế, nó là một mẫu cực kỳ phổ biến trong C và được coi là một trong những cách hợp lệ nhất để sử dụng goto. Bạn muốn tôi tạo 5 phương thức, mỗi phương thức có kiểm tra if không cần thiết của riêng chúng, chỉ để xử lý dọn dẹp khỏi chức năng này? Bây giờ bạn đã tạo mã và hiệu suất phình to. Hoặc bạn chỉ có thể sử dụng goto.
muusbolla

@muusbolla - 1) "tạo 5 phương thức" - Không. Tôi đang đề xuất một phương pháp duy nhất để dọn sạch mọi tài nguyên có giá trị khác không. HOẶC xem phương án thay thế của tôi, sử dụng gototất cả đi đến cùng một điểm thoát, có cùng logic (yêu cầu thêm nếu mỗi tài nguyên, như bạn nói). Nhưng đừng bận tâm, khi sử dụng Cbạn là đúng - bất kể lý do gì là mã trong C, gần như chắc chắn đó là một sự đánh đổi ủng hộ mã "trực tiếp" nhất. (Đề xuất của tôi xử lý các tình huống phức tạp trong đó bất kỳ tài nguyên cụ thể nào có thể được phân bổ hoặc không được phân bổ. Nhưng vâng, quá mức trong trường hợp này.)
ToolmakerSteve

0

Edsger Dijkstra, một nhà khoa học máy tính có những đóng góp lớn trong lĩnh vực này, cũng nổi tiếng vì chỉ trích việc sử dụng GoTo. Có một bài viết ngắn về lập luận của ông trên Wikipedia .


0

Nó có ích để xử lý chuỗi ký tự thông minh theo thời gian.

Hãy tưởng tượng một cái gì đó giống như ví dụ printf-esque này:

for cur_char, next_char in sliding_window(input_string) {
    if cur_char == '%' {
        if next_char == '%' {
            cur_char_index += 1
            goto handle_literal
        }
        # Some additional logic
        if chars_should_be_handled_literally() {
            goto handle_literal
        }
        # Handle the format
    }
    # some other control characters
    else {
      handle_literal:
        # Complicated logic here
        # Maybe it's writing to an array for some OpenGL calls later or something,
        # all while modifying a bunch of local variables declared outside the loop
    }
}

Bạn có thể cấu trúc lại goto handle_literalmột lệnh gọi hàm, nhưng nếu nó sửa đổi một số biến cục bộ khác nhau, bạn sẽ phải chuyển các tham chiếu đến từng biến trừ khi ngôn ngữ của bạn hỗ trợ các bao đóng có thể thay đổi. Bạn vẫn phải sử dụng một continuecâu lệnh (được cho là một dạng goto) sau cuộc gọi để có cùng ngữ nghĩa nếu logic của bạn làm cho một trường hợp khác không hoạt động.

Tôi cũng đã sử dụng gotos một cách thận trọng trong các từ vựng, điển hình cho các trường hợp tương tự. Bạn không cần chúng hầu hết thời gian, nhưng họ rất vui khi có những trường hợp kỳ lạ đó.

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.