Bagaimana cara menghindari rantai "jika"?


266

Dengan asumsi saya punya pseudo-code ini:

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

executeThisFunctionInAnyCase();

Fungsi executeStepXharus dijalankan jika dan hanya jika sebelumnya berhasil. Bagaimanapun, executeThisFunctionInAnyCasefungsi tersebut harus dipanggil di bagian akhir. Saya seorang pemula dalam pemrograman, sangat menyesal untuk pertanyaan yang sangat mendasar: apakah ada cara (dalam C / C ++ misalnya) untuk menghindari ifrantai panjang yang menghasilkan semacam "piramida kode", dengan mengorbankan keterbacaan kode ?

Saya tahu bahwa jika kita dapat melewati executeThisFunctionInAnyCasepemanggilan fungsi, kode dapat disederhanakan sebagai:

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

Tetapi kendala adalah executeThisFunctionInAnyCasepemanggilan fungsi. Bisakah breakpernyataan itu digunakan dengan cara tertentu?


254
@ FrédéricHamidi salah salah salah! Jangan pernah mengatakan bahwa mengarahkan aliran program Anda dengan pengecualian itu baik! Pengecualian pasti TIDAK cocok untuk tujuan ini, karena terlalu banyak alasan.
Piotr Zierhoffer

26
@Piotr, saya dimanjakan oleh Python (yang sebenarnya mendorong ini). Saya tahu pengecualian tidak seharusnya digunakan untuk kontrol aliran di C ++, tetapi apakah ini benar-benar kontrol aliran di sini? Tidak bisakah fungsi yang dikembalikan falsedianggap sebagai situasi yang luar biasa?
Frédéric Hamidi

13
Itu tergantung pada semantik program. Sebuah falsekembali dapat cukup normal.
dornhege

28
Saya telah memutar kembali pertanyaan Anda ke revisi pertama. Anda tidak boleh mengubah pertanyaan Anda secara radikal setelah menerima sejumlah pertanyaan (> 0), karena itu akan membatalkan semua jawaban yang diberikan hingga saat itu dan akan membuat kebingungan. Buka pertanyaan baru sebagai gantinya.
Sepatu

13
Saya berharap semua "programmer pemula" akan mengajukan pertanyaan desain seperti ini.
Jezen Thomas

Jawaban:


486

Anda dapat menggunakan &&(logika AND):

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

ini akan memenuhi kedua kebutuhan Anda:

  • executeStep<X>()harus mengevaluasi hanya jika yang sebelumnya berhasil (ini disebut evaluasi hubung singkat )
  • executeThisFunctionInAnyCase() akan dieksekusi dalam hal apa pun

29
Ini secara semantik benar (memang, kami ingin SEMUA kondisi menjadi benar) serta teknik pemrograman yang sangat baik (evaluasi hubung singkat). Selain itu, ini dapat digunakan dalam situasi kompleks di mana membuat fungsi akan mengacaukan kode.
Sanchises

58
@RobAu: Maka akan baik bagi para programmer junior untuk akhirnya melihat kode yang bergantung pada evaluasi jalan pintas dan dapat mendorong mereka untuk meneliti topik ini yang pada gilirannya akan membantu mereka dalam perjalanan untuk akhirnya menjadi programmer senior. Jadi jelas win-win: kode yang layak dan seseorang belajar sesuatu dari membacanya.
x4u

24
Ini harus menjadi jawaban teratas
Nama Tampilan

61
@RobAu: Ini bukan hack mengambil keuntungan dari beberapa trik sintaksis yang tidak jelas, ini sangat idiomatis di hampir setiap bahasa pemrograman, sampai-sampai menjadi praktik standar yang tidak bisa dibantah.
BlueRaja - Danny Pflughoeft

38
Solusi ini hanya berlaku jika kondisi sebenarnya yang disebut fungsi sederhana. Dalam kode sebenarnya, syarat-syarat itu mungkin 2-5 baris (dan itu sendiri merupakan kombinasi dari banyak lainnya &&dan ||) sehingga tidak ada cara Anda ingin bergabung dengan mereka menjadi satu ifpernyataan tanpa merusak keterbacaan. Dan tidak selalu mudah untuk memindahkan kondisi tersebut ke fungsi luar, karena mereka mungkin bergantung pada banyak variabel lokal yang sebelumnya dihitung, yang akan membuat kekacauan yang mengerikan jika Anda mencoba untuk melewati masing-masing sebagai argumen individu.
hamstergene

358

Cukup gunakan fungsi tambahan untuk membuat versi kedua Anda berfungsi:

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

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

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

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

Menggunakan ifs yang sangat bersarang (varian pertama Anda) atau keinginan untuk keluar dari "bagian dari fungsi" biasanya berarti Anda memang membutuhkan fungsi tambahan.


51
+1 akhirnya seseorang mengirim jawaban yang masuk akal. Menurut saya ini adalah cara yang paling benar, aman, dan mudah dibaca.
Lundin

31
+1 Ini adalah ilustrasi yang bagus dari "Prinsip Tanggung Jawab Tunggal". Fungsinya foobekerja melalui serangkaian kondisi dan tindakan terkait. Fungsi bardipisahkan dengan bersih dari keputusan. Jika kita melihat detail dari kondisi dan tindakan, mungkin ternyata foomasih melakukan terlalu banyak, tetapi untuk sekarang ini adalah solusi yang baik.
GranitRobert

13
Kelemahannya adalah bahwa C tidak memiliki fungsi bersarang jadi jika 3 langkah itu diperlukan untuk menggunakan variabel dari barAnda harus secara manual meneruskannya ke foosebagai parameter. Jika itu yang terjadi dan jika foohanya dipanggil sekali saya akan melakukan kesalahan terhadap menggunakan versi goto untuk menghindari mendefinisikan dua fungsi yang digabungkan secara ketat yang pada akhirnya tidak menjadi sangat dapat digunakan kembali.
hugomg

7
Tidak yakin dengan sintaks hubungan pendek untuk C, tetapi dalam C # foo () dapat ditulis sebagaiif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Goto digunakan sepanjang waktu, terutama dalam pemrograman sistem ketika Anda perlu melepas banyak kode pengaturan.
Scotty Bauer

166

Pemrogram C sekolah lama menggunakan gotodalam kasus ini. Ini adalah salah satu penggunaan gotoyang sebenarnya didorong oleh styleguide Linux, itu disebut fungsi keluar terpusat:

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

Beberapa orang bekerja menggunakan gotodengan membungkus tubuh menjadi satu lingkaran dan melepaskannya, tetapi secara efektif kedua pendekatan melakukan hal yang sama. The gotopendekatan yang lebih baik jika Anda membutuhkan beberapa pembersihan lainnya hanya jika executeStepA()itu sukses:

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

Dengan pendekatan loop Anda akan berakhir dengan dua tingkat loop dalam kasus itu.


108
+1. Banyak orang melihat gotodan langsung berpikir "Ini kode yang mengerikan" tetapi memang memiliki kegunaan yang valid.
Graeme Perrow

46
Kecuali ini sebenarnya kode yang cukup berantakan, dari sudut pandang pemeliharaan. Terutama ketika ada banyak label, di mana kode juga sulit dibaca. Ada cara yang lebih elegan untuk mencapai ini: gunakan fungsi.
Lundin

29
-1 Saya melihat gotodan saya bahkan tidak perlu berpikir untuk melihat bahwa ini adalah kode yang mengerikan. Saya pernah harus memelihara itu, itu sangat jahat. OP menyarankan alternatif bahasa C yang masuk akal di akhir pertanyaan, dan saya memasukkannya dalam jawaban saya.
Ceria dan hth. - Alf

56
Tidak ada yang salah dengan penggunaan goto yang terbatas dan mandiri ini. Namun berhati-hatilah, goto adalah obat gerbang dan jika Anda tidak hati-hati suatu hari Anda akan menyadari bahwa tidak ada orang lain yang makan spageti dengan kaki mereka, dan Anda sudah melakukannya selama 15 tahun mulai setelah Anda berpikir "Saya bisa memperbaikinya jika tidak, bug mimpi buruk dengan label ... "
Tim Post

66
Saya telah menulis mungkin ratusan ribu baris kode yang sangat jelas, mudah dirawat dengan gaya ini. Dua hal utama yang perlu dipahami adalah (1) disiplin! menetapkan pedoman yang jelas untuk tata letak setiap fungsi dan hanya melanggarnya dengan kebutuhan ekstrem, dan (2) memahami bahwa apa yang kami lakukan di sini adalah mensimulasikan pengecualian dalam bahasa yang tidak memilikinya . throwdalam banyak hal lebih buruk daripada gotokarena dengan throwitu bahkan tidak jelas dari konteks lokal di mana Anda akan berakhir! Gunakan pertimbangan desain yang sama untuk aliran kontrol gaya-goto seperti halnya pengecualian.
Eric Lippert

131

Ini adalah situasi yang umum dan ada banyak cara umum untuk menghadapinya. Inilah upaya saya untuk jawaban kanonik. Beri komentar jika saya melewatkan sesuatu dan saya akan terus memperbarui postingan ini.

Ini adalah Panah

Apa yang Anda diskusikan dikenal sebagai panah anti-pola . Disebut panah karena rantai ifs bersarang membentuk blok kode yang meluas lebih jauh ke kanan dan kemudian kembali ke kiri, membentuk panah visual yang "menunjuk" ke sisi kanan panel editor kode.

Ratakan Panah dengan Penjaga

Beberapa cara umum untuk menghindari Panah dibahas di sini . Metode yang paling umum adalah dengan menggunakan pola penjaga , di mana kode menangani aliran pengecualian pertama dan kemudian menangani aliran dasar, misalnya alih-alih

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

... Anda akan menggunakan ....

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

Ketika ada serangkaian panjang penjaga, ini meratakan kode karena semua penjaga muncul jauh ke kiri dan jika Anda tidak bersarang. Selain itu, Anda secara visual memasangkan kondisi logika dengan kesalahan yang terkait, yang membuatnya jauh lebih mudah untuk mengetahui apa yang sedang terjadi:

Panah:

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

Menjaga:

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

Ini secara obyektif dan kuantitatif lebih mudah dibaca karena

  1. Karakter {dan} untuk blok logika yang diberikan lebih dekat bersama-sama
  2. Jumlah konteks mental yang dibutuhkan untuk memahami garis tertentu lebih kecil
  3. Keseluruhan logika yang terkait dengan kondisi if lebih cenderung berada pada satu halaman
  4. Kebutuhan bagi pembuat kode untuk menggulir halaman / jalur mata sangat berkurang

Cara menambahkan kode umum di akhir

Masalah dengan pola penjaga adalah bahwa ia bergantung pada apa yang disebut "pengembalian oportunistik" atau "keluar oportunistik." Dengan kata lain, itu memecah pola bahwa setiap fungsi harus memiliki tepat satu titik keluar. Ini masalah karena dua alasan:

  1. Itu menggosok beberapa orang dengan cara yang salah, misalnya orang yang belajar kode pada Pascal telah belajar bahwa satu fungsi = satu titik keluar.
  2. Itu tidak menyediakan bagian kode yang dieksekusi saat keluar tidak peduli apa , yang merupakan subjek di tangan.

Di bawah ini saya telah menyediakan beberapa opsi untuk mengatasi keterbatasan ini baik dengan menggunakan fitur bahasa atau dengan menghindari masalah sama sekali.

Opsi 1. Anda tidak dapat melakukan ini: gunakan finally

Sayangnya, sebagai pengembang c ++, Anda tidak dapat melakukan ini. Tetapi ini adalah jawaban nomor satu untuk bahasa-bahasa yang mengandung kata kunci terakhir, karena inilah yang tepat untuk itu.

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

Opsi 2. Hindari masalah: Merestrukturisasi fungsi Anda

Anda dapat menghindari masalah dengan memecah kode menjadi dua fungsi. Solusi ini memiliki manfaat bekerja untuk bahasa apa pun, dan selain itu dapat mengurangi kompleksitas siklomatik , yang merupakan cara yang terbukti untuk mengurangi tingkat cacat Anda, dan meningkatkan kekhususan setiap tes unit otomatis.

Ini sebuah contoh:

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

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

Opsi 3. Trik bahasa: Gunakan loop palsu

Trik umum lainnya yang saya lihat adalah menggunakan while (true) dan break, seperti yang ditunjukkan pada jawaban lain.

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

Meskipun ini kurang "jujur" daripada menggunakan goto, itu kurang cenderung kacau ketika refactoring, karena jelas menandai batas-batas ruang lingkup logika. Seorang programmer naif yang memotong dan menempelkan label Anda atau gotopernyataan Anda dapat menyebabkan masalah besar! (Dan terus terang polanya sangat umum sekarang saya pikir itu jelas mengomunikasikan maksud, dan karena itu tidak "tidak jujur" sama sekali).

Ada varian lain dari opsi ini. Sebagai contoh, seseorang bisa menggunakan switchbukan while. Konstruk bahasa apa pun dengan breakkata kunci mungkin akan berfungsi.

Opsi 4. Memanfaatkan siklus hidup objek

Satu pendekatan lain memanfaatkan siklus hidup objek. Gunakan objek konteks untuk membawa parameter Anda (sesuatu yang kurang naif dari contoh mencurigakan kami) dan buang ketika Anda selesai.

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
}

Catatan: Pastikan Anda memahami siklus hidup objek bahasa pilihan Anda. Anda memerlukan semacam pengumpulan sampah deterministik agar ini berfungsi, yaitu Anda harus tahu kapan destruktor akan dipanggil. Dalam beberapa bahasa Anda harus menggunakan Disposealih-alih destruktor.

Opsi 4.1. Memanfaatkan siklus hidup objek (pola pembungkus)

Jika Anda akan menggunakan pendekatan berorientasi objek, sebaiknya lakukan dengan benar. Opsi ini menggunakan kelas untuk "membungkus" sumber daya yang memerlukan pembersihan, serta operasi lainnya.

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

Sekali lagi, pastikan Anda memahami siklus hidup objek Anda.

Opsi 5. Trik bahasa: Gunakan evaluasi hubung singkat

Teknik lain adalah memanfaatkan evaluasi hubung singkat .

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

Solusi ini memanfaatkan cara kerja operator &&. Ketika sisi kiri && dievaluasi salah, sisi kanan tidak pernah dievaluasi.

Trik ini paling berguna ketika kode kompak diperlukan dan ketika kode tidak mungkin melihat banyak pemeliharaan, misalnya Anda menerapkan algoritma yang terkenal. Untuk pengkodean yang lebih umum, struktur kode ini terlalu rapuh; bahkan perubahan kecil pada logika dapat memicu penulisan ulang total.


12
Akhirnya? C ++ tidak memiliki klausa akhirnya. Objek dengan destruktor digunakan dalam situasi di mana Anda pikir Anda membutuhkan klausa akhirnya.
Bill Door

1
Diedit untuk mengakomodasi dua komentar di atas.
John Wu

2
Dengan kode trivial (misalnya dalam contoh saya) pola bersarang mungkin lebih mudah dipahami. Dengan kode dunia nyata (yang dapat menjangkau beberapa halaman), pola pelindung secara obyektif lebih mudah dibaca karena akan membutuhkan lebih sedikit pengguliran dan pelacakan mata, misalnya jarak rata-rata dari {ke} lebih pendek.
John Wu

1
Saya telah melihat pola bersarang di mana kode tidak terlihat lagi pada layar 1920 x 1080 ... Coba cari tahu kode penanganan kesalahan apa yang akan dilakukan jika tindakan ketiga gagal ... Saya telah menggunakan do {...} saat (0) sebagai gantinya sehingga Anda tidak perlu istirahat terakhir (di sisi lain, sementara (benar) {...} memungkinkan "lanjutkan" untuk memulai dari awal lagi.
gnasher729

2
Opsi Anda 4 adalah kebocoran memori dalam C ++ sebenarnya (mengabaikan kesalahan sintaks minor). Orang tidak menggunakan "baru" dalam kasus seperti ini, cukup katakan "MyContext myContext;".
Sumudu Fernando

60

Kerjakan saja

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

Sesederhana itu.


Karena tiga pengeditan yang masing-masing telah secara mendasar mengubah pertanyaan (empat jika seseorang menghitung revisi kembali ke versi # 1), saya menyertakan contoh kode yang saya jawab:

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

executeThisFunctionInAnyCase();

14
Saya menjawab ini (lebih ringkas) sebagai tanggapan terhadap versi pertama dari pertanyaan, dan mendapat 20 upvotes sebelum dihapus oleh Bill the Lizard, setelah beberapa komentar dan penyuntingan oleh Darkness Races di Orbit.
Ceria dan hth. - Alf

1
@ Cheersandhth-Alf: Saya tidak percaya itu mod-dihapus. Sayang sekali. (+1)
The Paramagnetic Croissant

2
+1 saya untuk keberanian Anda! (3 kali penting: D).
pukul

Sangat penting bagi programmer pemula untuk mempelajari hal-hal seperti eksekusi-order dengan banyak "ands" boolean, bagaimana hal itu berbeda dalam berbagai bahasa, dll. Dan jawaban ini adalah petunjuk yang bagus untuk itu. Tapi itu hanya "non-starter" dalam pemrograman komersial normal. Ini tidak sederhana, tidak dapat dipelihara, tidak dapat dimodifikasi. Tentu, jika Anda hanya menulis "kode koboi" cepat untuk diri sendiri, lakukan ini. Tetapi hanya memiliki semacam "tidak ada koneksi ke rekayasa perangkat lunak sehari-hari seperti yang dipraktikkan hari ini." BTW maaf untuk "edit-kebingungan" konyol Anda harus bertahan di sini, @cheers :)
Fattie

1
@ JoBlow: Saya dengan Alf dalam hal ini - saya menemukan &&daftar lebih jelas. Saya biasanya memecah kondisi pada baris yang terpisah dan menambahkan trailing // explanationke masing-masing .... Pada akhirnya, itu adalah kode yang lebih sedikit untuk dilihat, dan setelah Anda memahami cara &&kerjanya tidak ada upaya mental yang berkelanjutan. Kesan saya adalah bahwa sebagian besar programmer C ++ profesional akan terbiasa dengan ini, tetapi seperti yang Anda katakan di industri / proyek yang berbeda fokus dan pengalaman berbeda.
Tony Delroy

35

Sebenarnya ada cara untuk menunda tindakan dalam C ++: memanfaatkan destruktor objek.

Dengan asumsi bahwa Anda memiliki akses ke C ++ 11:

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

Dan kemudian menggunakan utilitas itu:

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
Ini hanya kebingungan bagi saya. Saya benar-benar tidak mengerti mengapa begitu banyak programmer C ++ suka memecahkan masalah dengan melemparkan sebanyak mungkin fitur bahasa, sampai pada titik di mana semua orang lupa tentang masalah apa yang sebenarnya Anda pecahkan: mereka tidak lagi peduli, karena mereka sekarang sangat tertarik untuk menggunakan semua fitur bahasa yang eksotis. Dan sejak saat itu, Anda dapat membuat diri Anda sibuk selama berhari-hari dan berminggu-minggu menulis kode meta, kemudian mempertahankan kode meta, kemudian menulis kode meta untuk menangani kode meta.
Lundin

24
@Lundin: Ya, saya tidak mengerti bagaimana orang bisa puas dengan kode rapuh yang akan pecah segera setelah melanjutkan / istirahat / kembali awal diperkenalkan atau pengecualian dilemparkan. Di sisi lain, solusi ini tangguh dalam menghadapi pemeliharaan di masa depan, dan hanya bergantung pada kenyataan bahwa destruktor dijalankan selama unwinding yang merupakan salah satu fitur paling penting dari C ++. Memenuhi syarat eksotis ini sementara prinsip dasar yang memberdayakan semua wadah Standar lucu, untuk sedikitnya.
Matthieu M.

7
@Lundin: Manfaat dari kode Matthieu adalah bahwa executeThisFunctionInAnyCase();akan mengeksekusi bahkan jika foo();melempar pengecualian. Saat menulis kode pengecualian-aman, praktik yang baik untuk meletakkan semua fungsi pembersihan seperti itu di destruktor.
Brian

6
@ Brian Kemudian jangan lempar pengecualian foo(). Dan jika Anda melakukannya, tangkap. Masalah terpecahkan. Perbaiki bug dengan memperbaikinya, bukan dengan menulis cara mengatasi.
Lundin

18
@Lundin: Deferkelas adalah sepotong kecil kode yang dapat digunakan kembali yang memungkinkan Anda melakukan pembersihan di akhir blok, dengan pengecualian cara yang aman. itu lebih dikenal sebagai pelindung lingkup . ya, setiap penggunaan pelindung ruang lingkup dapat diekspresikan dengan cara manual lainnya, seperti halnya setiap forloop dapat dinyatakan sebagai blok dan whileloop, yang pada gilirannya dapat diekspresikan dengan ifdan goto, yang dapat diekspresikan dalam bahasa assembly jika Anda mau, atau bagi mereka yang benar-benar tuan, dengan mengubah bit dalam ingatan melalui sinar kosmik yang diarahkan oleh efek kupu-kupu dari dengungan dan nyanyian pendek khusus. tapi, mengapa melakukan itu.
Ceria dan hth. - Alf

34

Ada teknik bagus yang tidak memerlukan fungsi pembungkus tambahan dengan pernyataan pengembalian (metode yang ditentukan oleh Itjax). Itu menggunakan do while(0)pseudo-loop. Itu while (0)memastikan bahwa itu sebenarnya bukan loop tetapi dieksekusi hanya sekali. Namun, sintaks loop memungkinkan penggunaan pernyataan break.

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

11
Menurut saya, menggunakan fungsi dengan pengembalian banyak adalah serupa, tetapi lebih mudah dibaca.
Lundin

4
Ya, pasti itu lebih mudah dibaca ... namun dari sudut pandang efisiensi, pemanggilan fungsi tambahan (parameter passing dan return) overhead dapat dihindari dengan do {} while (0) construct.
Debasis

5
Anda masih bebas membuat fungsinya inline. Bagaimanapun, ini adalah teknik yang bagus untuk diketahui, karena ini membantu lebih dari masalah ini.
Ruslan

2
@Lundin Anda harus memperhitungkan lokalitas kode, menyebarkan kode ke terlalu banyak tempat memiliki masalah juga.
API-Beast

3
Dalam pengalaman saya, ini adalah ungkapan yang sangat tidak biasa. Butuh beberapa saat untuk mencari tahu apa yang sedang terjadi, dan itu pertanda buruk ketika saya meninjau kode. Mengingat tampaknya tidak memiliki keunggulan dibandingkan yang lain, lebih umum dan karenanya lebih mudah dibaca, saya tidak bisa menandatanganinya.
Cody Grey

19

Anda juga bisa melakukan ini:

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

Dengan cara ini Anda memiliki ukuran pertumbuhan linier minimal, +1 saluran per panggilan, dan mudah dikelola.


EDIT : (Terima kasih @Unda) Bukan penggemar besar karena Anda kehilangan visibilitas 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
Itu tentang panggilan fungsi yang tidak disengaja di dalam push_back (), tetapi Anda tetap memperbaikinya :)
Quentin

4
Saya ingin tahu mengapa ini mendapat downvote. Dengan asumsi langkah-langkah eksekusi memang simetris, seperti yang terlihat, maka itu bersih dan dapat dipertahankan.
ClickRick

1
Meskipun ini bisa dibilang terlihat lebih bersih. Mungkin lebih sulit untuk dipahami orang, dan jelas lebih sulit untuk dipahami bagi penyusun!
Roy T.

1
Memperlakukan fungsi Anda lebih banyak karena data seringkali merupakan ide yang bagus - setelah Anda melakukannya, Anda juga akan melihat refactoring berikutnya. Bahkan lebih baik adalah di mana setiap bagian yang Anda tangani adalah referensi objek, bukan hanya referensi fungsi - ini akan memberi Anda lebih banyak kemampuan untuk meningkatkan kode Anda di telepon.
Bill K

3
Sedikit over-engineered untuk kasus sepele, tetapi teknik ini pasti memiliki fitur bagus yang tidak dimiliki orang lain: Anda dapat mengubah urutan eksekusi dan [jumlah] fungsi yang dipanggil saat runtime, dan itu bagus :)
Xocoatzin

18

Apakah ini akan berhasil? Saya pikir ini setara dengan kode Anda.

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

3
Saya biasanya memanggil variabel okketika menggunakan variabel yang sama seperti ini.
Macke

1
Saya akan sangat tertarik untuk mengetahui mengapa downvotes. Apa yang salah di sini?
ABCplus

1
bandingkan jawaban Anda dengan pendekatan hubung singkat untuk kompleksitas siklomatik.
AlphaGoku

14

Dengan asumsi kode yang diinginkan adalah seperti yang saya lihat saat ini:

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

Saya akan mengatakan bahwa pendekatan yang benar, dalam hal ini adalah yang paling sederhana untuk dibaca dan paling mudah dipertahankan, akan memiliki tingkat lekukan yang lebih sedikit, yang (saat ini) merupakan tujuan dari pertanyaan yang dinyatakan.

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

Ini menghindari apa pun kebutuhan gotos, pengecualian, boneka whileloop, atau konstruksi sulit lainnya dan hanya mendapat di dengan pekerjaan sederhana di tangan.


Saat menggunakan loop, biasanya dapat diterima untuk menggunakan returndan breakmelompat keluar dari loop tanpa perlu memperkenalkan variabel "flag" tambahan. Dalam hal ini, menggunakan goto akan sama innocuou - ingat bahwa Anda berdagang kompleksitas goto ekstra untuk kompleksitas variabel ekstra yang bisa berubah.
hugomg

2
@hugomg Variabel-variabel tersebut ada di pertanyaan awal. Tidak ada kompleksitas tambahan di sini. Ada asumsi yang dibuat tentang pertanyaan (misalnya bahwa variabel perlu dalam kode redacted) sehingga mereka dipertahankan. Jika tidak diperlukan maka kode dapat disederhanakan, tetapi mengingat sifat pertanyaan yang tidak lengkap tidak ada asumsi lain yang dapat dibuat secara sah.
ClickRick

Pendekatan yang sangat berguna, khususnya. untuk digunakan oleh seorang yang mengaku diri newbie, ia memberikan solusi yang lebih bersih tanpa kekurangan. Saya perhatikan juga tidak tidak bergantung pada stepsmemiliki tanda tangan yang sama atau bahkan menjadi fungsi daripada blok. Saya bisa melihat ini digunakan sebagai refactor lulus pertama bahkan di mana pendekatan yang lebih canggih valid.
Keith

12

Bisakah break statement digunakan dengan cara tertentu?

Mungkin bukan solusi terbaik tetapi Anda bisa membuat pernyataan dalam satu do .. while (0)lingkaran dan menggunakan breakpernyataan return.


2
Bukan saya yang menurunkannya, tapi ini akan menjadi penyalahgunaan konstruksi berulang untuk sesuatu di mana efeknya adalah apa yang diinginkan saat ini tetapi pasti akan menghasilkan rasa sakit. Mungkin untuk pengembang berikutnya yang harus memeliharanya dalam waktu 2 tahun setelah Anda pindah ke proyek lain.
ClickRick

3
@ClickRick gunakan do .. while (0)untuk definisi makro juga menyalahgunakan loop tetapi dianggap OK.
ouah

1
Mungkin, tetapi ada cara yang lebih bersih untuk mencapainya.
ClickRick

4
@ClickRick satu-satunya tujuan jawaban saya adalah untuk menjawab. Bisa mematahkan pernyataan yang digunakan dalam beberapa cara dan jawabannya adalah ya, kata-kata pertama dalam jawaban saya menunjukkan ini mungkin bukan solusi untuk digunakan.
ouah

2
Jawaban ini seharusnya menjadi komentar
msmucker0527

12

Anda bisa meletakkan semua ifkondisi, diformat seperti yang Anda inginkan dalam fungsi mereka sendiri, yang kembali menjalankan executeThisFunctionInAnyCase()fungsi.

Dari contoh dasar dalam OP, pengujian dan eksekusi kondisi dapat dipisahkan seperti itu;

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

Dan kemudian disebut seperti itu;

InitialSteps();
executeThisFunctionInAnyCase();

Jika C ++ 11 lambdas tersedia (tidak ada tag C ++ 11 di OP, tetapi mereka masih bisa menjadi pilihan), maka kita bisa melupakan fungsi terpisah dan membungkusnya menjadi lambda.

// 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

Jika Anda tidak suka gotodan tidak suka do { } while (0);loop dan suka menggunakan C ++ Anda juga dapat menggunakan lambda sementara untuk memiliki efek yang sama.

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

executeThisFunctionInAnyCase();

1
if Anda tidak menyukai goto, && Anda tidak suka melakukan {} sementara (0) && Anda menyukai C ++ ... Maaf, tidak bisa menolak, tetapi kondisi terakhir itu gagal karena pertanyaannya ditandai c serta c ++
ClickRick

@ClickRick selalu sulit untuk menyenangkan semua orang. Menurut pendapat saya tidak ada yang namanya C / C ++ Anda biasanya kode di salah satu dari mereka dan penggunaan yang lain mengerutkan kening.
Alex

9

Rantai IF / ELSE dalam kode Anda bukan masalah bahasa, tetapi desain program Anda. Jika Anda dapat menentukan ulang faktor atau menulis ulang program Anda, saya ingin menyarankan agar Anda mencari di Design Patterns ( http://sourcemaking.com/design_patterns ) untuk menemukan solusi yang lebih baik.

Biasanya, ketika Anda melihat banyak IF & lain dalam kode Anda, itu adalah kesempatan untuk menerapkan Pola Desain Strategi ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) atau mungkin kombinasi dari pola lain.

Saya yakin ada alternatif untuk menulis daftar panjang jika / yang lain, tapi saya ragu mereka akan mengubah apa pun kecuali bahwa rantai tersebut akan terlihat cantik untuk Anda (Namun, keindahan yang ada di mata yang melihatnya masih berlaku untuk kode juga :-)). Anda harus khawatir tentang hal-hal seperti (dalam 6 bulan ketika saya memiliki kondisi baru dan saya tidak ingat apa-apa tentang kode ini, apakah saya dapat menambahkannya dengan mudah? Atau bagaimana jika rantai berubah, seberapa cepat dan bebas dari kesalahan akankah saya menerapkannya)


9

Anda hanya melakukan ini ..

coverConditions();
executeThisFunctionInAnyCase();

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

99 kali dari 100, ini adalah satu-satunya cara untuk melakukannya.

Tidak pernah, pernah, mencoba melakukan sesuatu yang "rumit" dalam kode komputer.


Ngomong - ngomong, aku cukup yakin yang berikut ini adalah solusi aktual yang ada dalam pikiran Anda ...

The melanjutkan pernyataan sangat penting dalam pemrograman algoritmik. (Seperti halnya, pernyataan goto sangat penting dalam pemrograman algoritmik.)

Dalam banyak bahasa pemrograman Anda dapat melakukan ini:

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

(Catatan pertama-tama: blok telanjang seperti contoh itu adalah bagian penting dan penting dari penulisan kode yang indah, terutama jika Anda berurusan dengan pemrograman "algoritmik".)

Sekali lagi, itulah tepatnya yang ada di kepala Anda, bukan?Dan itulah cara yang indah untuk menulisnya, sehingga Anda memiliki naluri yang baik.

Namun, tragisnya, dalam versi obyektif-c saat ini (Selain - Saya tidak tahu tentang Swift, maaf) ada fitur yang dapat dilihat di mana ia memeriksa apakah blok penutup adalah sebuah loop.

masukkan deskripsi gambar di sini

Inilah cara Anda menyiasatinya ...

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

Jadi jangan lupakan itu ..

do {} while (false);

hanya berarti "lakukan blokir ini sekali".

yaitu, sama sekali tidak ada perbedaan antara menulis do{}while(false);dan hanya menulis {}.

Ini sekarang berfungsi sempurna seperti yang Anda inginkan ... inilah hasilnya ...

masukkan deskripsi gambar di sini

Jadi, mungkin saja itulah cara Anda melihat algoritme di kepala Anda. Anda harus selalu mencoba menulis apa yang ada di kepala Anda. (Terutama jika Anda tidak sadar, karena saat itulah yang cantik keluar! :))

Dalam proyek "algoritmik" di mana ini sering terjadi, dalam objektif-c, kami selalu memiliki makro seperti ...

#define RUNONCE while(false)

... jadi kamu bisa melakukan ini ...

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

Ada dua poin:

a, meskipun itu bodoh bahwa tujuan-c memeriksa jenis blok melanjutkan pernyataan, sulit untuk "melawan itu". Jadi ini keputusan yang sulit.

b, ada pertanyaan yang harus Anda indentasi, dalam contoh, blok itu? Saya kurang tidur karena pertanyaan seperti itu, jadi saya tidak bisa memberi saran.

Semoga ini bisa membantu.



Apakah Anda memasukkan karunia untuk mendapatkan lebih banyak perwakilan? :)
TMS

Alih-alih meletakkan semua komentar itu di if, Anda juga bisa menggunakan nama fungsi yang lebih deskriptif dan meletakkan komentar di fungsi.
Thomas Ahle

Yuck. Saya akan mengambil solusi 1 baris yang berbunyi singkat dengan evaluasi hubung singkat (yang telah dalam bahasa selama lebih dari 20 tahun dan terkenal) atas kekacauan ini setiap hari. Saya pikir kita berdua bisa sepakat bahwa kita senang tidak bekerja satu sama lain.
M2tM

8

Minta fungsi eksekusi Anda melempar pengecualian jika gagal alih-alih salah. Maka kode panggilan Anda dapat terlihat seperti ini:

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

Tentu saja saya berasumsi bahwa dalam contoh asli Anda langkah eksekusi hanya akan mengembalikan false jika terjadi kesalahan dalam langkah tersebut?


3
menggunakan pengecualian untuk mengontrol aliran sering dianggap sebagai praktik buruk dan kode bau
user902383

8

Sudah banyak jawaban yang bagus, tetapi sebagian besar dari mereka tampaknya mengimbangi beberapa (diakui sangat sedikit) dari fleksibilitas. Pendekatan umum yang tidak memerlukan tradeoff ini adalah menambahkan variabel status / terus-menerus . Harga tentu saja merupakan satu nilai ekstra untuk melacak:

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;

Kenapa ok &= conditionX;dan tidak sederhana ok = conditionX;?
ABCplus

@ user3253359 Dalam banyak kasus, ya, Anda bisa melakukannya. Ini adalah demo konseptual; dalam kode kerja kami akan mencoba menyederhanakannya sebanyak mungkin
blgt

+1 Salah satu dari beberapa jawaban bersih dan dapat dipelihara yang berfungsi dalam c , sebagaimana ditentukan dalam pertanyaan.
ClickRick

6

Di C ++ (pertanyaannya ditandai C dan C ++), jika Anda tidak dapat mengubah fungsi untuk menggunakan pengecualian, Anda masih dapat menggunakan mekanisme pengecualian jika Anda menulis fungsi pembantu kecil seperti

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

Maka kode Anda dapat dibaca sebagai berikut:

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

executeThisFunctionInAnyCase();

Jika Anda menyukai sintaksis mewah, Anda bisa membuatnya bekerja melalui pemeran eksplisit:

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

Kemudian Anda dapat menulis kode Anda sebagai

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

executeThisFunctionInAnyCase();

Refactoring nilai memeriksa ke dalam pengecualian tidak selalu merupakan cara yang baik untuk pergi, ada pengecualian overhead yang cukup banyak.
John Wu

4
-1 Menggunakan pengecualian untuk aliran normal di C ++ seperti ini adalah praktik pemrograman yang buruk. Dalam C ++ pengecualian harus disediakan untuk keadaan luar biasa.
Jack Aidley

1
Dari teks pertanyaan (penekanan oleh saya): "Fungsi executeStepX harus dieksekusi jika dan hanya jika sebelumnya berhasil. " Dengan kata lain, nilai kembali digunakan untuk menunjukkan kegagalan. Artinya, ini adalah penanganan kesalahan (dan orang akan berharap bahwa kegagalan itu luar biasa). Kesalahan penanganan persis pengecualian apa yang diciptakan untuk.
celtschk

1
Nggak. Pertama, pengecualian dibuat untuk memungkinkan penyebaran kesalahan , bukan penanganan kesalahan ; kedua, "Fungsi executeStepX harus dieksekusi jika dan hanya jika sebelumnya berhasil." tidak berarti bahwa boolean false dikembalikan oleh fungsi sebelumnya menunjukkan kasus yang jelas luar biasa / salah. Pernyataan Anda karenanya tidak berurutan . Penanganan kesalahan dan sanitasi aliran dapat diimplementasikan dengan banyak cara yang berbeda, pengecualian adalah alat yang memungkinkan penyebaran kesalahan dan penanganan kesalahan di tempat , dan unggul pada saat itu.

6

Jika kode Anda sesederhana contoh Anda dan bahasa Anda mendukung evaluasi hubung singkat, Anda dapat mencoba ini:

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

Jika Anda meneruskan argumen ke fungsi Anda dan mendapatkan kembali hasil lainnya sehingga kode Anda tidak dapat ditulis dengan cara sebelumnya, banyak dari jawaban lain akan lebih cocok untuk masalah tersebut.


Sebenarnya saya mengedit pertanyaan saya untuk lebih menjelaskan topik, tetapi ditolak untuk tidak membatalkan sebagian besar jawaban. : \
ABCplus

Saya pengguna baru di SO, dan programmer pemula. 2 pertanyaan kemudian: apakah ada risiko bahwa pertanyaan lain seperti yang Anda katakan akan ditandai sebagai digandakan karena pertanyaan INI? Poin lainnya adalah: bagaimana mungkin seorang pengguna SO pemula / programmer memilih jawaban terbaik di antara semuanya (hampir baik saya kira ..)?
ABCplus

6

Untuk C ++ 11 dan seterusnya, pendekatan yang bagus mungkin untuk mengimplementasikan sistem exit scope mirip dengan mekanisme scope (exit) D.

Salah satu cara yang mungkin untuk mengimplementasikannya adalah menggunakan C ++ 11 lambdas dan beberapa makro pembantu:

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

Ini akan memungkinkan Anda untuk kembali lebih awal dari fungsi dan memastikan kode pembersihan apa pun yang Anda tetapkan selalu dijalankan saat ruang lingkup keluar:

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

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Makro benar-benar hanya hiasan. MakeScopeExit()dapat digunakan secara langsung.


Tidak perlu makro untuk membuat ini berfungsi. Dan [=]biasanya salah untuk lambda yang dicakup.
Yakk - Adam Nevraumont

Ya, makro hanya untuk hiasan dan bisa dibuang. Tetapi tidakkah Anda mengatakan bahwa menangkap dengan nilai adalah pendekatan "generik" teraman?
Glampert

1
tidak: jika lambda Anda tidak akan bertahan di luar cakupan saat ini di mana lambda dibuat, gunakan [&]: itu aman, dan sedikit mengejutkan. Tangkap dengan nilai hanya ketika lambda (atau salinan) dapat bertahan lebih lama dari ruang lingkup pada titik deklarasi ...
Yakk - Adam Nevraumont

Ya, itu masuk akal. Saya akan mengubahnya. Terima kasih!
Glampert

6

Mengapa tidak ada yang memberikan solusi paling sederhana? : D

Jika semua fungsi Anda memiliki tanda tangan yang sama maka Anda dapat melakukannya dengan cara ini (untuk bahasa C):

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

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

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Untuk solusi C ++ yang bersih, Anda harus membuat kelas antarmuka yang berisi metode eksekusi dan membungkus langkah-langkah Anda dalam objek.
Maka, solusi di atas akan terlihat seperti ini:

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

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

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

executeThisFunctionInAnyCase();

5

Dengan asumsi Anda tidak memerlukan variabel kondisi individual, membalikkan tes dan menggunakan else-falthrough sebagai jalur "ok" akan memungkinkan Anda mendapatkan set pernyataan if / else yang lebih vertikal:

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

Menghilangkan variabel yang gagal membuat kode IMO agak terlalu kabur.

Mendeklarasikan variabel di dalamnya baik-baik saja, tidak perlu khawatir tentang = vs ==.

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

Ini tidak jelas, tetapi kompak:

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

runThisFunctionInAnyCase();

5

Ini terlihat seperti mesin keadaan, yang berguna karena Anda dapat dengan mudah menerapkannya dengan pola-negara .

Di Jawa akan terlihat seperti ini:

interface StepState{
public StepState performStep();
}

Suatu implementasi akan bekerja sebagai berikut:

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

Dan seterusnya. Maka Anda dapat mengganti yang besar jika kondisinya dengan:

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

5

Seperti yang disebutkan Rommik, Anda bisa menerapkan pola desain untuk ini, tapi saya akan menggunakan pola Dekorator daripada Strategi karena Anda ingin membuat panggilan. Jika kodenya sederhana, maka saya akan pergi dengan salah satu jawaban terstruktur dengan baik untuk mencegah bersarang. Namun, jika rumit atau memerlukan perangkaian dinamis, maka pola Penghias adalah pilihan yang baik. Berikut adalah diagram kelas yUML :

diagram kelas yUML

Berikut ini contoh program LinqPad C #:

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

Buku terbaik untuk membaca tentang pola desain, IMHO, adalah Pola Desain Kepala Pertama .


Apa manfaatnya dibandingkan jawaban Jefffrey?
Dason

jauh lebih tahan perubahan, ketika persyaratan mengubah pendekatan ini lebih mudah untuk mengelola tanpa banyak pengetahuan domain. Terutama ketika Anda mempertimbangkan seberapa dalam dan panjang beberapa bagian dari nested ifs bisa didapat. Itu semua bisa menjadi sangat rapuh dan karenanya berisiko tinggi untuk dikerjakan. Jangan salah paham beberapa skenario optimisasi dapat menyebabkan Anda merobek ini dan kembali ke ifs tetapi 99% dari waktu ini baik-baik saja. Tetapi intinya adalah ketika Anda mencapai tingkat itu Anda tidak peduli tentang pemeliharaan Anda membutuhkan kinerja.
John Nicholas

4

Beberapa jawaban mengisyaratkan pola yang saya lihat dan gunakan berkali-kali, terutama dalam pemrograman jaringan. Dalam tumpukan jaringan sering kali ada urutan permintaan yang panjang, yang salah satunya dapat gagal dan akan menghentikan proses.

Pola umum yang digunakan do { } while (false);

Saya menggunakan makro untuk while(false)membuatnya do { } once;Pola umum adalah:

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

Pola ini relatif mudah dibaca, dan memungkinkan objek untuk digunakan yang akan merusak dengan baik dan juga menghindari beberapa pengembalian membuat langkah dan debug sedikit lebih mudah.


4

Untuk meningkatkan jawaban Mathieu C ++ 11 dan menghindari biaya runtime yang timbul karena penggunaan std::function, saya akan menyarankan untuk menggunakan yang berikut ini

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

Kelas template sederhana ini akan menerima functor yang dapat dipanggil tanpa parameter apa pun, dan melakukannya tanpa alokasi memori dinamis dan karenanya lebih sesuai dengan tujuan abstraksi C ++ tanpa overhead yang tidak perlu. Template fungsi tambahan ada untuk menyederhanakan penggunaan dengan pengurangan parameter template (yang tidak tersedia untuk parameter template kelas)

Contoh penggunaan:

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

Sama seperti jawaban Mathieu, solusi ini sepenuhnya pengecualian aman, dan executeThisFunctionInAnyCaseakan dipanggil dalam semua kasus. Jika executeThisFunctionInAnyCaseitu sendiri melempar, destruktor secara implisit ditandai noexceptdan oleh karena itu panggilan untuk std::terminateakan dikeluarkan alih-alih menyebabkan pengecualian untuk dilemparkan selama tumpukan dibuka.


+1 Saya mencari jawaban ini sehingga saya tidak perlu mempostingnya. Anda harus memajukan-maju functordi deferredkonstruktor, tidak perlu memaksa a move.
Yakk - Adam Nevraumont

@Yakk mengubah konstruktor menjadi konstruktor penerusan
Joe

3

Sepertinya Anda ingin melakukan semua panggilan Anda dari satu blok. Seperti yang telah diusulkan orang lain, Anda harus menggunakan whileperulangan dan menggunakan breakatau fungsi baru yang dapat Anda tinggalkan return(mungkin lebih bersih).

Saya pribadi mengusir goto, bahkan untuk keluar fungsi. Mereka lebih sulit dikenali saat debugging.

Alternatif elegan yang harus bekerja untuk alur kerja Anda adalah membangun sebuah array fungsi dan beralih pada yang satu ini.

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

Untungnya, debugger menemukan mereka untuk Anda. Jika Anda men-debug dan tidak melangkah melalui kode, Anda salah melakukannya.
Cody Grey

Saya tidak mengerti apa yang Anda maksud, dan mengapa saya tidak bisa menggunakan langkah tunggal?
AxFab

3

Karena Anda juga memiliki [... blok kode ...] antara eksekusi, saya kira Anda memiliki alokasi memori atau inisialisasi objek. Dengan cara ini Anda harus peduli membersihkan semua yang sudah Anda inisialisasi saat keluar, dan juga membersihkannya jika Anda akan menemui masalah dan salah satu fungsi akan kembali salah.

Dalam hal ini, yang terbaik yang saya miliki dalam pengalaman saya (ketika saya bekerja dengan CryptoAPI) adalah membuat kelas kecil, di konstruktor Anda menginisialisasi data Anda, di destructor Anda uninitialize itu. Setiap kelas fungsi berikutnya harus merupakan anak dari kelas fungsi sebelumnya. Jika ada yang salah - lempar pengecualian.

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

Dan kemudian dalam kode Anda, Anda hanya perlu menelepon:

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

Saya kira itu adalah solusi terbaik jika setiap panggilan dari ConditionX menginisialisasi sesuatu, memori allocs dan lain-lain. Terbaik untuk memastikan semuanya akan dibersihkan.


3

cara yang menarik adalah bekerja dengan pengecualian.

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

Jika Anda menulis kode seperti itu, Anda akan pergi ke arah yang salah. Saya tidak akan melihatnya sebagai "masalah" untuk memiliki kode seperti itu, tetapi memiliki "arsitektur" yang berantakan.

Kiat: diskusikan kasus-kasus tersebut dengan pengembang berpengalaman yang Anda percayai ;-)


Saya pikir ide ini tidak dapat menggantikan setiap rantai jika. Bagaimanapun, dalam banyak kasus, ini adalah pendekatan yang sangat bagus!
WoIIe

3

Pendekatan lain - do - whileloop, meskipun telah disebutkan sebelumnya tidak ada contoh yang akan menunjukkan tampilannya:

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

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

executeThisFunctionInAnyCase();

(Yah sudah ada jawaban dengan whileloop tetapi do - whileloop tidak secara berlebihan memeriksa benar (pada awalnya) tetapi sebaliknya pada akhir xD (ini dapat dilewati, meskipun)).


Hai Zaffy - jawaban ini memiliki penjelasan besar tentang pendekatan do {} while (false). stackoverflow.com/a/24588605/294884 Dua jawaban lain juga menyebutkannya.
Fattie

Ini adalah pertanyaan yang menarik apakah lebih elegan untuk menggunakan LANJUTKAN atau BREAK dalam situasi ini!
Fattie

Hei @ JoBlow, saya melihat semua jawaban ... hanya ingin menunjukkan alih-alih membicarakannya :)
Zaffy

Jawaban pertama saya di sini, saya katakan, "Tidak ada yang menyebut ini ..." dan segera seseorang dengan ramah mengatakan bahwa itu adalah jawaban teratas ke-2 :)
Fattie

@ JoBlow Eh, kamu benar. Saya akan mencoba memperbaikinya. Saya merasa seperti ... xD Terima kasih, lain kali saya akan membayar lebih banyak perhatian :)
Zaffy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.