C hata yönetimi için geçerli goto kullanımı?


95

Bu soru aslında bir süre önce programlama.reddit.com'daki ilginç bir tartışmanın sonucudur . Temel olarak aşağıdaki koda indirgenir:

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;
}

gotoBurada kullanımı, tüm olasılıkların en temiz ve en verimli koduyla sonuçlanan en iyi yol gibi görünüyor veya en azından bana öyle geliyor. Code Complete'de Steve McConnell'den alıntı yapmak :

Goto, kaynakları tahsis eden, bu kaynaklar üzerinde işlemler gerçekleştiren ve ardından kaynakları serbest bırakan bir rutinde kullanışlıdır. Bir goto ile kodun bir bölümünü temizleyebilirsiniz. Goto, bir hata tespit ettiğiniz her yerdeki kaynakları ayırmayı unutmanız olasılığını azaltır.

Bu yaklaşım için başka bir destek , bu bölümdeki Linux Aygıt Sürücüleri kitabından gelmektedir .

Ne düşünüyorsun? Bu durum gotoC için geçerli bir kullanım mı? Daha kıvrımlı ve / veya daha az verimli kod üreten, ancak kaçınan diğer yöntemleri tercih eder miydiniz goto?



@Eli: Neden etiketleri kaldırıp (cleanup_3 ();) işlevini if'in parantezine yerleştirmiyorsunuz?

@Akito: Ne demek istiyorsun? Önerinizi eksiksiz bir kod örneği ile bir cevap olarak gönderebilir misiniz?
Eli Bendersky

@EliBendersky: Lütfen cevabıma bakın.

Visual Basic'ten (VBS ve VB6 dahil) en çok nefret ettiğim şeylerden biri on error goto errorhata işleme sistemiydi :)
Manu343726

Yanıtlar:


64

FWIF, soru örneğinde verdiğiniz hata işleme deyimini, şu ana kadar yanıtlarda verilen alternatiflerden daha okunaklı ve anlaşılması daha kolay buluyorum. gotoGenel olarak kötü bir fikir olsa da , basit ve tekdüze bir şekilde yapıldığında hata işleme için faydalı olabilir. Bu durumda, a olmasına rağmen goto, iyi tanımlanmış ve az ya da çok yapılandırılmış bir şekilde kullanılmaktadır.


1
Bu etiketleri kaldırıp işlevleri doğrudan eğer içine koyamaz mı? Daha okunaklı olmayacak mı?

8
@StartupCrazy, bunun çok eski olduğunu biliyorum, ancak bu sitedeki gönderilerin geçerliliği adına hayır yapamayacağına işaret edeceğim. Eğer kodunda hata3 gotoda bir hata alırsa, 1 2 ve 3'ü temizlerdi, sizin önerilen çözümünüzde sadece temizlik 3'ü çalıştırırdı. Bir şeyleri iç içe geçirebilirdi, ancak bu sadece ok antipatterni olurdu, programcıların kaçınması gereken bir şey .
gbtimmon

18

Genel bir kural olarak, gitmekten kaçınmak iyi bir fikirdir, ancak Dijkstra ilk kez 'GOTO Zararlı Olarak Kabul Edildi' yazarken yaygın olan suistimaller bugünlerde çoğu insanın aklından geçmiyor bile.

Anlattığınız şey, hata işleme sorununa genelleştirilebilir bir çözümdür - dikkatlice kullanıldığı sürece benim için sorun değil.

Belirli örneğiniz aşağıdaki gibi basitleştirilebilir (1. adım):

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;
}

Süreci devam ettirmek:

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;
}

Bunun orijinal koda denk olduğuna inanıyorum. Orijinal kodun kendisi çok temiz ve iyi organize edilmiş olduğu için bu özellikle temiz görünüyor. Çoğunlukla, kod parçaları bu kadar derli toplu değildir (yine de olması gereken bir argümanı kabul edeceğim); örneğin, başlatma (kurulum) rutinlerine genellikle gösterilenden daha fazla durum geçirilir ve bu nedenle de temizleme rutinlerine geçirilecek daha fazla durum vardır.


24
Evet, yuvalanmış çözüm uygulanabilir alternatiflerden biridir. Maalesef yuvalama seviyesi derinleştikçe daha az uygulanabilir hale gelir.
Eli Bendersky

4
@eliben: Kabul edildi - ancak daha derin iç içe geçme, daha fazla işlev eklemeniz veya hazırlık adımlarının daha fazlasını yapmasını sağlamanız veya kodunuzu başka şekilde yeniden düzenlemeniz gerektiğinin bir göstergesi olabilir (muhtemelen). Hazırlama işlevlerinin her birinin kurulumunu yapması, zincirdeki bir sonrakini çağırması ve kendi temizliğini yapması gerektiğini söyleyebilirim. Bu çalışmayı yerelleştirir - üç temizleme işlevinden bile tasarruf edebilirsiniz. Ayrıca, kısmen, herhangi bir başka arama sırasında kurulum veya temizleme işlevlerinden herhangi birinin kullanılıp kullanılmadığına (kullanılabilir) bağlıdır.
Jonathan Leffler

6
Maalesef bu, döngülerle çalışmaz - bir döngü içinde bir hata oluşursa, o zaman bir goto, bayrakları ayarlama ve kontrol etme alternatifinden (sadece akıllıca gizlenmiş gotoslardır) çok, çok daha temizdir.
Adam Rosenfield

1
@Smith, Daha çok kurşun geçirmez yelek olmadan araba kullanmak gibi.
strager

6
Burada büyücülük yaptığımı biliyorum, ancak bu tavsiyeyi oldukça yetersiz buluyorum - ok modeline karşı çıkmaktan kaçınılmalıdır.
KingRadical

16

Kimsenin bu alternatifi önermemesine şaşırdım, bu yüzden soru bir süredir ortalıkta olsa da ekleyeceğim: Bu sorunu ele almanın iyi bir yolu, mevcut durumu takip etmek için değişkenler kullanmaktır. Bu, gototemizleme koduna ulaşmak için kullanılıp kullanılmadığına bakılmaksızın kullanılabilecek bir tekniktir . Herhangi bir kodlama tekniği gibi, artıları ve eksileri vardır ve her durum için uygun olmayacaktır, ancak bir stil seçiyorsanız, özellikle gotoderinlemesine iç içe geçmeden kaçınmak istiyorsanız dikkate almaya değerif .

Temel fikir, yapılması gerekebilecek her temizleme eylemi için, değerinden temizlemenin yapılması gerekip gerekmediğini anlayabileceğimiz bir değişken olmasıdır.

gotoİlk olarak versiyonu göstereceğim , çünkü orijinal sorudaki koda daha yakı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;
}

Bunun diğer bazı tekniklere göre bir avantajı, başlatma işlevlerinin sırası değiştirilirse, doğru temizlemenin yine de gerçekleşecek olmasıdır - örneğin, switchbaşka bir yanıtta açıklanan yöntemi kullanarak, başlatma sırası değişirse, o zaman switchİlk başta başlatılmamış bir şeyi temizlemeye çalışmaktan kaçınmak için çok dikkatli bir şekilde düzenlenmesi gerekir.

Şimdi, bazıları bu yöntemin pek çok ekstra değişken eklediğini iddia edebilir - ve bu durumda bu doğrudur - ancak pratikte genellikle mevcut bir değişken gerekli durumu zaten izler veya izlemek için yapılabilir. Örneğin prepare_stuff(), aslında bir malloc()ya da bir çağrı ise , open()döndürülen işaretçiyi veya dosya tanımlayıcısını tutan değişken kullanılabilir - örneğin:

int fd = -1;

....

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

...

cleanup:

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

Şimdi, ek olarak hata durumunu bir değişkenle gotoizlersek, ihtiyacımız olan daha derin ve derinleşen girintilere sahip olmadan, tamamen önleyebilir ve yine de doğru şekilde temizleyebiliriz:

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;
}

Yine, bununla ilgili potansiyel eleştiriler var:

  • Tüm bu "eğer" performansa zarar vermiyor mu? Hayır - çünkü başarı durumunda, yine de tüm kontrolleri yapmanız gerekir (aksi takdirde tüm hata durumlarını kontrol edemezsiniz); ve başarısızlık durumunda çoğu derleyici, başarısız if (oksofar)denetimlerin sırasını temizleme koduna tek bir sıçramaya optimize edecektir (GCC kesinlikle yapar) - ve her durumda, hata durumu performans için genellikle daha az kritiktir.
  • Bu yine başka bir değişken eklemiyor mu? Bu durumda evet, ancak genellikle return_valuedeğişken oksofarburada oynayan rolü oynamak için kullanılabilir . İşlevlerinizi tutarlı bir şekilde hata döndürecek şekilde yapılandırırsanız, ifher durumda ikincisini bile önleyebilirsiniz :

    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);
    }
    

    Bunun gibi kodlamanın avantajlarından biri, tutarlılığın, orijinal programcının dönüş değerini kontrol etmeyi unuttuğu herhangi bir yerin hassas bir başparmak gibi çıkması ve bu da (bir sınıfın) hataların bulunmasını çok daha kolay hale getirmesidir.

Yani - bu (henüz) bu sorunu çözmek için kullanılabilecek bir stil daha. Doğru kullanıldığında çok temiz, tutarlı kod sağlar - ve herhangi bir teknik gibi, yanlış ellerde uzun soluklu ve kafa karıştırıcı kodlar üretebilir :-)


2
Görünüşe göre partiye geç kaldın, ama cevabı kesinlikle beğendim!

Linus muhtemelen kod bloglarınızı
Fizz

1
@ user3588161: İsterse, bu onun ayrıcalığıdır - ancak bağlantı kurduğunuz makaleye göre, durumun bu olduğundan emin değilim: anlattığım stilde, (1) koşul ifadelerinin iç içe geçmediğini unutmayın keyfi derinlemesine ve (2) yine de ihtiyacınız olana kıyasla ek "eğer" ifadesi yoktur (tüm dönüş kodlarını kontrol edeceğinizi varsayarak).
psmears

O kadar ki, korkunç goto ve daha da kötüsü ok karşıtı çözüm yerine bu!
Paramagnetic Kruvasan

8

gotoAnahtar kelimeyle ilgili sorun çoğunlukla yanlış anlaşılıyor. Açıkça kötü değil. Sadece her gittiğinizde yarattığınız ekstra kontrol yollarının farkında olmanız gerekir. Kodunuz ve dolayısıyla geçerliliği hakkında mantık yürütmek zorlaşır.

FWIW, developer.apple.com öğreticilerine bakarsanız, hata işlemeye gitme yaklaşımını kullanırlar.

Biz gotos kullanmıyoruz. Getiri değerlerine daha fazla önem verilmektedir. İstisna yönetimi, setjmp/longjmpne kadar az yaparsanız yapın.


8
Kesinlikle çağrıldığı bazı durumlarda setjmp / longjmp kullansam da, bunu goto'dan bile "daha kötü" olarak düşünürdüm (ayrıca, istendiğinde biraz daha az ihtiyatlı kullanıyorum). Setjmp / longjmp'yi kullandığım zamanlar, (1) Hedef her şeyi mevcut durumundan etkilenmeyecek şekilde kapatacaksa veya (2) Hedef, içinde kontrol edilen her şeyi yeniden başlatacak. setjmp / longjmp korumalı blok, mevcut durumundan bağımsız bir şekilde.
supercat

4

Goto ifadesiyle ilgili ahlaki açıdan yanlış (geçersiz) * işaretçilerde ahlaki olarak yanlış olan bir şey yoktur.

Her şey aracı nasıl kullandığınıza bağlı. Sunduğunuz (önemsiz) durumda, bir vaka ifadesi, daha fazla ek yük ile de olsa aynı mantığı sağlayabilir. Asıl soru, "hız gereksinimim nedir?"

goto sadece hızlıdır, özellikle kısa bir sıçramaya göre derlendiğinden emin olursanız. Hızın önemli olduğu uygulamalar için mükemmeldir. Diğer uygulamalar için, bakım kolaylığı açısından genel giderleri if / else + durumuyla almak muhtemelen mantıklıdır.

Unutmayın: goto uygulamaları öldürmez, geliştiriciler uygulamaları öldürür.

GÜNCELLEME: İşte vaka örneği

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
'Vaka' alternatifini sunabilir misiniz? Ayrıca, bunun C'deki herhangi bir ciddi veri yapısı soyutlaması için gerekli olan void * 'den çok farklı olduğunu iddia ediyorum. Boşluğa * ciddi şekilde karşı çıkan kimsenin olduğunu düşünmüyorum ve olmadan tek bir büyük kod tabanı bulamayacaksınız. o.
Eli Bendersky

Re: void *, bu tam olarak benim açımdan, ahlaki olarak da yanlış bir şey yok. Aşağıdaki anahtar / durum örneği. int foo (int bar) {int return_value = 0; int error_value = 0; eğer (! do_something (bar)) {fail_value = 1; } başka if (! init_stuff (bar)) {fail_value = 2; } başka if (hazırla_düzenleme (bar)) {{dönüş_değer = yapılacak_ şey (bar); cleanup_3 (); } anahtar (hata_değer) {durum 2: temizleme_2 (); kırmak; durum 1: temizleme_1 (); kırmak; varsayılan: ara; }}
webmarc

5
@webmarc, üzgünüm ama bu korkunç! Etiketlere gitmeyi tamamen simüle ettiniz - etiketler için kendi tanımlayıcı olmayan değerlerinizi icat ettiniz ve goto'yu switch / case ile uyguladınız. Failure_value = 1, "goto cleanup_something" den daha temiz mi?
Eli Bendersky

4
Beni buraya ayarladığını hissediyorum ... sorunuz bir fikir için ve benim neyi tercih ederim. Yine de cevabımı verdiğimde, bu korkunç. :-( Etiket adlarıyla ilgili şikayetinize gelince, bunlar, örneğinizin geri kalanı kadar açıklayıcıdır: cleanup_1, foo, bar.
Soruyla ilgisi

1
"Hazırlanmak" gibi bir niyetim yoktu ve herhangi bir olumsuz duyguya neden oluyordum, bunun için üzgünüm! Görünüşe göre yeni yaklaşımınız, daha fazla netlik katmadan yalnızca 'kaldırmaya git'i hedefliyor. Sanki goto'nun ne yaptığını yeniden uyguladınız, daha az net olan daha fazla kod kullanarak. Bu IMHO yapılacak iyi bir şey değil - sadece uğruna gotodan kurtulmak.
Eli Bendersky

2

GOTO kullanışlıdır. Bu işlemcinizin yapabileceği bir şeydir ve bu yüzden ona erişiminiz olmalıdır.

Bazen işlevinize küçük bir şey eklemek istersiniz ve tek goto bunu kolayca yapalım. Zaman kazandırabilir ..


3
İşlemcinizin yapabileceği her şeye erişmenize gerek yok. Çoğu zaman gitmek, alternatife göre daha kafa karıştırıcıdır.
David Thornley

@DavidThornley: Evet, bunu tersi durumda, işlemci harcıyorsun, işlemcinizin yapabileceği her bir şeye ihtiyacı erişimini. Goto, programlamadaki en iyi eğitimdir.
Ron Maimon

1

Genel olarak, bir kod parçasının , program akışının genellikle istenenden daha karmaşık olduğuna dair gotobir belirti olarak en açık şekilde yazılabileceğini düşünürdüm . Diğer program yapılarının kullanımından kaçınmak için garip şekillerde birleştirilmesi goto, hastalığı değil semptomu tedavi etmeye çalışacaktır. Sizin özel örneğiniz aşağıdakiler olmadan uygulanması çok zor olmayabilir goto:

  yapmak {
    .. yalnızca erken çıkış durumunda temizlenmesi gereken şey1'i kurun
    eğer (hata) mola;
    yapmak
    {
      .. erken çıkış durumunda temizlenmesi gereken şey2'yi kurun
      eğer (hata) mola;
      // ***** BU SATIR İLE İLGİLİ METİNE BAKIN
    } süre (0);
    .. cleanup thing2;
  } süre (0);
  .. cleanup thing1;

ancak temizleme yalnızca işlev başarısız olduğunda gerçekleşecekse, gotovaka returnilk hedef etiketinin hemen önüne bir konularak ele alınabilir . Yukarıdaki kod, returnile işaretlenen satıra bir eklemeyi gerektirir *****.

"Normal durumda bile temizleme" senaryosunda, diğer şeylerin yanı gotosıra , kullanımının do/ while(0)yapılarından daha açık olduğunu düşünürdüm , çünkü hedef etiketlerin kendileri pratik olarak breakve do/ while(0)yapılandırmalarından çok daha fazla "BANA BAK" diye haykırırlar . "Yalnızca hata durumunda temizleme" durumu için, returnifade , okunabilirlik açısından hemen hemen mümkün olan en kötü yerde olmak zorunda kalır (dönüş ifadeleri genellikle bir işlevin başında veya "göründüğü gibi" olmalıdır. son); Bir haiz returnbir hedef etiket sadece bir "döngü" bitmeden birini haiz çok daha kolay bu yeterlilik karşılar hemen önce.

BTW, bazen gotohata işleme için kullandığım senaryolardan biri switch, birden çok vakanın kodu aynı hata kodunu paylaştığında bir ifade içindedir . Derleyicim genellikle birden fazla vakanın aynı kodla bittiğini anlayacak kadar akıllı olsa da, şunu söylemenin daha açık olduğunu düşünüyorum:

 REPARSE_PACKET:
  anahtar (paket [0])
  {
    durum PKT_THIS_OPERATION:
      eğer (problem durumu)
        git PACKET_ERROR;
      ... THIS_OPERATION işle
      kırmak;
    durum PKT_THAT_OPERATION:
      eğer (problem durumu)
        git PACKET_ERROR;
      ... THAT_OPERATION işle
      kırmak;
    ...
    durum PKT_PROCESS_CONDITIONALLY
      eğer (paket_uzunluğu <9)
        git PACKET_ERROR;
      eğer ([4] paketini içeren paket_şartı)
      {
        paket_uzunluğu - = 5;
        memmove (paket, paket + 5, paket_uzunluğu);
        REPARSE_PACKET'e gidin;
      }
      Başka
      {
        paket [0] = PKT_CONDITION_SKIPPED;
        paket [4] = paket_uzunluğu;
        packet_length = 5;
        packet_status = READY_TO_SEND;
      }
      kırmak;
    ...
    varsayılan:
    {
     PACKET_ERROR:
      packet_error_count ++;
      packet_length = 4;
      paket [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      kırmak;
    }
  }   

gotoİfadeleri ile değiştirilse ve sarmalanmış koşullu yürütme paketini işlemek için / döngüsü ile birlikte {handle_error(); break;}kullanılabilmesine rağmen , bunun a kullanmaktan daha açık olduğunu düşünmüyorum . Ayrıca, kullanıldığı her yerden kodu kopyalamak mümkün olabilir ve bir derleyici çoğaltılan kodu bir kez yazabilir ve çoğu oluşumu paylaşılan kopyaya atlayarak değiştirebilirken, 'yi kullanmak yerleri fark etmeyi kolaylaştırır. paketi biraz farklı bir şekilde ayarlayan (örneğin, "koşullu yürütme" komutu yürütmemeye karar verirse).dowhile(0)continuegotoPACKET_ERRORgoto PACKET_ERRORgoto


1

Ben şahsen "Güvenlik Kritik Kodunu Yazmak için On - 10 Kuralın Gücü" nün takipçisiyim .

Bu metinden goto hakkında iyi bir fikir olduğuna inandığım şeyi gösteren küçük bir pasaj ekleyeceğim.


Kural: Tüm kodu çok basit kontrol akışı yapılarıyla sınırlayın - goto deyimleri, setjmp veya longjmp yapıları ve doğrudan veya dolaylı özyineleme kullanmayın.

Gerekçe: Daha basit kontrol akışı, doğrulama için daha güçlü yetenekler anlamına gelir ve genellikle daha iyi kod netliği sağlar. Özyinelemenin yasaklanması belki de buradaki en büyük sürprizdir. Yineleme olmadan, yine de, kod çözümleyicileri tarafından yararlanılabilen ve sınırlandırılması gereken tüm yürütmelerin aslında sınırlı olduğunu kanıtlamaya doğrudan yardımcı olabilecek döngüsel olmayan bir işlev çağrısı grafiğine sahip olacağımız garanti edilir. (Bu kuralın tüm işlevlerin tek bir dönüş noktasına sahip olmasını gerektirmediğini unutmayın - bu genellikle kontrol akışını da basitleştirir. Yine de erken bir hata dönüşünün daha basit bir çözüm olduğu yeterli durum vardır.)


Goto kullanımının yasaklanması kötü görünüyor ama:

Kurallar ilk başta acımasız görünüyorsa , hayatınızın tam anlamıyla hayatınızın doğruluğuna bağlı olabileceği durumlarda kodu kontrol etmeyi mümkün kılmak için tasarlandıklarını unutmayın: üzerinde uçtuğunuz uçağı, nükleer santrali kontrol etmek için kullanılan kod yaşadığınız yerden birkaç mil uzakta veya astronotları yörüngeye taşıyan uzay aracı. Kurallar, arabanızdaki emniyet kemeri gibi davranır: Başlangıçta belki biraz rahatsız olurlar, ancak bir süre sonra kullanımları ikinci bir doğa haline gelir ve onları kullanmamak düşünülemez hale gelir.


22
Bununla ilgili sorun şu ki, bunu tamamen gotoortadan kaldırmanın olağan yolu, derinlemesine iç içe geçmiş ifs veya döngülerde bazı “akıllı” boolean kullanmaktır. Bu gerçekten yardımcı olmuyor. Belki araçları daha iyi grok, ancak sen olmayacak ve daha önemlisin.
Donal Fellows

1

Soruda verilen ters sırayla temizleme işleminin, çoğu işlevde işleri temizlemenin en temiz yolu olduğuna katılıyorum. Ama bazen şunu da belirtmek isterim ki, yine de işlevinizin temizlenmesini istersiniz. Bu durumlarda, if (0) {label:} deyimi temizleme işleminin doğru noktasına gitmek için aşağıdaki değişkeni kullanıyorum:

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

Bana cleanup_3temizliğini yapıp sonra ara cleanup_2. Benzer şekilde, cleanup_2temizlemeyi yapmalı, ardından cleanup_1'i çağırmalısınız. Her ne zaman yaparsanız yapın cleanup_[n], bu cleanup_[n-1]gerekli, bu nedenle yöntemin sorumluluğu olmalıdır (böylece, örneğin, cleanup_3arama yapmadan cleanup_2ve muhtemelen bir sızıntıya neden olmadan asla çağrılamaz.)

Bu yaklaşıma göre, gotos yerine temizlik rutinini çağırır ve sonra geri dönersiniz.

gotoYaklaşım değildir yanlış veya kötü olsa da, mutlaka "en temiz" yaklaşımı (IMHO) olmadığını belirterek, sadece değer.

En iyi performansı arıyorsanız, gotoçözümün en iyisi olduğunu düşünüyorum. Ben sadece bununla ilgili olmasını bekliyorum, ancak, performans açısından kritik olan bazı uygulamalarda (örn. Aygıt sürücüleri, gömülü aygıtlar, vb.). Aksi takdirde, kod netliğinden daha düşük önceliğe sahip bir mikro optimizasyondur.


4
Bu kesintiye uğramaz - temizlik, yalnızca bu rutinde bu sırayla tahsis edilen kaynaklara özgüdür. Diğer yerlerde birbirleriyle ilgili değiller, bu yüzden birini diğerinden aramak mantıklı değil.
Eli Bendersky

0

Buradaki sorunun verilen kodla ilgili yanlış olduğunu düşünüyorum.

Düşünmek:

  1. do_something (), init_stuff () ve ready_stuff () bu durumda false veya nil döndürdüklerinden başarısız olup olmadıklarını biliyor gibi görünürler.
  2. Doğrudan foo () içinde ayarlanan bir durum olmadığından, durumu kurma sorumluluğu bu işlevlerin sorumluluğunda görünmektedir.

Bu nedenle: do_something (), init_stuff () ve ready_stuff () kendi temizliğini yapıyor olmalıdır . Do_something () sonrasında temizleyen ayrı bir cleanup_1 () işlevine sahip olmak, kapsülleme felsefesini kırar. Kötü tasarım.

Kendi temizliğini yaptılarsa, foo () oldukça basit hale gelir.

Diğer yandan. Foo () aslında parçalanması gereken kendi durumunu yarattıysa, o zaman goto uygun olacaktır.


0

İşte tercih ettiğim şey:

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

Eski tartışma, ne var ki .... "ok karşıtı desen" kullanmaya ve daha sonra statik bir satır içi işlevde her iç içe geçmiş düzeyi kapsüllemeye ne dersiniz? Kod temiz görünür, optimaldir (optimizasyonlar etkinleştirildiğinde) ve hiçbir goto kullanılmaz. Kısacası, böl ve fethet. Bir örneğin altında:

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;
}

Uzay açısından, yığında üç kat değişken oluşturuyoruz, bu iyi değil, ancak bu basit örnekte değişkeni yığından kaldırıp bir kayıt kullanarak -O2 ile derlerken bu kayboluyor. Yukarıdaki bloktan aldığım gcc -S -O2 test.cşey şuydu:

    .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

Aşağıdaki örnekte açıklanan tekniği kullanmayı tercih ediyorum ...

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);

}

kaynak: http://blog.staila.com/?p=114


2
bayraklı kod ve ok karşıtı desen (her ikisi de sizin örneğinizde gösterilmiştir), kodu gereksiz yere karmaşık hale getiren şeylerdir. Bunları kullanmak için "goto is evil" nin dışında herhangi bir gerekçe yoktur.
KingRadical

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.