pengembangan-web-mp.com

Dari mana datangnya gagasan "satu kembali saja"?

Saya sering berbicara dengan programmer yang mengatakan "Jangan letakkan banyak pernyataan pengembalian dalam metode yang sama." Ketika saya meminta mereka untuk memberi tahu saya alasannya, yang saya dapatkan hanyalah "Pengodean standar mengatakan demikian. "atau" Membingungkan. "Ketika mereka menunjukkan solusi kepada saya dengan pernyataan pengembalian tunggal, kode itu terlihat lebih buruk bagi saya. Sebagai contoh:

if (condition)
   return 42;
else
   return 97;

"Ini jelek, Anda harus menggunakan variabel lokal!"

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

Bagaimana kode 50% mengasapi ini membuat program lebih mudah dimengerti? Secara pribadi, saya merasa lebih sulit, karena ruang keadaan baru saja meningkat oleh variabel lain yang dapat dengan mudah dicegah.

Tentu saja, biasanya saya hanya menulis:

return (condition) ? 42 : 97;

Tetapi banyak programmer menghindari operator bersyarat dan lebih suka bentuk panjang.

Dari mana gagasan "satu kembali saja" ini berasal? Adakah alasan historis mengapa kebaktian ini terjadi?

1077
fredoverflow

"Single Entry, Single Exit" ditulis ketika sebagian besar pemrograman dilakukan dalam bahasa Assembly, FORTRAN, atau COBOL. Ini telah banyak disalahtafsirkan secara luas, karena bahasa modern tidak mendukung praktik-praktik yang Dijkstra peringatkan.

"Entri Tunggal" berarti "jangan membuat titik entri alternatif untuk fungsi". Dalam bahasa Assembly, tentu saja, dimungkinkan untuk memasukkan fungsi pada instruksi apa pun. FORTRAN mendukung banyak entri ke fungsi dengan pernyataan ENTRY:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"Single Exit" berarti bahwa suatu fungsi hanya boleh kembali ke satu tempat: pernyataan segera setelah panggilan. Itu tidak berarti bahwa suatu fungsi hanya akan mengembalikan dari satu tempat. Ketika Pemrograman Terstruktur ditulis, itu adalah praktik umum untuk fungsi untuk menunjukkan kesalahan dengan kembali ke lokasi alternatif. FORTRAN mendukung ini melalui "pengembalian alternatif":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

Kedua teknik ini sangat rawan kesalahan. Penggunaan entri alternatif sering menyebabkan beberapa variabel tidak diinisialisasi. Penggunaan pengembalian alternatif memiliki semua masalah pernyataan GOTO, dengan komplikasi tambahan bahwa kondisi cabang tidak berdekatan dengan cabang, tetapi di suatu tempat dalam subrutin.

1145
kevin cline

Gagasan Entri Tunggal, Keluar Tunggal (SESE) berasal dari bahasa dengan manajemen sumber daya eksplisit, seperti C dan Assembly. Dalam C, kode seperti ini akan membocorkan sumber daya:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

Dalam bahasa tersebut, Anda pada dasarnya memiliki tiga opsi:

  • Replikasi kode pembersihan.
    Ugh. Redundansi selalu buruk.

  • Gunakan goto untuk melompat ke kode pembersihan.
    Ini membutuhkan kode pembersihan untuk menjadi hal terakhir dalam fungsi. (Dan inilah mengapa beberapa orang berpendapat bahwa goto memiliki tempatnya. Dan memang - di C.)

  • Memperkenalkan variabel lokal dan memanipulasi aliran kontrol melalui itu.
    Kerugiannya adalah aliran kontrol yang dimanipulasi melalui sintaksis (pikirkan break, return, if, while) jauh lebih mudah diikuti daripada control flow dimanipulasi melalui keadaan variabel (karena variabel-variabel tersebut tidak memiliki keadaan ketika Anda melihat algoritma).

Di Assembly bahkan lebih aneh, karena Anda dapat melompat ke alamat mana pun dalam suatu fungsi saat Anda memanggil fungsi itu, yang secara efektif berarti Anda memiliki jumlah titik masuk yang hampir tak terbatas untuk fungsi apa pun. (Kadang-kadang ini membantu. Pemukul seperti itu adalah teknik umum bagi kompiler untuk mengimplementasikan penyesuaian this penunjuk yang diperlukan untuk memanggil fungsi virtual dalam skenario multiple-inheritance di C++.)

Ketika Anda harus mengelola sumber daya secara manual, mengeksploitasi opsi memasukkan atau keluar dari fungsi di mana saja mengarah ke kode yang lebih kompleks, dan dengan demikian ke bug. Karena itu, muncul aliran pemikiran yang menyebarkan SESE, untuk mendapatkan kode yang lebih bersih dan lebih sedikit bug.


Namun, ketika suatu bahasa memiliki pengecualian, (hampir) fungsi apa pun mungkin keluar sebelum waktunya di (hampir) titik mana pun, jadi Anda perlu membuat ketentuan untuk pengembalian prematur. (Saya pikir finally terutama digunakan untuk itu di Java dan using (ketika mengimplementasikan IDisposable, finally sebaliknya) di C #; C++ sebagai gantinya mempekerjakan RAII .) Setelah Anda melakukan ini, Anda tidak bisa gagal membersihkan setelah Anda jatuh tempo ke pernyataan return awal, jadi apa yang mungkin merupakan argumen terkuat yang mendukung SESE telah menghilang.

Itu membuat keterbacaan. Tentu saja, fungsi 200 LoC dengan setengah lusin return pernyataan ditaburkan secara acak di atasnya bukan gaya pemrograman yang baik dan tidak membuat kode yang dapat dibaca. Tetapi fungsi seperti itu tidak akan mudah dipahami tanpa pengembalian prematur itu.

Dalam bahasa di mana sumber daya tidak atau tidak seharusnya dikelola secara manual, ada sedikit atau tidak ada nilai dalam mengikuti konvensi SESE lama. OTOH, seperti yang saya katakan di atas, SESE sering membuat kode lebih kompleks. Ini adalah dinosaurus yang (kecuali untuk C) tidak cocok dengan sebagian besar bahasa saat ini. Alih-alih membantu pemahaman kode, justru menghambatnya.


Mengapa Java programmer tetap pada ini? Saya tidak tahu, tetapi dari POV (luar) saya, Java mengambil banyak konvensi dari C (di mana mereka masuk akal) dan menerapkannya pada OO dunianya (di mana mereka tidak berguna atau sangat buruk), di mana sekarang menempel pada mereka, tidak peduli berapa biayanya. (Seperti konvensi untuk menentukan semua variabel Anda di awal ruang lingkup.)

Programmer menempel pada semua jenis notasi aneh karena alasan irasional. (Pernyataan struktural yang sangat bersarang - "panah" - dalam bahasa seperti Pascal, pernah dilihat sebagai kode yang indah.) Menerapkan alasan logis murni untuk ini tampaknya gagal meyakinkan mayoritas dari mereka untuk menyimpang dari cara yang telah mereka tetapkan. Cara terbaik untuk mengubah kebiasaan seperti itu mungkin mengajar mereka sejak dini untuk melakukan yang terbaik, bukan apa yang konvensional. Anda, sebagai guru pemrograman, memilikinya di tangan Anda. :)

921
sbi

Di satu sisi, pernyataan pengembalian tunggal membuat logging lebih mudah, serta bentuk debugging yang mengandalkan logging. Saya ingat banyak kali saya harus mengurangi fungsi menjadi pengembalian tunggal hanya untuk mencetak nilai pengembalian pada satu titik.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

Di sisi lain, Anda bisa mengubah ini menjadi function() yang memanggil _function() dan mencatat hasilnya.

82
perreal

"Single Entry, Single Exit" berasal dari revolusi Pemrograman Terstruktur pada awal 1970-an, yang dimulai dengan surat Edsger W. Dijkstra kepada Editor " Pernyataan GOTO Dianggap Berbahaya ". Konsep-konsep di balik pemrograman terstruktur dijelaskan secara rinci dalam buku klasik "Structured Programming" oleh Ole Johan-Dahl, Edsger W. Dijkstra, dan Charles Anthony Richard Hoare.

"Pernyataan GOTO Dianggap Berbahaya" wajib dibaca, bahkan hari ini. "Pemrograman Terstruktur" bertanggal, tetapi masih sangat, sangat bermanfaat, dan harus berada di puncak daftar "Harus Dibaca" pengembang, jauh di atas apa pun dari mis. Steve McConnell. (Bagian Dahl menjabarkan dasar-dasar kelas dalam Simula 67, yang merupakan dasar teknis untuk kelas-kelas dalam C++ dan semua pemrograman berorientasi objek.)

53
John R. Strohm

Selalu mudah untuk menautkan Fowler.

Salah satu contoh utama yang bertentangan dengan SESE adalah klausa penjaga:

Ganti Nested Conditional dengan Guard Clauses

Gunakan Klausa Penjaga untuk semua kasus khusus

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
return result;
};  

 http://www.refactoring.com/catalog/arrow.gif

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
};  

Untuk informasi lebih lanjut lihat halaman 250 dari Refactoring ...

36
Pieter B

saya menulis posting blog tentang topik ini beberapa waktu lalu.

Intinya adalah bahwa aturan ini berasal dari usia bahasa yang tidak memiliki pengumpulan sampah atau penanganan pengecualian. Tidak ada studi formal yang menunjukkan bahwa aturan ini mengarah pada kode yang lebih baik dalam bahasa modern. Jangan ragu untuk mengabaikannya setiap kali ini akan menyebabkan kode lebih pendek atau lebih mudah dibaca. The Java orang-orang yang bersikeras tentang hal ini secara membabi buta dan tidak perlu mengikuti aturan lama yang tidak berguna.

Pertanyaan ini juga ditanyakan pada Stackoverflow

11
Anthony

Sekali kembali membuat refactoring lebih mudah. Cobalah untuk melakukan "ekstrak metode" ke tubuh bagian dalam loop for yang berisi kembali, istirahat atau melanjutkan. Ini akan gagal karena Anda telah merusak aliran kendali Anda.

Intinya adalah: Saya kira tidak ada yang pura-pura menulis kode yang sempurna. Jadi kode biasanya sedang dalam refactoring untuk "ditingkatkan" dan diperluas. Jadi tujuan saya adalah untuk menjaga kode saya sebagai refactoring ramah mungkin.

Seringkali saya menghadapi masalah bahwa saya harus memformulasi ulang fungsi sepenuhnya jika mengandung pemecah aliran kontrol dan jika saya ingin menambahkan sedikit fungsionalitas saja. Ini sangat rawan kesalahan saat Anda mengubah keseluruhan aliran kendali alih-alih memperkenalkan jalur baru ke sarang yang terisolasi. Jika Anda hanya memiliki satu pengembalian tunggal di akhir atau jika Anda menggunakan penjaga untuk keluar dari loop Anda tentu saja memiliki lebih banyak kode bersarang dan lebih banyak. Tetapi Anda mendapatkan compiler dan IDE mendukung kemampuan refactoring.

6
oopexpert

Pertimbangkan fakta bahwa banyak pernyataan pengembalian setara dengan memiliki GOTO untuk pernyataan pengembalian tunggal. Ini adalah kasus yang sama dengan pernyataan break. Karena itu, beberapa orang, seperti saya, menganggapnya GOTO untuk semua maksud dan tujuan.

Namun, saya tidak menganggap jenis GOTO ini berbahaya dan tidak akan ragu untuk menggunakan GOTO yang sebenarnya dalam kode saya jika saya menemukan alasan yang bagus untuk itu.

Aturan umum saya adalah bahwa GOTO hanya untuk kontrol aliran. Mereka seharusnya tidak pernah digunakan untuk perulangan apa pun, dan Anda tidak boleh GOTO 'ke atas' atau 'ke belakang'. (yang merupakan cara istirahat/pengembalian bekerja)

Seperti yang disebutkan orang lain, berikut ini adalah harus dibaca Pernyataan GOTO Dianggap Berbahaya
Namun, perlu diingat bahwa ini ditulis pada tahun 1970 ketika GOTO terlalu banyak digunakan. Tidak setiap GOTO berbahaya dan saya tidak akan mengecilkan penggunaannya selama Anda tidak menggunakannya daripada konstruksi normal, melainkan dalam kasus aneh bahwa menggunakan konstruksi normal akan sangat merepotkan.

Saya menemukan bahwa menggunakannya dalam kasus kesalahan di mana Anda perlu melarikan diri dari suatu daerah karena kegagalan yang seharusnya tidak pernah terjadi dalam kasus normal yang berguna di kali. Tetapi Anda juga harus mempertimbangkan untuk menempatkan kode ini ke dalam fungsi yang terpisah sehingga Anda bisa kembali lebih awal daripada menggunakan GOTO ... tapi terkadang itu juga merepotkan.

5
user606723

Kompleksitas Siklomatik

Saya telah melihat SonarCube menggunakan pernyataan pengembalian berganda untuk menentukan kompleksitas siklomatik. Jadi semakin banyak pernyataan pengembalian, semakin tinggi kompleksitas siklomatiknya

Return Type Change

Beberapa pengembalian berarti kita perlu mengubah di beberapa tempat dalam fungsi ketika kita memutuskan untuk mengubah jenis pengembalian kita.

Beberapa Keluar

Lebih sulit untuk di-debug karena logika perlu dipelajari dengan cermat bersama dengan pernyataan kondisional untuk memahami apa yang menyebabkan nilai yang dikembalikan.

Solusi Refactored

Solusi untuk beberapa pernyataan pengembalian adalah dengan menggantinya dengan polimorfisme yang memiliki pengembalian tunggal setelah menyelesaikan objek implementasi yang diperlukan.

4
Sorter