Apakah bagian 'akhirnya' dari konstruksi 'coba ... tangkap ... akhirnya' bahkan diperlukan?

25

Beberapa bahasa (seperti C ++ dan versi awal PHP) tidak mendukung finallybagian try ... catch ... finallykonstruksi. Apakah finallyperlu? Karena kode di dalamnya selalu berjalan, mengapa saya tidak harus menempatkan kode itu setelah try ... catchblok tanpa finallyklausa? Mengapa menggunakan satu? (Saya mencari alasan / motivasi untuk menggunakan / tidak menggunakan finally, bukan alasan untuk menghilangkan 'tangkapan' atau mengapa itu sah untuk dilakukan.)

Agi Hammerthief
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
maple_shaft

Jawaban:

36

Selain apa yang dikatakan orang lain, ada kemungkinan pengecualian dilemparkan ke dalam klausa tangkapan. Pertimbangkan ini:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

Dalam contoh ini, Cleanup()fungsi tidak pernah berjalan, karena pengecualian dilemparkan dalam klausa tangkapan dan tangkapan tertinggi berikutnya di tumpukan panggilan akan menangkap itu. Menggunakan blok akhirnya menghilangkan risiko ini, dan membuat kode lebih bersih untuk boot.

Erik
sumber
4
Terima kasih atas jawaban singkat dan langsung yang tidak menyimpang ke teori dan 'bahasa X lebih baik daripada wilayah Y'.
Agi Hammerthief
56

Seperti yang telah disebutkan orang lain, tidak ada jaminan bahwa kode setelah trypernyataan akan dieksekusi kecuali Anda menangkap setiap pengecualian yang mungkin. Yang mengatakan, ini:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

dapat ditulis ulang 1 sebagai:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

Tetapi yang terakhir mengharuskan Anda untuk menangkap semua pengecualian yang tidak ditangani, menduplikasi kode pembersihan, dan ingat untuk melempar kembali. Jadi finallytidak perlu , tapi ini berguna .

C ++ tidak memiliki finallykarena Bjarne Stroustrup percaya RAII lebih baik , atau setidaknya cukup untuk sebagian besar kasus:

Mengapa C ++ tidak menyediakan konstruksi "akhirnya"?

Karena C ++ mendukung alternatif yang hampir selalu lebih baik: Teknik "akuisisi sumber daya adalah inisialisasi" (TC ++ PL3 bagian 14.4). Ide dasarnya adalah untuk mewakili sumber daya oleh objek lokal, sehingga destruktor objek lokal akan melepaskan sumber daya. Dengan begitu, programmer tidak bisa melupakan untuk merilis resource.


1 Kode khusus untuk menangkap semua pengecualian dan rethrow tanpa kehilangan informasi jejak tumpukan bervariasi menurut bahasa. Saya telah menggunakan Java, di mana jejak stack ditangkap ketika pengecualian dibuat. Di C # Anda hanya akan menggunakan throw;.

Doval
sumber
8
Anda juga harus menangkap Pengecualian dalam handleError()kasus kedua, bukan?
Juri Robl
1
Anda mungkin juga membuat kesalahan. Saya akan ulangi dengan itu catch (Throwable t) {}, dengan mencoba .. menangkap blok di sekitar seluruh blok awal (untuk menangkap barang yang dapat dibuang dari handleErrorjuga)
njzk2
1
Saya benar-benar akan menambahkan try-catch tambahan yang Anda hilangkan saat memanggil handleErro();yang akan menjadikannya argumen yang lebih baik mengapa akhirnya blok berguna (meskipun itu bukan pertanyaan awal).
Alex
1
Jawaban ini tidak benar-benar menjawab pertanyaan mengapa C ++ tidak memiliki finally, yang jauh lebih bernuansa.
DeadMG
1
@AgiHammerthief Sarang tryada di dalam catchuntuk pengecualian khusus . Kedua, mungkin Anda tidak tahu apakah Anda dapat menangani kesalahan dengan sukses sampai Anda telah memeriksa pengecualian, atau bahwa penyebab pengecualian juga mencegah Anda menangani kesalahan (setidaknya pada tingkat itu). Itu cukup umum ketika melakukan I / O. Rethrow ada di sana karena satu-satunya cara untuk menjamin cleanUpberjalan adalah untuk menangkap semuanya , tetapi kode asli akan memungkinkan pengecualian yang berasal dari catch (SpecificException e)blok untuk menyebar ke atas.
Doval
22

finally blok biasanya digunakan untuk membersihkan sumber daya yang dapat membantu keterbacaan saat menggunakan beberapa pernyataan pengembalian:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

vs.

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}
AlexFoxGill
sumber
2
Saya pikir ini adalah jawaban terbaik. Menggunakan akhirnya sebagai pengganti pengecualian umum sepertinya menyebalkan. Kasing penggunaan yang benar adalah untuk membersihkan sumber daya atau operasi analog.
Kik
3
Mungkin yang lebih umum adalah kembali di dalam blok coba, daripada di dalam blok tangkap.
Michael Anderson
Menurut saya, kode tersebut tidak menjelaskan penggunaan finally. (Saya akan menggunakan kode seperti pada blok kedua karena banyak pernyataan pengembalian tidak disarankan di tempat saya bekerja.)
Agi Hammerthief
15

Seperti yang Anda duga, ya, C ++ menyediakan kemampuan yang sama tanpa mekanisme itu. Dengan demikian, tegasnya, yang try/ finallymekanisme tidak benar-benar diperlukan.

Yang mengatakan, melakukan tanpa itu memberlakukan beberapa persyaratan pada cara sisa bahasa dirancang. Dalam C ++ set tindakan yang sama diwujudkan dalam destruktor kelas. Ini berfungsi terutama (secara eksklusif?) Karena doa destruktor dalam C ++ bersifat deterministik. Ini, pada gilirannya, mengarah pada beberapa aturan yang agak rumit tentang masa hidup objek, beberapa di antaranya jelas tidak intuitif.

Sebagian besar bahasa lain menyediakan beberapa bentuk pengumpulan sampah. Meskipun ada hal-hal tentang pengumpulan sampah yang kontroversial (misalnya, efisiensinya relatif terhadap metode lain manajemen memori) satu hal yang umumnya tidak: waktu yang tepat ketika suatu objek akan "dibersihkan" oleh pengumpul sampah tidak terikat langsung ke ruang lingkup objek. Ini mencegah penggunaannya ketika pembersihan harus bersifat deterministik baik ketika itu hanya diperlukan untuk operasi yang benar, atau ketika berhadapan dengan sumber daya yang sangat berharga sehingga pembersihan mereka paling tidak ditunda secara sewenang-wenang. trySaya finallymenyediakan cara bagi bahasa-bahasa semacam itu untuk menghadapi situasi-situasi yang mengharuskan pembersihan deterministik.

Saya pikir orang-orang yang mengklaim bahwa sintaks C ++ untuk kemampuan ini "kurang ramah" daripada Java agak tidak penting. Lebih buruk lagi, mereka kehilangan poin yang jauh lebih penting tentang pembagian tanggung jawab yang jauh melampaui sintaksis, dan lebih banyak berkaitan dengan bagaimana kode dirancang.

Dalam C ++, pembersihan deterministik ini terjadi pada destruktor objek. Itu berarti objek dapat (dan biasanya harus) dirancang untuk membersihkannya sendiri. Ini menuju esensi desain berorientasi objek - kelas harus dirancang untuk memberikan abstraksi, dan menegakkan invariannya sendiri. Dalam C ++, seseorang melakukan hal itu - dan salah satu invarian yang disediakannya adalah ketika objek dihancurkan, sumber daya yang dikendalikan oleh objek tersebut (semuanya, bukan hanya memori) akan dihancurkan dengan benar.

Java (dan sejenisnya) agak berbeda. Sementara mereka mendukung (semacam) afinalize yang secara teoritis dapat memberikan kemampuan yang serupa, dukungan tersebut sangat lemah sehingga pada dasarnya tidak dapat digunakan (dan pada kenyataannya, pada dasarnya tidak pernah digunakan).

Akibatnya, alih-alih kelas itu sendiri dapat melakukan pembersihan yang diperlukan, klien kelas perlu mengambil langkah-langkah untuk melakukannya. Jika kita melakukan perbandingan yang cukup singkat, pada pandangan pertama dapat terlihat bahwa perbedaan ini cukup kecil dan Java cukup kompetitif dengan C ++ dalam hal ini. Kita berakhir dengan sesuatu seperti ini. Di C ++, kelasnya terlihat seperti ini:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... dan kode klien terlihat seperti ini:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

Di Jawa kita menukar sedikit lebih banyak kode di mana objek digunakan untuk sedikit kurang di kelas. Ini awalnya terlihat seperti trade-off yang lumayan. Pada kenyataannya, itu jauh dari itu, karena dalam kebanyakan kode khas kita hanya mendefinisikan kelas di satu tempat, tetapi kita menggunakannya di banyak tempat. Pendekatan C ++ berarti kita hanya menulis kode itu untuk menangani pembersihan di satu tempat. Pendekatan Java berarti kita harus menulis kode itu untuk menangani pembersihan berulang kali, di banyak tempat - setiap tempat kita menggunakan objek kelas itu.

Singkatnya, pendekatan Java pada dasarnya menjamin bahwa banyak abstraksi yang kami coba berikan adalah "bocor" - setiap dan setiap kelas yang membutuhkan pembersihan deterministik mewajibkan klien kelas untuk mengetahui tentang detail apa yang harus dibersihkan dan bagaimana melakukan pembersihan. , bukannya detail yang disembunyikan di dalam kelas itu sendiri.

Meskipun saya menyebutnya "pendekatan Java" di atas, try/ finallydan mekanisme serupa dengan nama lain tidak sepenuhnya terbatas pada Java. Untuk satu contoh yang menonjol, sebagian besar (semua?) Bahasa .NET (misalnya, C #) menyediakan hal yang sama.

Iterasi terbaru dari Java dan C # juga menyediakan titik tengah antara Java "klasik" dan C ++ dalam hal ini. Dalam C #, objek yang ingin mengotomatiskan pembersihannya dapat mengimplementasikan IDisposableantarmuka, yang menyediakan Disposemetode yang (setidaknya samar-samar) mirip dengan destruktor C ++. Sementara ini dapat digunakan melalui try/ finallyseperti di Jawa, C # mengotomatiskan tugas sedikit lebih banyak dengan membuang kelas dalam pelaksanaannya . Yang tersisa untuk programmer klien adalah beban yang lebih rendah dari menulis pernyataan untuk memastikan bahwausing pernyataan yang memungkinkan Anda menentukan sumber daya yang akan dibuat sebagai ruang lingkup dimasukkan, dan dimusnahkan ketika ruang lingkup keluar. Meskipun masih jauh dari tingkat otomatisasi dan kepastian yang disediakan oleh C ++, ini masih merupakan peningkatan substansial di Jawa. Secara khusus, perancang kelas dapat memusatkan detail tentang caranyaIDisposableusingIDisposableantarmuka akan digunakan saat seharusnya. Di Java 7 dan yang lebih baru, namanya telah diubah untuk melindungi yang bersalah, tetapi ide dasarnya pada dasarnya identik.

Jerry Coffin
sumber
1
Jawaban sempurna. Destructors adalah THE must-have feature di C ++.
Thomas Eding
13

Tidak percaya tidak ada orang lain telah mengangkat ini (no pun intended) - Anda tidak memerlukan sebuah menangkap klausa!

Ini sangat masuk akal:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Tidak ada klausa tangkapan di mana pun yang terlihat, karena metode ini tidak dapat melakukan apa pun yang berguna dengan pengecualian tersebut; mereka dibiarkan merambatkan kembali tumpukan panggilan ke penangan yang bisa . Menangkap dan melempar kembali pengecualian dalam setiap metode adalah Ide Buruk, terutama jika Anda hanya melemparkan kembali pengecualian yang sama. Ini benar-benar bertentangan dengan bagaimana Penanganan Perkecualian Terstruktur seharusnya bekerja (dan cukup dekat untuk mengembalikan "kode kesalahan" dari setiap metode, hanya dalam "bentuk" Pengecualian).

Namun, apa yang harus dilakukan metode ini adalah membersihkannya sendiri, sehingga "Dunia Luar" tidak perlu tahu apa pun tentang kekacauan yang terjadi. Klausa terakhir tidak hanya itu - tidak peduli bagaimana metode yang disebut berperilaku, klausa akhirnya akan dieksekusi "di jalan keluar" dari metode (dan hal yang sama berlaku untuk setiap akhirnya klausa antara titik di mana Exception dilemparkan dan klausa tangkapan akhir yang menanganinya); masing-masing dijalankan sebagai tumpukan panggilan "unwinds".

Phill W.
sumber
9

Apa yang akan terjadi jika dan pengecualian dilemparkan yang tidak Anda harapkan. Percobaan akan keluar di tengahnya dan tidak ada klausa tangkapan dieksekusi.

Blok akhirnya adalah untuk membantu dengan itu dan memastikan bahwa tidak peduli pengecualian pembersihan akan terjadi.

ratchet freak
sumber
4
Itu bukan alasan yang cukup untuk finally, karena Anda dapat mencegah pengecualian "tak terduga" dengan catch(Object)atau catch(...)menangkap semuanya.
MSalters
1
Yang kedengarannya seperti bekerja di sekitar. Secara konseptual akhirnya lebih bersih. Padahal saya harus mengaku jarang menggunakannya.
cepat_now
7

Beberapa bahasa menawarkan konstruktor dan destruktor untuk objek mereka (mis. C ++ saya percaya). Dengan bahasa-bahasa ini Anda dapat melakukan sebagian besar (bisa dibilang semua) dari apa yang biasanya dilakukan di finallydalam destruktor. Dengan demikian - dalam bahasa-bahasa tersebut - finallyklausa mungkin berlebihan.

Dalam bahasa tanpa destruktor (misal Java) sulit (bahkan mungkin mustahil) untuk mencapai pembersihan yang benar tanpa finallyklausa. NB - Di Jawa ada finalisemetode tetapi tidak ada jaminan itu akan dipanggil.

OldCurmudgeon
sumber
Mungkin bermanfaat untuk dicatat bahwa destruktor membantu membersihkan sumber daya ketika kehancuran bersifat deterministik . Jika kita tidak tahu kapan objek akan dihancurkan dan / atau dikumpulkan, maka destruktor tidak cukup aman.
Morwenn
@Morwenn - Poin bagus. Saya mengisyaratkan dengan referensi saya ke Jawa finalisetapi saya lebih suka untuk tidak masuk ke argumen politik di sekitar destruktor / finalis saat ini.
OldCurmudgeon
Dalam C ++ kehancuran adalah deterministik. Ketika ruang lingkup yang berisi objek otomatis keluar (misalnya akan muncul dari tumpukan), destruktornya disebut. (C ++ memungkinkan Anda untuk mengalokasikan objek pada stack, bukan hanya heap.)
Rob K
@RobK - Dan ini adalah fungsionalitas yang tepat dari finalisetetapi dengan rasa yang dapat diperpanjang dan mekanisme mirip oop - sangat ekspresif dan sebanding dengan finalisemekanisme bahasa lain.
OldCurmudgeon
1

Coba akhirnya dan coba tangkap adalah dua hal berbeda yang hanya berbagi kata kunci: "coba". Secara pribadi saya ingin melihat yang berbeda. Alasan Anda melihatnya bersama adalah karena pengecualian menghasilkan "lompatan".

Dan akhirnya mencoba dirancang untuk menjalankan kode bahkan jika aliran pemrograman melompat keluar. Entah itu karena pengecualian atau alasan lain. Ini cara yang rapi untuk mendapatkan sumber daya dan memastikannya dibersihkan setelah tanpa harus khawatir tentang lompatan.

Pieter B
sumber
3
Dalam. NET mereka diimplementasikan menggunakan mekanisme terpisah; di Jawa, bagaimanapun, satu-satunya konstruksi yang dikenali oleh JVM secara semantik setara dengan "on error goto", sebuah pola yang secara langsung mendukung try catchtetapi tidak try finally; kode menggunakan yang terakhir dikonversi menjadi kode hanya menggunakan yang pertama, dengan menyalin konten finallyblok di semua tempat kode yang mungkin perlu dijalankan.
supercat
@supercat bagus, terima kasih atas info tambahan tentang Java.
Pieter B
1

Karena pertanyaan ini tidak menentukan C ++ sebagai bahasa saya akan mempertimbangkan campuran C ++ dan Java, karena mereka mengambil pendekatan yang berbeda untuk penghancuran objek, yang semakin disarankan sebagai salah satu alternatif.

Alasan Anda mungkin menggunakan blok akhirnya, bukan kode setelah blok try-catch

  • Anda kembali lebih awal dari blok percobaan: Pertimbangkan ini

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    dibandingkan dengan:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • Anda kembali lebih awal dari blok tangkap: Bandingkan

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Anda memikirkan kembali pengecualian. Membandingkan:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

Contoh-contoh ini tidak membuatnya tampak terlalu buruk, tetapi sering kali Anda memiliki beberapa kasus yang berinteraksi dan lebih dari satu pengecualian / jenis sumber daya yang dimainkan. finallydapat membantu menjaga kode Anda dari menjadi mimpi buruk pemeliharaan yang kusut.

Sekarang di C ++ ini dapat ditangani dengan objek berbasis lingkup. Tetapi IMO ada dua kelemahan dari pendekatan ini 1. sintaksisnya kurang bersahabat. 2. Urutan konstruksi sebagai kebalikan dari urutan kehancuran dapat membuat segalanya menjadi kurang jelas.

Di Jawa Anda tidak dapat mengaitkan metode final untuk melakukan pembersihan karena Anda tidak tahu kapan itu akan terjadi - (Anda tentu bisa, tetapi itu adalah jalan yang dipenuhi dengan kondisi lomba yang menyenangkan - JVM memiliki banyak ruang untuk memutuskan kapan akan dihancurkan. hal - sering kali bukan saat Anda mengharapkannya - baik lebih awal atau lebih lambat dari yang Anda harapkan - dan itu dapat berubah ketika kompilator hot-spot mulai ... menghela napas ...)

Michael Anderson
sumber
1

Semua yang logis "perlu" dalam bahasa pemrograman adalah instruksi:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

Algoritma apa pun dapat diimplementasikan dengan hanya menggunakan instruksi di atas, semua konstruksi bahasa lain ada untuk membuat program lebih mudah untuk ditulis dan lebih dimengerti oleh programmer lain.

Lihat komputer lama yang kuno untuk perangkat keras aktual menggunakan set instruksi minimal.

James Anderson
sumber
1
Jawaban Anda tentu benar, tetapi saya tidak membuat kode dalam pertemuan; itu terlalu menyakitkan. Saya bertanya mengapa menggunakan fitur yang saya tidak mengerti maksudnya dalam bahasa yang mendukungnya, bukan apa set instruksi minimum dari suatu bahasa.
Agi Hammerthief
1
Intinya adalah bahasa apa pun yang mengimplementasikan hanya 5 operasi ini yang dapat mengimplementasikan algoritma apa pun - meskipun agak berbelit-belit. Kebanyakan vers / operator dalam bahasa tingkat tinggi tidak "diperlukan" jika tujuannya hanya untuk mengimplementasikan suatu algoritma. Jika tujuannya adalah untuk memiliki pengembangan yang cepat dari kode yang dapat dipelihara yang dapat dibaca maka sebagian besar diperlukan tetapi "dapat dibaca" dan "dapat dipelihara" tidak dapat diukur dan sangat subyektif. Pengembang bahasa yang baik memasukkan banyak fitur: jika Anda tidak menggunakannya untuk beberapa dari mereka maka jangan menggunakannya.
James Anderson
0

Sebenarnya kesenjangan yang lebih besar bagi saya biasanya dalam bahasa yang mendukung finallytetapi tidak memiliki destruktor, karena Anda dapat memodelkan semua logika yang terkait dengan "pembersihan" (yang akan saya pisahkan menjadi dua kategori) melalui destruktor di tingkat pusat tanpa berurusan secara manual dengan pembersihan. logika di setiap fungsi yang relevan. Ketika saya melihat C # atau kode Java melakukan hal-hal seperti membuka secara manual mutex dan menutup file dalam finallyblok, itu terasa ketinggalan jaman dan agak seperti kode C ketika semua itu diotomatiskan di C ++ melalui destructor dengan cara yang membebaskan manusia dari tanggung jawab itu.

Namun, saya masih akan menemukan kenyamanan ringan jika C ++ disertakan finallydan itu karena ada dua jenis pembersihan:

  1. Menghancurkan / membebaskan / membuka kunci / menutup / dll sumber daya lokal (destruktor sangat cocok untuk ini).
  2. Membatalkan / memutar kembali efek samping eksternal (destruktor cukup untuk ini).

Yang kedua setidaknya tidak memetakan secara intuitif pada ide penghancuran sumber daya, meskipun Anda dapat melakukannya dengan baik dengan penjaga lingkup yang secara otomatis mengembalikan perubahan ketika mereka dihancurkan sebelum dilakukan. Ada finallybisa dibilang menyediakan setidaknya sedikit (hanya dengan sedikit) mekanisme yang lebih mudah untuk pekerjaan daripada penjaga ruang lingkup.

Namun, mekanisme yang bahkan lebih langsung adalah rollbackblok yang belum pernah saya lihat dalam bahasa apa pun sebelumnya. Ini semacam pipa impian saya jika saya pernah merancang bahasa yang melibatkan penanganan pengecualian. Ini akan menyerupai ini:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

Itu akan menjadi cara yang paling mudah untuk memodelkan rollback efek samping, sementara destruktor cukup banyak mekanisme yang sempurna untuk pembersihan sumber daya lokal. Sekarang ini hanya menyimpan beberapa baris kode tambahan dari solusi guard lingkup, tetapi alasan saya sangat ingin melihat bahasa dengan ini di sana adalah bahwa rollback efek samping cenderung menjadi aspek penanganan penanganan pengecualian yang paling diabaikan (tapi paling sulit). dalam bahasa yang berputar di sekitar mutabilitas. Saya pikir fitur ini akan mendorong pengembang untuk berpikir tentang penanganan pengecualian dengan cara yang tepat dalam hal memutar kembali transaksi setiap kali fungsi menyebabkan efek samping dan gagal untuk menyelesaikan dan, sebagai bonus tambahan ketika orang melihat betapa sulitnya melakukan rollback dengan benar, mereka mungkin lebih suka menulis lebih banyak fungsi tanpa efek samping.

Ada juga beberapa kasus tidak jelas di mana Anda hanya ingin melakukan hal-hal lain apa pun saat keluar dari suatu fungsi, terlepas dari bagaimana itu keluar, seperti mungkin mencatat waktu. Ada finallyyang bisa dibilang solusi yang paling mudah dan sempurna untuk pekerjaan itu, karena mencoba untuk instantiate objek hanya dengan menggunakan destruktornya untuk tujuan tunggal mencatatkan cap waktu hanya terasa sangat aneh (meskipun Anda dapat melakukannya dengan baik dan cukup nyaman dengan lambdas) ).


sumber
-9

Seperti banyak hal tidak biasa lainnya tentang bahasa C ++, kurangnya try/finallykonstruk adalah cacat desain, jika Anda bahkan dapat menyebutnya bahwa dalam bahasa yang sering kali tampaknya tidak memiliki pekerjaan desain yang sebenarnya dilakukan sama sekali.

RAII (penggunaan doa destruktor deterministik berbasis lingkup pada objek berbasis tumpukan untuk pembersihan) memiliki dua kelemahan serius. Yang pertama adalah bahwa itu memerlukan penggunaan objek berbasis tumpukan , yang merupakan kekejian yang melanggar Prinsip Pergantian Liskov. Ada banyak alasan bagus mengapa tidak ada bahasa OO lain sebelum atau sejak C ++ telah menggunakannya - dalam epsilon; D tidak diperhitungkan karena didasarkan pada C ++ dan tidak memiliki pangsa pasar - dan menjelaskan masalah yang ditimbulkannya berada di luar cakupan jawaban ini.

Kedua, yang finallybisa dilakukan adalah superset penghancuran objek. Banyak dari apa yang dilakukan dengan RAII di C ++ akan dijelaskan dalam bahasa Delphi, yang tidak memiliki pengumpulan sampah, dengan pola berikut:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

Ini adalah pola RAII yang dibuat eksplisit; jika Anda membuat rutin C ++ yang hanya berisi yang setara dengan baris pertama dan ketiga di atas, apa yang akan dihasilkan oleh kompiler akan tampak seperti apa yang saya tulis dalam struktur dasarnya. Dan karena itu satu-satunya akses ke try/finallykonstruk yang disediakan C ++, pengembang C ++ berakhir dengan pandangan yang agak rabun try/finally: ketika semua yang Anda miliki adalah palu, semuanya mulai terlihat seperti destruktor, sehingga bisa dikatakan.

Tetapi ada hal-hal lain yang dapat dilakukan oleh pengembang berpengalaman dengan finallykonstruk. Ini bukan tentang kehancuran deterministik, bahkan dalam menghadapi pengecualian yang diangkat; ini tentang eksekusi kode deterministik , bahkan dalam menghadapi pengecualian yang diangkat.

Berikut ini hal lain yang biasa Anda lihat dalam kode Delphi: Objek dataset dengan kontrol pengguna terikat padanya. Dataset menyimpan data dari sumber eksternal, dan kontrol mencerminkan keadaan data. Jika Anda akan memuat banyak data ke dalam dataset Anda, Anda akan ingin untuk sementara menonaktifkan pengikatan data sehingga tidak melakukan hal-hal aneh pada UI Anda, mencoba memperbaruinya berulang-ulang dengan setiap catatan baru yang dimasukkan , jadi Anda akan mengkodekannya seperti ini:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

Jelas, tidak ada objek yang dihancurkan di sini, dan tidak perlu untuk satu. Kode ini sederhana, ringkas, eksplisit, dan efisien.

Bagaimana ini dilakukan dalam C ++? Pertama, Anda harus membuat kode seluruh kelas . Mungkin akan dipanggil DatasetEnableratau semacamnya. Seluruh keberadaannya akan menjadi pembantu RAII. Maka Anda perlu melakukan sesuatu seperti ini:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

Ya, kurung kurawal yang tampaknya tidak perlu itu diperlukan untuk mengelola pelingkupan yang tepat dan memastikan bahwa dataset diaktifkan kembali dengan segera dan tidak pada akhir metode. Jadi apa yang Anda dapatkan akhirnya tidak membutuhkan lebih sedikit baris kode (kecuali jika Anda menggunakan kawat gigi Mesir). Dibutuhkan objek berlebihan yang harus dibuat, yang memiliki overhead. (Bukankah kode C ++ seharusnya cepat?) Ini tidak eksplisit, tetapi lebih bergantung pada sihir kompiler. Kode yang dieksekusi dijelaskan di mana pun dalam metode ini, tetapi sebaliknya berada di kelas yang sama sekali berbeda, mungkin dalam file yang sama sekali berbeda . Singkatnya, ini sama sekali bukan solusi yang lebih baik daripada bisa menulis try/finallyblok sendiri.

Masalah semacam ini cukup umum dalam desain bahasa sehingga ada nama untuknya: inversi abstraksi. Ini terjadi ketika konstruksi tingkat tinggi dibangun di atas konstruksi tingkat rendah, dan kemudian konstruksi tingkat rendah tidak secara langsung didukung dalam bahasa, yang mengharuskan mereka yang ingin menggunakannya untuk mengimplementasikannya kembali dalam hal konstruksi tingkat tinggi, seringkali dengan hukuman yang curam untuk keterbacaan dan efisiensi kode.

Mason Wheeler
sumber
Komentar dimaksudkan untuk memperjelas atau meningkatkan pertanyaan dan jawaban. Jika Anda ingin berdiskusi tentang jawaban ini maka silakan pergi ke ruang obrolan. Terima kasih.
maple_shaft