“İf” zincirlerinden nasıl kaçınılır?


266

Bu sözde kod var varsayarsak:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

İşlevler executeStepX, yalnızca bir önceki başarılı olursa çalıştırılmalıdır. Her durumda, executeThisFunctionInAnyCasefonksiyon sonunda çağrılmalıdır. Programlama konusunda bir acemi değilim, çok basit bir soru için özür dilerim: ifkod okunabilirliği pahasına, bu tür "kod piramidi" üreten bu uzun zincirin önlenmesi için bir yol var mı (örneğin C / C ++) ?

executeThisFunctionInAnyCaseİşlev çağrısını atlayabilirsek , kodun basitleştirilebileceğini biliyorum:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Ancak kısıt, executeThisFunctionInAnyCaseişlev çağrısıdır. İfade bir breakşekilde kullanılabilir mi?


254
@ FrédéricHamidi yanlış yanlış yanlış! İstisnalar dışında program akışınızı sürmenin iyi olduğunu söylemeyin! İstisnalar kesinlikle çok fazla nedenden dolayı bu amaç için uygun DEĞİLDİR.
Piotr Zierhoffer

26
@Piotr, Python tarafından şımartıldım (aslında bunu teşvik ediyor). İstisnaların C ++ 'da akış kontrolü için kullanılması gerekmediğini biliyorum, ama burada gerçekten akış kontrolü var mı? Geri dönen bir fonksiyonun falseistisnai bir duruma benzediği düşünülemez mi?
Frédéric Hamidi

13
Bu bir programın anlambilimine bağlıdır. Bir falsedönüş oldukça normal olabilir.
Haziran'da dornhege

28
Sorunuzu ilk revizyonuna geri döndürdüm. Belli sayıda soru (> 0) aldıktan sonra sorunuzu kökten değiştirmemelisiniz, çünkü bu o ana kadar verilen tüm cevapları geçersiz kılacak ve karışıklık yaratacaktır. Bunun yerine yeni bir soru açın.
Ayakkabı

13
Keşke tüm “acemi programcıları” bunun gibi tasarım soruları sorsalardı.
Jezen Thomas

Yanıtlar:


486

Bir &&(mantık AND) kullanabilirsiniz:

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

bu gereksinimlerinizin ikisini de karşılayacaktır:

  • executeStep<X>()yalnızca öncekinin başarılı olması durumunda değerlendirilmelidir (buna kısa devre değerlendirmesi denir )
  • executeThisFunctionInAnyCase() her durumda infaz edilecek

29
Bu hem anlamsal olarak doğrudur (gerçekten, TÜM koşulların doğru olmasını istiyoruz) hem de çok iyi bir programlama tekniği (kısa devre değerlendirmesi). Ayrıca, bu, işlevlerin oluşturulmasının kodu bozacağı herhangi bir karmaşık durumda kullanılabilir.
Sanchises

58
@RobAu: O zaman genç programcıların nihayet kısa yol değerlendirmesine dayanan kodu görmeleri ve sonuçta kıdemli programcı olma yolunda onlara yardımcı olacak bu konuyu araştırmalarını isteyebilir. Açıkça bir kazan-kazan: iyi bir kod ve birileri onu okumaktan bir şeyler öğrendi.
x4u

24
Bu en iyi cevap olmalı
Görünen Ad

61
@RobAu: Bu, bazı belirsiz sözdizim hilelerinden yararlanan bir hack değil, neredeyse her programlama dilinde, tartışmasız standart uygulama noktasına kadar son derece deyimsel.
BlueRaja - Danny Pflughoeft

38
Bu çözüm yalnızca koşullar aslında basit işlev çağrılarıysa geçerlidir. Gerçek kodda, bu koşullar 2-5 satır uzunluğunda olabilir (ve kendileri başkalarının kombinasyonları olabilir &&ve ||), bu nedenle ifokunabilirliği sarsmadan tek bir ifadeye katılmak istemezsiniz . Ve bu koşulları dış fonksiyonlara taşımak her zaman kolay değildir, çünkü daha önce hesaplanmış birçok yerel değişkene bağımlı olabilirler, bu da her birini bireysel argüman olarak geçirmeye çalışırsanız korkunç bir karmaşa yaratacaktır.
hamstergene

358

İkinci sürümünüzü çalıştırmak için ek bir işlev kullanmanız yeterlidir:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

Derin iç içe ifs (ilk varyantınız) veya "bir fonksiyonun bir bölümü" nden ayrılma arzusu kullanmak, genellikle ekstra bir fonksiyona ihtiyacınız olduğu anlamına gelir.


51
+1 sonunda biri aklı başında bir cevap gönderdi. Bence bu en doğru, güvenli ve okunaklı yol.
Lundin

31
+1 Bu "Tek Sorumluluk İlkesi" nin iyi bir örneğidir. İşlev foo, ilgili koşullar ve eylemler dizisi boyunca ilerler. İşlev barkararlardan temiz bir şekilde ayrılır. Koşulların ve eylemlerin ayrıntılarını görürsek foo, hala çok fazla iş yaptığı ortaya çıkabilir , ancak şimdilik bu iyi bir çözüm.
GraniteRobert

13
Dezavantajı, C'nin iç içe geçmiş işlevlere sahip olmamasıdır, bu nedenle barsizden değişkenleri kullanmak için gereken bu 3 adımın bunları manuel fooolarak parametre olarak geçirmesi gerekir . Eğer durum buysa ve foosadece bir kez çağrılırsa, çok tekrar kullanılmayacak olan sıkıca bağlanmış iki işlevi tanımlamaktan kaçınmak için goto sürümünü kullanmaya doğru hata yapardım.
hugomg

7
C için kısa devre sözdiziminden emin değilim, ancak C # foo () olarak yazılabilirif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Goto'lar her zaman kullanılır, özellikle çok fazla kurulum kodu çözmeniz gerektiğinde sistem programlamasında.
Scotty Bauer

166

Eski okul C programcıları gotobu durumda kullanırlar. gotoAslında Linux stil kılavuzu tarafından teşvik edilen tek kullanımdır , buna merkezi işlev çıkışı denir:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Bazı insanlar goto, vücudu bir döngüye sararak ve ondan koparak kullanarak çalışırlar , ancak etkili bir şekilde her iki yaklaşım da aynı şeyi yapar. Bu gotoyaklaşım, yalnızca executeStepA()başarılı olsaydı başka bir temizliğe ihtiyacınız varsa daha iyidir :

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Döngü yaklaşımı ile bu durumda iki seviye döngü elde edersiniz.


108
+1. Birçok insan goto"Bu korkunç kod" u görür ve hemen düşünür, ancak geçerli kullanımları vardır.
Graeme Perrow

46
Bunun dışında aslında bakım açısından oldukça dağınık bir kod var. Özellikle, kodun okunmasının daha da zorlaştığı birden fazla etiket olduğunda. Bunu başarmanın daha zarif yolları vardır: işlevleri kullanın.
Lundin

29
-1 Anlıyorum gotove bunun korkunç bir kod olduğunu düşünmek zorunda bile değilim. Bir zamanlar böyle korumak zorundaydım, çok kötü. OP, sorunun sonunda makul bir C dili alternatifi önerir ve bunu cevabıma dahil ettim.
Şerefe ve s. - Alf

56
Bu sınırlı, müstakil goto kullanımı ile ilgili yanlış bir şey yoktur. Ama git, dikkat olan bir geçiş maddesi ve size ayaklarıyla kimsenin yediği spagetti ve size sadece bu düzeltebilirim" sandıktan sonra 15 yıl başlatılması için onu bir gün farkına yaptığını olacağı dikkatli olmazsak aksi takdirde bir etiket ile kabus böcek ... "
Tim Post

66
Muhtemelen bu tarzda yüz bin satır son derece net, bakımı kolay kod yazdım. Anlaşılması gereken iki önemli şey (1) disiplin! her işlevin düzeni için açık bir kılavuz oluşturmalı ve yalnızca aşırı ihtiyaç durumunda ihlal etmeli ve (2) burada yaptığımızın , sahip olmadığı bir dilde istisnaları simüle ettiğini anlayın . throwbirçok yönden daha kötüdür, gotoçünkü sonunda nereye gideceğiniz yerel bağlamdanthrow bile net değildir ! Goto tarzı kontrol akışı için istisnalar için kullandığınız tasarım düşüncelerinin aynısını kullanın.
Eric Lippert

131

Bu yaygın bir durumdur ve bununla başa çıkmanın birçok yaygın yolu vardır. İşte kanonik bir cevap verme girişimim. Bir şey kaçırırsam lütfen yorum yapın ve bu yayını güncel tutacağım.

Bu bir ok

Tartıştığınız konu ok anti-paterni olarak bilinir . Buna ok adı verilir, çünkü iç içe ifs zinciri, kod düzenleyici bölmesinin sağ tarafını "gösteren" görsel bir ok oluşturarak sağa ve daha sonra sola doğru genişleyen kod blokları oluşturur.

Muhafızla Oku Düzleştir

Oktan kaçınmanın bazı yaygın yolları burada tartışılmıştır . En yaygın yöntem, kodun önce istisna akışlarını ve ardından temel akışı işlediği bir koruma düzeni kullanmaktır , örneğin

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... kullanırsınız ....

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Uzun bir koruma serisi olduğunda, tüm korumalar sola kadar göründüğü ve ifs'leriniz iç içe olmadığından bu kodu önemli ölçüde düzleştirir. Ayrıca, mantık koşulunu ilişkili hatayla görsel olarak eşleştiriyorsunuz, bu da neler olduğunu anlatmayı çok daha kolay hale getiriyor:

Ok:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

koru:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Bu objektif ve ölçülebilir bir şekilde okunması daha kolaydır çünkü

  1. Belirli bir mantık bloğunun {ve} karakterleri birbirine daha yakın
  2. Belirli bir çizgiyi anlamak için gerekli zihinsel bağlam miktarı daha azdır
  3. Bir if koşuluyla ilişkili mantığın bütününün bir sayfada olması daha olasıdır
  4. Kodlayıcının sayfayı / göz parçasını kaydırma ihtiyacı büyük ölçüde azalır

Sonunda ortak kod nasıl eklenir

Muhafız modelindeki sorun, "fırsatçı dönüş" veya "fırsatçı çıkış" olarak adlandırılan şeye dayanmasıdır. Başka bir deyişle, her bir işlevin tam olarak bir çıkış noktasına sahip olması gereken deseni kırar. Bu iki nedenden dolayı bir sorundur:

  1. Bazı insanları yanlış bir şekilde ovuyor, örneğin Pascal'a kod yazmayı öğrenen insanlar bir fonksiyonun = bir çıkış noktası olduğunu öğrendiler.
  2. Ne olursa olsun , eldeki konu ne olursa olsun çıkışta çalışan bir kod bölümü sağlamaz .

Aşağıda, dil özelliklerini kullanarak veya sorunu tamamen önleyerek bu kısıtlamaya bir çözüm bulmak için bazı seçenekler sağladım.

Seçenek 1. Bunu yapamazsınız: kullanın finally

Ne yazık ki, bir c ++ geliştiricisi olarak bunu yapamazsınız. Ancak bu, nihayetinde anahtar kelime içeren diller için bir numaralı yanıttır, çünkü tam olarak bunun için kullanılır.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

2. Seçenek. Sorunu önleyin: İşlevlerinizi yeniden yapılandırın

Kodu iki işleve bölerek sorunu önleyebilirsiniz. Bu çözüm, herhangi bir dil için çalışma avantajına sahiptir ve ek olarak, hata oranınızı azaltmanın kanıtlanmış bir yolu olan ve herhangi bir otomatik birim testinin özgüllüğünü geliştiren siklomatik karmaşıklığı azaltabilir.

İşte bir örnek:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Seçenek 3. Dil hilesi: Sahte bir döngü kullanın

Gördüğüm bir başka yaygın hile, diğer cevaplarda gösterildiği gibi while (true) ve break kullanmaktır.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Bu, kullanmaktan daha az "dürüst" gotoolsa da, yeniden düzenleme sırasında dağılmaya daha az eğilimlidir, çünkü mantık kapsamının sınırlarını açıkça işaretler. Etiketlerinizi veya etiketlerinizi kesen ve yapıştıran saf bir kodlayıcıgoto ifadelerinizi büyük sorunlara neden olabilir! (Ve açıkçası desen çok yaygın şimdi bence niyetini açıkça iletiyor ve bu nedenle "dürüst olmayan" değil).

Bu seçeneklerin başka çeşitleri de vardır. Örneğin, switchbunun yerine kullanılabilir while. Bir breakanahtar kelimeye sahip herhangi bir dil yapısı muhtemelen işe yarar.

Seçenek 4. Nesne yaşam döngüsünü kullanın

Diğer bir yaklaşım nesne yaşam döngüsünü güçlendirir. Parametrelerinizi taşımak için bir bağlam nesnesi kullanın (naif örneğimizde şüpheli olmayan bir şey) ve işiniz bittiğinde imha edin.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Not: Seçtiğiniz dilin nesne yaşam döngüsünü anladığınızdan emin olun. Bunun çalışması için bir çeşit deterministik çöp toplamaya ihtiyacınız var, yani yıkıcının ne zaman çağrıldığını bilmelisiniz. Bazı dillerde kullanmanız gerekecekDispose yıkıcı yerine .

Seçenek 4.1. Nesne yaşam döngüsünü kullanın (sarma deseni)

Nesneye yönelik bir yaklaşım kullanacaksanız, doğru da yapabilirsiniz. Bu seçenek, temizleme gerektiren kaynakları ve diğer işlemlerini "sarmak" için bir sınıf kullanır.

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Yine, nesne yaşam döngünüzü anladığınızdan emin olun.

Seçenek 5. Dil hilesi: Kısa devre değerlendirmesini kullanma

Diğer bir teknik kısa devre değerlendirmesinden faydalanmaktır .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Bu çözüm, && operatörünün çalışma şeklinden yararlanır. && öğesinin sol tarafı yanlış olarak değerlendirildiğinde, sağ taraf asla değerlendirilmez.

Bu hile en çok kompakt kod gerektiğinde ve kodun çok fazla bakım görmesi mümkün olmadığında yararlıdır, örneğin iyi bilinen bir algoritma uyguluyorsunuz. Daha genel kodlama için bu kodun yapısı çok kırılgandır; mantıktaki küçük bir değişiklik bile toplam yeniden yazmayı tetikleyebilir.


12
En sonunda? C ++ 'ın sonunda bir yan tümcesi yoktur. Yıkıcılı bir nesne, nihayet bir maddeye ihtiyacınız olduğunu düşündüğünüz durumlarda kullanılır.
Bill Door

1
Yukarıdaki iki yorumu barındıracak şekilde düzenlendi.
John Wu

2
Önemsiz kodla (örneğin benim örneğimde) iç içe geçmiş desenler muhtemelen daha anlaşılır. Gerçek dünya koduyla (birkaç sayfaya yayılabilen), koruma modelinin okunması objektif olarak daha kolaydır, çünkü daha az kaydırma ve daha az göz takibi gerektirir, örneğin {ila} arasındaki ortalama mesafe daha kısadır.
John Wu

1
Kodun 1920 x 1080 ekranda artık görünmediği iç içe bir desen gördüm ... Üçüncü eylem başarısız olursa hangi hata işleme kodunun gerçekleştirileceğini bulmaya çalışın ... do {...} (0) bunun yerine son molaya ihtiyacınız yoktur (öte yandan, (true) {...} "devam
etmenin

2
Seçenek 4, aslında C ++ 'da bir bellek sızıntısıdır (küçük sözdizimi hatasını yoksayarak). Bu durumda "new" kullanılmaz, "MyContext myContext;" deyin.
Sumudu Fernando

60

Sadece yap

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

Bu kadar basit.


Her birinin temelde soruyu değiştirdiği üç düzenleme nedeniyle (biri revizyonu sürüm 1'e geri sayarsa dört), cevapladığım kod örneğini ekliyorum:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
Sorunun ilk versiyonuna yanıt olarak bunu (daha kısaca) yanıtladım ve Orbit'teki Darkness Races tarafından yapılan bazı yorum ve düzenlemelerden sonra, Kertenkele Bill tarafından silinmeden önce 20 yukarı oy aldı.
Şerefe ve s. - Alf

1
@ Cheersandhth-Alf: Modun silindiğine inanamıyorum. Bu çok kötü. (+1)
Paramanyetik Kruvasan

2
+ 1'iniz cesaretinize! (3 kez önemlidir: D).
2014'te

Yeni başlayan programcıların birden fazla boole "ve" ile sipariş yürütme, bunun farklı dillerde nasıl farklı olduğu vb. Hakkında bilgi edinmesi tamamen önemlidir. Ancak bu normal ticari programlamada sadece bir "marş dışı" dır. Basit değil, bakımı mümkün değil, değiştirilemez. Tabii, sadece kendiniz için hızlı bir "kovboy kodu" yazıyorsanız, bunu yapın. Ama sadece "bugün uygulandığı gibi günlük yazılım mühendisliği ile bir bağlantısı yok". BTW burada katlanmak zorunda kaldığınız gülünç "düzenleme-karışıklık" için üzgünüm, @cheers :)
Fattie

1
@JoeBlow: Bu konuda Alf'la birlikteyim - &&listeyi çok daha net buluyorum . Ben genellikle satırları ayırmak ve // explanationher birine bir iz eklemek için koşulları kırmak .... Sonunda, bakmak için çok daha az kod ve nasıl &&çalıştığını anladıktan sonra sürekli zihinsel çaba yok. Benim izlenimim, çoğu profesyonel C ++ programcısının buna aşina olacağı, ancak farklı endüstrilerde / projelerde söylediğiniz gibi odaklanma ve deneyim farklıdır.
Tony Delroy

35

Aslında C ++ 'daki eylemleri ertelemenin bir yolu vardır: bir nesnenin yıkıcısını kullanmak.

C ++ 11'e erişiminiz olduğunu varsayarsak:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

Ve sonra bu yardımcı programı kullanarak:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

67
Bu sadece bana tam bir şaşkınlık. Neden bu kadar çok C ++ programcısının, bir sorunu çözmeyi çok sevdiğini anlamıyorum, herkesin hangi sorunu çözdüğünüzü unuttuğu noktaya kadar: artık umursamıyorlar çünkü artık tüm bu egzotik dil özelliklerini kullanmakla ilgileniyor. Ve oradan, günler ve haftalarca meta kod yazıp, daha sonra meta kodu koruyabilir, ardından meta kodu işlemek için meta kod yazabilirsiniz.
Lundin

24
@Lundin: Eh, bir erken devam / kırılma / geri dönüş başlatıldığında veya bir istisna atıldığında kırılacak kırılgan koddan nasıl tatmin olabileceğini anlamıyorum. Öte yandan, bu çözüm gelecekteki bakım karşısında esnektir ve sadece yıkıcıların C ++ 'ın en önemli özelliklerinden biri olan çözme sırasında yürütüldüğü gerçeğine dayanır. Bu hak kazanma egzotik o güçler tüm standart konteyner eğlendirirken olduğunu yatan ilke iken, en az ki.
Matthieu

7
@Lundin: Matthieu'nun kodunun yararı, bir istisna executeThisFunctionInAnyCase();bile olsa yürütülecek olmasıdır foo();. İstisna güvenli kod yazarken, bu tür tüm temizleme işlevlerini bir yıkıcıya koymak iyi bir uygulamadır.
Brian

6
@Brian O zaman bir istisna atmayın foo(). Ve eğer yaparsan, yakala. Sorun çözüldü. Hatalar, geçici çözümler yazarak değil düzelterek düzeltin.
Lundin

18
@Lundin: Defersınıf, istisnai olarak güvenli bir şekilde herhangi bir blok sonu temizliği yapmanızı sağlayan yeniden kullanılabilir küçük bir kod parçasıdır. daha çok bir kapsam koruması olarak bilinir . evet, bir kapsam koruyucunun herhangi bir kullanımı, diğer tüm manuel yollarla forifade edilebilir , tıpkı herhangi bir döngü bir blok ve bir whiledöngü olarak ifade edilebileceği gibi, sırayla ifade edilebilir ifve gotoeğer isterseniz montaj dilinde ifade edilebilir, ya da gerçek ustalar olanlar için, özel kısa homurdanma ve tezahüratların kelebek etkisi tarafından yönlendirilen kozmik ışınlar yoluyla hafızadaki bitleri değiştirerek. ama bunu neden yapıyorsun?
Şerefe ve s. - Alf

34

Dönüş ifadeleriyle ek bir sarma işlevine ihtiyaç duymayan güzel bir teknik var (Itjax tarafından öngörülen yöntem). Bir do while(0)sözde döngü kullanır. while (0)Aslında bir döngü değil ama yalnızca bir kez yürütülür olmasını sağlar. Ancak, döngü sözdizimi break ifadesinin kullanılmasına izin verir.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
Bence birden fazla dönüşlü bir işlev kullanmak benzer, ancak daha okunabilir.
Lundin

4
Evet, kesinlikle daha okunabilir ... ancak verimlilik açısından do {} while (0) yapısı ile ek işlev çağrısı (parametre aktarma ve geri dönüş) ek yükü önlenebilir.
Debasis

5
İşlevi hala yapmakta özgürsünüz inline. Her neyse, bu bilmek güzel bir teknik, çünkü bu problemden daha fazlasına yardımcı oluyor.
Ruslan

2
@Lundin Kod yerini hesaba katmanız gerekiyor, kodu çok fazla yere yayıyor, sorun da var.
API-Beast

3
Deneyimlerime göre, bu çok sıradışı bir deyim. Halkanın neler olduğunu anlamak biraz zaman alacaktı ve kodu incelerken bu kötü bir işaret. Diğer, daha yaygın ve bu nedenle daha okunabilir yaklaşımlara göre hiçbir avantajı olmadığı göz önüne alındığında, bundan vazgeçemedim.
Cody Gray

19

Bunu da yapabilirsiniz:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

Bu şekilde, minimum doğrusal büyüme boyutuna, çağrı başına +1 hattına sahip olursunuz ve kolayca bakım yapılabilir.


EDIT : (Teşekkürler @Unda) Görünürlüğü kaybettiğiniz için büyük bir hayran değil IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
Bu, push_back () içindeki yanlışlıkla işlev çağrılarıyla ilgiliydi, ancak yine de düzelttiniz :)
Quentin

4
Bunun neden bir düşüş yaptığını bilmek isterim. Yürütme adımlarının gerçekten göründükleri gibi simetrik olduğunu varsayarsak, temiz ve bakım yapılabilir.
ClickRick

1
Gerçi bu tartışmasız daha temiz görünebilir. İnsanlar için anlaşılması zor olabilir ve derleyici için anlaşılması kesinlikle daha zordur!
Roy T.

1
Fonksiyonlarınıza veri olarak daha fazla davranmak genellikle iyi bir fikirdir - bunu yaptıktan sonra müteakip yeniden düzenleme işlemlerini de fark edeceksiniz. Daha da iyisi, işlediğiniz her parçanın sadece bir işlev referansı değil, bir nesne referansı olduğu durumdur - bu, kodunuzu satırda daha da geliştirebilmenizi sağlar.
Bill K

3
Önemsiz bir durum için biraz fazla mühendislik yapıldı, ancak bu teknik kesinlikle başkalarının hoş olmayan güzel bir özelliğe sahip: Yürütme sırasını ve çalışma zamanında çağrılan işlevleri [sayısını] değiştirebilirsiniz ve bu güzel :)
Xocoatzin

18

Bu işe yarar mı? Bunun kodunuzla eşdeğer olduğunu düşünüyorum.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
Ben genellikle okböyle aynı değişkeni kullanırken değişkeni çağırır .
Macke

1
Neden iniş çıkışların olduğunu bilmekle çok ilgilenirim. Burada sorun ne?
ABCplus

1
cevabını siklomatik karmaşıklık için kısa devre yaklaşımıyla karşılaştırır.
AlphaGoku

14

İstenen kod şu anda gördüğüm gibi varsayarsak:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Doğru yaklaşımın, okunması en kolay ve bakımı en kolay olanı, daha az girinti seviyesine sahip olacağını söyleyebilirim (şu anda) sorunun belirtilen amacı.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Bu, s, istisnalar, kukla döngüler veya diğer zor yapılara herhangi bir ihtiyacı önler ve sadece eldeki basit işe başlar.gotowhile


Döngüler kullanıldığında, ekstra "bayrak" değişkenleri eklemeye gerek kalmadan döngüden çıkmak returnve breakatlamak genellikle kabul edilebilir . Bu durumda, bir goto kullanmak benzer şekilde masum olur - ekstra değişken değişken karmaşıklığı için ekstra goto karmaşıklığı ticareti yaptığınızı unutmayın.
Haziran'da hugomg

2
@hugomg Değişkenler orijinal sorudaydı. Burada fazladan karmaşıklık yok. Soru ile ilgili varsayımlar vardı (örneğin değişkenlerin düzeltilmiş kodda ihtiyaç duyulması), bu yüzden tutuldular. Eğer gerekli değilse, kod basitleştirilebilir, ancak sorunun eksik doğası göz önüne alındığında, geçerli olarak yapılabilecek başka bir varsayım yoktur.
ClickRick

Çok faydalı bir yaklaşım, özellikle. kendi kendini savunan bir kişi tarafından kullanılmak üzere newbie, hiçbir dezavantajı olmayan daha temiz bir çözüm sağlar. Ayrıca steps, bloklardan ziyade aynı imzanın veya işlevlerin olmasına da bağlı olmadığını unutmayın . Bunun daha sofistike bir yaklaşımın geçerli olduğu durumlarda bile ilk geçiş refaktörü olarak kullanıldığını görebiliyorum.
Keith

12

Break ifadesi bir şekilde kullanılabilir mi?

Belki en iyi çözüm değildir, ancak ifadelerinizi bir do .. while (0)döngüye koyabilir ve breakbunun yerine ifadeleri kullanabilirsiniz return.


2
Onu küçümseyen ben değildim, ama bu, etkinin şu anda istendiği, ancak kaçınılmaz olarak acıya yol açacağı bir şey için döngüsel bir yapının kötüye kullanılması olacaktır. Muhtemelen başka bir projeye geçtikten sonra 2 yıl içinde sürdürmesi gereken bir sonraki geliştirici için.
ClickRick

3
@ClickRick do .. while (0), makro tanımları için kullanımı da döngüleri kötüye kullanıyor ancak OK olarak kabul ediliyor.
ouah

1
Belki de, ancak bunu başarmanın daha temiz yolları vardır.
ClickRick

4
@ClickRick Cevabımın tek amacı cevabı kırmaktır. Bir şekilde açıklama cümlesi kullanılabilir mi ve cevap evet, cevabımdaki ilk kelimeler bunun bir çözüm olmadığını gösteriyor.
ouah

2
Bu cevap sadece bir yorum olmalı
msmucker0527

12

İstediğiniz ifgibi biçimlendirilmiş tüm koşulları kendi işlevlerinde koyabilirsiniz , dönüşteexecuteThisFunctionInAnyCase() işlevi .

OP'deki temel örnekten, koşul testi ve yürütme bu şekilde ayrılabilir;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

Ve sonra böyle denir;

InitialSteps();
executeThisFunctionInAnyCase();

C ++ 11 lambdas mevcutsa (OP'de C ++ 11 etiketi yoktu, ancak yine de bir seçenek olabilir), o zaman ayrı bir işlevi bırakabilir ve bunu bir lambdaya sarabiliriz.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

Döngülerden hoşlanmıyorsanız gotove sevmiyorsanız do { } while (0);ve C ++ kullanmak istiyorsanız, aynı etkiye sahip olmak için geçici bir lambda da kullanabilirsiniz.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if && (0) && C ++ 'ı sevdiğinizde hoşlanmadınız goto'dan hoşlanmıyorsunuz ... Üzgünüz, direnemedim, ancak son durum başarısız oldu, çünkü soru c yanı sıra c ++
14'de ClickRick

@ClickRick herkesi memnun etmek her zaman zordur. In my görüş C / C ++ bunlardan birine bir ve diğer kullanımında genellikle kod hoş karşılanmaz diye bir şey yoktur.
Alex

9

Kodunuzdaki IF / ELSE zincirleri dil sorunu değil, programınızın tasarımıdır. Programınızı yeniden faktörlendirebiliyor veya yeniden yazabiliyorsanız, Tasarım Desenlerine ( http://sourcemaking.com/design_patterns) bakmanızı öneririm. daha iyi bir çözüm bulmak için ) .

Genellikle, kodunuzda çok sayıda IF ve başka bir şey gördüğünüzde, Strateji Tasarım Deseni'ni uygulamak için bir fırsattır ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) veya belki de bir kombinasyon diğer desenler.

Eminim eğer / else uzun bir liste yazmak için alternatifler vardır, ama zincir size güzel görünecek dışında bir şey değişecek şüpheliyim (Ancak, güzellik hala bakanın gözünde hala kod için geçerlidir çok :-)). Gibi şeyler hakkında endişelenmelisiniz (yeni bir koşul olduğunda 6 ay içinde ve bu kod hakkında hiçbir şey hatırlamıyorum, kolayca ekleyebilir miyim? Veya zincir değişirse, ne kadar hızlı ve hatasız olursa? uygulayacak mıyım)


9

Sadece bunu yap ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

100'ün 99 katı, bunu yapmanın tek yolu bu.

Asla, asla, asla bilgisayar kodunda "zor" bir şey yapmaya çalışmayın.


Bu arada, eminim aşağıdakiler aklınızdaki gerçek çözümdür ...

sürdürmek açıklamada algoritmik programlamada kritiktir. (Goto ifadesi algoritmik programlamada kritik öneme sahiptir.)

Birçok programlama dilinde bunu yapabilirsiniz:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Her şeyden önce not edin: bu örnek gibi çıplak bloklar, özellikle "algoritmik" programlama ile uğraşıyorsanız, güzel kod yazmanın kritik ve önemli bir parçasıdır.)

Yine, kafanda tam olarak bu vardı, değil mi? Ve bunu yazmanın güzel yolu, bu yüzden iyi içgüdülerin var.

Ancak, trajik olarak, obj -c'nin mevcut sürümünde (Kenara - Swift hakkında bilmiyorum, üzgünüm), kapalı bloğun bir döngü olup olmadığını kontrol ettiği riske atılabilir bir özellik var.

resim açıklamasını buraya girin

İşte bununla başa çıkmak için ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Unutma bunu ..

{} while (false);

sadece "bu bloğu bir kez yap" anlamına gelir.

yani yazma do{}while(false);ile basitçe yazma arasında hiçbir fark yoktur {}.

Bu şimdi istediğiniz gibi mükemmel çalışıyor ... İşte çıktı ...

resim açıklamasını buraya girin

Yani, algoritmayı kafanızda bu şekilde görmek mümkündür. Her zaman kafanızdakileri yazmaya çalışmalısınız. (Özellikle ayık değilseniz, o zaman güzel ortaya çıkıyor! :))

Bunun çok fazla olduğu "algoritmik" projelerde, obj-c'de her zaman ...

#define RUNONCE while(false)

... o zaman bunu yapabilirsiniz ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

İki nokta vardır:

a, obj-c'nin bir devam ifadesinin içinde bulunduğu blok tipini kontrol etmesi aptalca olsa da, "bununla savaşmak" çok zordur. Bu zor bir karar.

b, bu blokta girintili olmanız gereken soru var mı? Böyle sorular üzerinde uykumu kaybediyorum, bu yüzden tavsiye edemem.

Umarım yardımcı olur.


En çok oy alan ikinci yanıtı kaçırdınız .
Palec

Daha fazla temsil için ödül koydun mu? :)
TMS

Tüm bu yorumları içine koymak yerine, ifdaha açıklayıcı işlev adları kullanabilir ve yorumları işlevlere koyabilirsiniz.
Thomas Ahle

Yuck. Kısa bir süre değerlendirme ile (20 yıldan fazla bir süredir dillere sahip ve iyi bilinen) kısa bir okuma ile özdeş olarak okuyan 1 hat çözümünü her gün bu karmaşadan alacağım. Bence ikimiz de birbirimizle çalışmadığımız için mutlu olduğumuzu kabul edebiliriz.
M2tM

8

False döndürmek yerine başarısız olursa yürütme işlevlerinizin bir istisna atmasını sağlayın. Arama kodunuz şöyle görünebilir:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Tabii ki orijinal örneğinizde yürütme adımının yalnızca adım içinde bir hata olması durumunda yanlış döndüreceğini varsayıyorum?


3
akışı kontrol etmek için istisna kullanmak genellikle kötü uygulama ve koklamak kodu olarak kabul edilir
user902383

8

Birçok iyi cevap zaten, ama çoğu esnekliğin bazılarına (kuşkusuz çok az) karşı çıkıyor gibi görünüyor. Bu değiş tokuşu gerektirmeyen ortak bir yaklaşım, bir durum / devam eden değişken eklemektir . Fiyat, elbette, takip etmek için ekstra bir değerdir:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

Neden ok &= conditionX;ve basit değil ok = conditionX;?
ABCplus

@ user3253359 Çoğu durumda, evet, bunu yapabilirsiniz. Bu kavramsal bir demo; çalışma kodunda mümkün olduğunca basitleştirmeye çalışacağız
blgt

+1 Soruda öngörüldüğü gibi , c ile çalışan az sayıdaki temiz ve sürdürülebilir cevaptan biri.
ClickRick

6

C ++ 'da (soru hem C hem de C ++ olarak etiketlenir), özel durumları kullanmak için işlevleri değiştiremezseniz, yine de küçük bir yardımcı işlev yazıyorsanız,

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Ardından kodunuz aşağıdaki gibi okunabilir:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Süslü sözdizimindeyseniz, bunun yerine açık döküm yoluyla çalışmasını sağlayabilirsiniz:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Sonra kodunuzu şu şekilde yazabilirsiniz:

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Değer kontrollerinin istisnalara göre yeniden düzenlenmesi mutlaka iyi bir yol değildir, önemli ölçüde genel gevşeme istisnaları vardır.
John Wu

4
-1 C ++ 'da normal akış için istisnaların kullanılması zayıf programlama uygulamasıdır. C ++ 'da istisnalar istisnai durumlar için ayrılmalıdır.
Jack Aidley

1
Soru metninden (benim tarafımdan vurgulanır): "executeStepX işlevleri yalnızca ve önceki başarılı olursa çalıştırılmalıdır . " Başka bir deyişle, dönüş değeri başarısızlığı belirtmek için kullanılır. Olduğunu, bu hata işleme (ve bir o arızaları umut ediyorum olan olağanüstü). Hata işleme, istisnalar için tam olarak icat edildi.
celtschk

1
Hayır! İlk olarak, hata işlemeye değil hata yayılmasına izin vermek için istisnalar oluşturuldu ; ikincisi, "Yalnızca önceki başarılı olursa executeStepX işlevleri yürütülmelidir." önceki işlev tarafından döndürülen boolean false değerinin açıkça istisnai / hatalı bir durumu gösterdiği anlamına gelmez. İfadeniz bu nedenle sıralı değildir . Hata işleme ve akış dezenfeksiyonu birçok farklı yolla gerçekleştirilebilir, istisnalar hata yayılımına ve yerinde olmayan hata işlemeye izin veren bir araçtır ve bu konuda mükemmelleşir.

6

Kodunuz örneğiniz kadar basitse ve diliniz kısa devre değerlendirmelerini destekliyorsa, bunu deneyebilirsiniz:

StepA() && StepB() && StepC() && StepD();
DoAlways();

İşlevlerinize bağımsız değişkenler veriyorsanız ve kodunuzun önceki şekilde yazılmaması için diğer sonuçları geri alıyorsanız, diğer yanıtların çoğu soruna daha uygun olacaktır.


Aslında konuyu daha iyi açıklamak için sorumu düzenledim, ancak çoğu cevabı geçersiz kılmamak reddedildi. : \
ABCplus

SO'da yeni bir kullanıcıyım ve yeni başlayan bir programcıyım. 2 soru öyleyse: Söylediğiniz soru gibi başka bir sorunun, bu soru nedeniyle çoğaltılmış olarak işaretlenmesi riski var mı? Başka bir nokta şudur: Bir acemi SO kullanıcı / programcı tüm orada arasındaki en iyi cevabı nasıl seçebilir (sanırım neredeyse iyi ..)?
ABCplus

6

C ++ 11 ve ötesinde, D'nin kapsamına (çıkış) benzer bir kapsam çıkış sistemi uygulamak iyi bir yaklaşım olabilir mekanizmasına .

Uygulamanın olası bir yolu, C ++ 11 lambdas ve bazı yardımcı makrolar kullanmaktır:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Bu, işlevden erken dönmenize ve tanımladığınız temizleme kodunun kapsam çıkışında her zaman yürütülmesini sağlar:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Makrolar gerçekten sadece dekorasyon. MakeScopeExit()doğrudan kullanılabilir.


Bunu yapmak için makrolara gerek yoktur. Ve [=]bir kapsamına sahip lambda için genellikle yanlıştır.
Yakk - Adam Nevraumont

Evet, makrolar sadece dekorasyon amaçlıdır ve atılabilir. Ama değere göre yakalamanın en güvenli "jenerik" yaklaşım olduğunu söylemez miydiniz?
glampert

1
hayır: lambda'nız lambda'nın oluşturulduğu mevcut kapsamın ötesinde kalmazsa, kullanın [&]: güvenli ve minimal şaşırtıcıdır. Sadece lambda (ya da kopyalar) deklarasyon noktasında kapsamdan daha uzun süre hayatta kalabildiğinde değere göre yakalayın ...
Yakk - Adam Nevraumont

Evet, bu mantıklı. Ben değiştireceğim. Teşekkürler!
glampert

6

Neden kimse en basit çözümü vermiyor? : D

Tüm işlevleriniz aynı imzayı taşıyorsa, bunu şu şekilde yapabilirsiniz (C dili için):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Temiz bir C ++ çözümü için, bir execute yöntemi içeren ve adımlarınızı nesnelerde saran bir arabirim sınıfı oluşturmalısınız .
Sonra, yukarıdaki çözüm şöyle görünecektir:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

Bireysel koşul değişkenlerine ihtiyacınız olmadığını varsayarsak, testleri tersine çevirin ve else-falthrough'u "ok" yolu olarak kullanmak, if / else ifadelerinin daha dikey bir kümesini elde etmenizi sağlar:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Başarısız değişkenin atlanması, kodu biraz fazla belirsiz IMO yapar.

İçindeki değişkenlerin iyi olduğunu beyan etmek, = vs == konusunda endişelenmenize gerek yok.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Bu belirsiz, ancak kompakt:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

Bu, bir durum modeliyle kolayca uygulayabileceğiniz için kullanışlı bir durum makinesine benziyor .

Java'da şöyle görünecektir:

interface StepState{
public StepState performStep();
}

Bir uygulama aşağıdaki gibi çalışır:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

Ve bunun gibi. O zaman big if koşulunu aşağıdakilerle değiştirebilirsiniz:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

Rommik'ın belirttiği gibi, bunun için bir tasarım deseni uygulayabilirsiniz, ancak çağrıları zincirlemek istediğiniz için Strateji yerine Dekoratör desenini kullanırım. Kod basitse, o zaman iç içe geçmesini önlemek için güzel yapılandırılmış cevaplardan biri ile gider. Bununla birlikte, karmaşıksa veya dinamik zincirleme gerektiriyorsa, Dekoratör deseni iyi bir seçimdir. İşte bir yUML sınıf diyagramı :

yUML sınıf diyagramı

İşte örnek bir LinqPad C # programı:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

Tasarım kalıpları üzerine okunacak en iyi kitap IMHO, Head First Tasarım Kalıpları'dır .


Bunun Jefffrey'in cevabı gibi bir şeyden ne faydası var?
Dason

çok daha fazla değişime dayanıklıdır, gereksinimler değiştiğinde bu yaklaşım çok fazla alan bilgisi olmadan yönetilmesi daha kolaydır. Özellikle iç içe geçmiş bazı bölümlerin ne kadar derin ve uzun olabileceğini düşündüğünüzde. Hepsi çok kırılgan olabilir ve bu nedenle çalışmak için yüksek risk taşır. Beni yanlış anlamayın, bazı optimizasyon senaryoları bunu söküp ifs'e dönmenize neden olabilir, ancak zamanın% 99'u iyi. Ancak asıl mesele, o seviyeye geldiğinizde, sürdürülebilirliğe önem vermediğinizde performansa ihtiyacınız vardır.
John Nicholas

4

Birkaç cevap, özellikle ağ programlamasında birçok kez gördüğüm ve kullandığım bir deseni ima etti. Ağ yığınlarında, çoğu başarısız olabilir ve işlemi durduracak uzun bir istek dizisi vardır.

Ortak desen kullanmaktı do { } while (false);

Bunu while(false)yapmak için bir makro kullandım do { } once;Ortak desen:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Bu paternin okunması nispeten kolaydı ve düzgün bir şekilde tahrip olacak ve aynı zamanda adım atmayı ve hata ayıklamayı biraz daha kolay hale getiren birden fazla geri dönüşü önleyen nesnelerin kullanılmasına izin verdi.


4

Mathieu'nın C ++ 11 cevabını geliştirmek ve kullanımı ile ortaya çıkan çalışma zamanı maliyetini önlemek için std::function, aşağıdakileri kullanmanızı öneririm

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Bu basit şablon sınıfı, herhangi bir parametre olmadan çağrılabilen herhangi bir işlevi kabul eder ve bunu herhangi bir dinamik bellek ayırması olmadan yapar ve bu nedenle C ++ 'ın gereksiz ek yük olmadan soyutlama hedefine daha iyi uyar. Ek fonksiyon şablonu, şablon parametre kesinti (sınıf şablonu parametreleri için mevcut değildir) ile kullanımı basitleştirmek için vardır.

Kullanım örneği:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Tıpkı Mathieu'nun cevabı gibi bu çözüm tamamen istisna güvenlidir ve executeThisFunctionInAnyCaseher durumda çağrılacaktır. Meli executeThisFunctionInAnyCasekendisi atmak ve yok ediciler örtülü işaretlenir noexceptve bu nedenle bir çağrı std::terminatebir istisna yayınlanan yerine neden olacağını yığın çözme işlemi sırasında atılmasına.


+1 Bu cevabı arıyordum, bu yüzden göndermek zorunda kalmam. Yapıcıda mükemmel ilerlemelisiniz functor, deferredzorlamaya gerek yok a move.
Yakk - Adam Nevraumont

@Yakk yapıcıyı bir sevkıyat yapıcısı olarak değiştirdi
Joe

3

Tüm çağrınızı tek bir bloktan yapmak istediğiniz anlaşılıyor. Diğerlerinin önerdiği gibi, bir whiledöngü ve kullanarak breakbırakmanız veya bırakabileceğiniz yeni bir işlev kullanmalısınız return(daha temiz olabilir).

İşlevden gotoçıkmak için bile şahsen yasaklıyorum . Hata ayıklama sırasında fark etmek daha zordur.

İş akışınız için çalışması gereken zarif bir alternatif, bir işlev dizisi oluşturmak ve bu dizini yinelemektir.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

Neyse ki, hata ayıklayıcı onları sizin için bulur. Hata ayıklama yapıyorsanız ve kodda tek adım atmıyorsanız, yanlış yapıyorsunuz demektir.
Cody Gray

Ne demek istediğini anlamıyorum, neden tek adım kullanamadım?
AxFab

3

Çünkü ayrıca [... kod bloğu ...] yürütmeler arasında , bellek ayırma veya nesne başlatma var sanırım. Bu şekilde, çıkışta zaten başlattığınız tüm temizliği önemsemelisiniz ve ayrıca problemle karşılaşırsanız ve işlevlerden herhangi biri yanlış döndürürse temizlemelisiniz.

Bu durumda, deneyimimde en iyi olanım (CryptoAPI ile çalışırken) küçük sınıflar oluşturmaktı, yapıcıda verilerinizi başlattınız, yıkıcıda onu başlattınız. Her bir sonraki işlev sınıfı, önceki işlev sınıfının alt öğesi olmalıdır. Bir şeyler ters giderse - istisna atın.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

Ve sonra kodunuzda aramanız yeterlidir:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Her ConditionX çağrısı bir şey, bellek ve benzeri şeyler başlatırsa en iyi çözüm sanırım. Her şeyin temizleneceğinden emin olmak en iyisidir.


3

ilginç bir yol istisnalarla çalışmaktır.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Böyle bir kod yazarsanız, bir şekilde yanlış yönde gidiyorsunuz demektir. Ben böyle bir kodu var, ama böyle dağınık bir "mimarisi" için "sorun" olarak görmeyeceğim.

İpucu: Bu vakaları güvendiğiniz deneyimli bir geliştiriciyle tartışın ;-)


Bence bu fikir her if-zincirinin yerini alamaz. Her neyse, birçok durumda, bu çok iyi bir yaklaşım!
WoIIe

3

Başka bir yaklaşım - do - whiledöngü, daha önce bahsedilmiş olmasına rağmen, nasıl göründüğünü gösteren bir örnek yoktu:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

( whileDöngü ile zaten bir cevap var, ancak do - whiledöngü gereksiz olarak doğru (başlangıçta) kontrol etmiyor, bunun yerine xD sonunda (bu atlanabilir).


Hey Zaffy - bu cevap do {} while (false) yaklaşımının büyük bir açıklamasına sahiptir. stackoverflow.com/a/24588605/294884 Başka iki cevap daha belirtti.
Fattie

Bu durumda CONTINUE veya BREAK kullanmanın daha zarif olup olmadığı büyüleyici bir soru!
Fattie

Hey @JoeBlow Tüm cevapları gördüm ... sadece bunun hakkında konuşmak yerine göstermek istedim :)
Zaffy

Buradaki ilk cevabım "Kimse
BU'dan

@JoeBlow Eh, haklısın. Bunu düzeltmeye çalışacağım. Hissediyorum ... xD Yine de teşekkür ederim, bir dahaki sefere ben biraz daha fazla dikkat ödeyecek :)
Zaffy
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.