Apakah penggunaan klausa akhirnya untuk melakukan pekerjaan setelah kembali gaya buruk / berbahaya?


30

Sebagai bagian dari penulisan Iterator, saya menemukan diri saya menulis potongan kode berikut (stripping error handling)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

merasa sedikit lebih mudah dibaca daripada

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

Saya tahu ini adalah contoh sederhana, di mana perbedaan dalam keterbacaan mungkin tidak terlalu besar, tapi saya tertarik pada pendapat umum, apakah itu buruk untuk menggunakan coba-akhirnya dalam kasus-kasus seperti ini di mana tidak ada pengecualian yang terlibat, atau jika itu sebenarnya lebih disukai ketika menyederhanakan kode.

Jika buruk: mengapa? Gaya, kinerja, jebakan, ...?

Kesimpulan Terima kasih atas semua jawaban Anda! Saya kira kesimpulannya (setidaknya bagi saya) adalah bahwa contoh pertama mungkin lebih mudah dibaca jika itu adalah pola umum, tetapi tidak. Oleh karena itu kebingungan yang diperkenalkan dengan menggunakan konstruksi di luar tujuannya, bersama dengan kemungkinan aliran pengecualian, akan lebih besar daripada penyederhanaan apa pun.


10
Saya pribadi akan dapat memahami yang kedua lebih dari yang pertama, tetapi saya jarang menggunakan finallybalok.
Christian Mann

2
return current = fetcher.fetchNext (saat ini); // Bagaimana dengan ini, alias apakah kamu benar-benar perlu membuat prefetch?
scarfridge

1
@ scarfridge Contohnya adalah tentang implementasi Iterator, di mana Anda memang perlu semacam prefetching agar hasNext()dapat bekerja. Cobalah sendiri.
maaartinus

1
@maaartinus saya tahu itu. Terkadang hasNext () hanya mengembalikan true mis. Urutan hailstone dan terkadang mengambil elemen berikutnya sangat mahal. Dalam kasus terakhir, Anda harus mengambil elemen hanya berdasarkan permintaan yang mirip dengan inisialisasi malas.
scarfridge

1
@ scarfridge Masalah dengan penggunaan umum Iteratoradalah bahwa Anda perlu mengambil nilai hasNext()(karena mengambilnya sering kali merupakan satu-satunya cara untuk mengetahui apakah ada) dan mengembalikannya next()seperti OP lakukan.
maaartinus

Jawaban:


18

Secara pribadi, saya akan memikirkan gagasan pemisahan permintaan perintah . Ketika Anda langsung melakukannya, next () memiliki dua tujuan: tujuan yang diiklankan mengambil elemen berikutnya, dan efek samping tersembunyi dari mutasi keadaan internal berikutnya. Jadi, apa yang Anda lakukan adalah melakukan tujuan yang diiklankan di tubuh metode dan kemudian menempel pada efek samping tersembunyi dalam klausa akhirnya, yang tampaknya ... aneh, meskipun tidak 'salah', tepatnya.

Apa yang sebenarnya menjadi intinya adalah bagaimana dimengerti kode itu, saya katakan, dan dalam hal ini jawabannya adalah "meh". Anda "mencoba" pernyataan pengembalian sederhana dan kemudian menjalankan efek samping tersembunyi dalam blok kode yang seharusnya untuk pemulihan kesalahan gagal-aman. Apa yang Anda lakukan adalah 'pintar', dan kode 'pintar' sering mendorong pengelola untuk bergumam, "apa ... oh, saya rasa saya mengerti." Lebih baik bagi orang yang membaca kode Anda untuk bergumam "ya, uh-ya, masuk akal, ya ..."

Jadi, bagaimana jika Anda memisahkan mutasi negara dari panggilan accessor? Saya membayangkan masalah keterbacaan yang Anda khawatirkan menjadi diperdebatkan, tetapi saya tidak tahu bagaimana hal itu memengaruhi abstraksi Anda yang lebih luas. Sesuatu yang perlu dipertimbangkan.


1
+1 untuk komentar "pintar". Itu dengan sangat akurat menangkap contoh ini.

2
Tindakan 'kembali dan maju' next () dipaksakan pada OP oleh antarmuka iterator Java yang berat.
kevin cline

37

Murni dari sudut pandang gaya, saya pikir tiga baris ini:

T current = next;
next = fetcher.getNext();
return current;

... keduanya lebih jelas dan lebih pendek daripada blok coba / akhirnya. Karena Anda tidak mengharapkan pengecualian untuk dilempar, menggunakan tryblok hanya akan membingungkan orang.


2
Blok coba / tangkap / akhirnya mungkin juga menambahkan overhead tambahan, saya tidak yakin apakah javac atau kompiler JIT akan menghapusnya bahkan jika Anda tidak melempar Exception.
Martijn Verburg

2
@ MartijnVerburg ya, javac masih menambahkan entri ke tabel pengecualian dan menduplikasi kode akhirnya bahkan jika blok coba kosong.
Daniel Lubarov

@Daniel - terima kasih saya terlalu malas untuk mengeksekusi javap :-)
Martijn Verburg

@ Martijn Verburg: AFAIK, blok try-catch-finallt pada dasarnya gratis selama tidak ada pengecualian yang dilemparkan. Di sini, javac tidak mencoba dan tidak perlu mengoptimalkan apa pun karena JIT akan membereskannya.
maaartinus

@maaartinus - terima kasih yang masuk akal - perlu membaca kode sumber OpenJDK lagi :-)
Martijn Verburg

6

Itu akan tergantung pada tujuan kode di dalam blok akhirnya. Contoh kanonik adalah menutup aliran setelah membaca / menulis darinya, semacam "pembersihan" yang harus selalu dilakukan. Mengembalikan objek (dalam hal ini sebuah iterator) ke keadaan IMHO yang valid juga dianggap sebagai pembersihan, jadi saya tidak melihat masalah di sini. Jika OTOH yang Anda gunakan returnsegera setelah nilai pengembalian Anda ditemukan, dan menambahkan banyak kode yang tidak terkait di blok akhirnya, maka itu akan mengaburkan tujuan dan membuatnya kurang dimengerti.

Saya tidak melihat ada masalah dalam menggunakannya ketika "tidak ada pengecualian yang terlibat". Ini sangat umum untuk digunakan try...finallytanpa catchkode ketika hanya bisa melempar RuntimeExceptiondan Anda tidak berencana menangani mereka. Kadang-kadang, yang terakhir hanyalah perlindungan, dan Anda tahu untuk logika program Anda bahwa tidak ada pengecualian yang akan dibuang (kondisi klasik "ini seharusnya tidak pernah terjadi").

Jebakan: setiap pengecualian yang muncul di dalam tryblok akan membuat finallybock berjalan. Itu dapat menempatkan Anda dalam keadaan tidak konsisten. Jadi, jika pernyataan pengembalian Anda adalah sesuatu seperti:

return preProcess(next);

dan kode ini menimbulkan pengecualian, fetchNextakan tetap berjalan. OTOH jika Anda mengkodekannya seperti:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

maka itu tidak akan berjalan. Saya tahu Anda mengasumsikan kode coba tidak pernah dapat memunculkan pengecualian, tetapi untuk kasus yang lebih kompleks dari ini, bagaimana Anda bisa yakin? Jika iterator Anda adalah objek yang berumur panjang, itu akan terus ada bahkan jika kesalahan yang tidak dapat dipulihkan terjadi di utas saat ini, daripada itu akan penting untuk tetap dalam keadaan valid setiap saat. Kalau tidak, itu tidak terlalu penting ...

Kinerja: akan menarik untuk mendekompilasi kode seperti itu untuk melihat cara kerjanya di bawah tenda, tapi saya tidak cukup tahu tentang JVM untuk mengambil tebakan yang bagus ... Pengecualian biasanya "luar biasa", jadi kode yang menangani mereka tidak perlu dioptimalkan untuk kecepatan (maka saran untuk tidak pernah menggunakan pengecualian dalam aliran kontrol normal program Anda), tetapi saya tidak tahu finally.


Bukankah Anda benar-benar memberikan alasan yang cukup baik untuk tidak menggunakan finallycara ini? Maksud saya, seperti yang Anda katakan, kode tersebut akhirnya memiliki konsekuensi yang tidak diinginkan; lebih buruk lagi, Anda mungkin bahkan tidak memikirkan pengecualian.
Andres F.

2
Kecepatan aliran kontrol normal tidak dipengaruhi secara signifikan oleh konstruksi penanganan-pengecualian. Penalti kinerja hanya dibayarkan ketika benar-benar melempar dan menangkap pengecualian, bukan ketika memasuki blok uji coba atau memasuki blok akhirnya dalam operasi normal.
yfeldblum

1
@AndresF. Benar, tetapi itu juga berlaku untuk kode pembersihan apa pun . Sebagai contoh, misalkan referensi ke aliran terbuka diatur nulloleh kode kereta; mencoba membaca darinya akan memunculkan eksepsi, mencoba menutupnya di finallyblok akan memunculkan eksepsi lain . Selain itu, ini adalah situasi di mana keadaan perhitungan tidak masalah, Anda ingin menutup aliran apakah pengecualian terjadi. Mungkin ada situasi lain seperti itu juga. Untuk alasan ini saya memperlakukannya sebagai perangkap untuk penggunaan yang valid, daripada merekomendasikan untuk tidak pernah menggunakannya.
mgibsonbr

Ada juga masalah bahwa ketika mencoba dan akhirnya memblokir melempar pengecualian, pengecualian dalam percobaan tidak pernah dilihat oleh siapa pun, tetapi mungkin itulah penyebab masalahnya.
Hans-Peter Störr

3

Pernyataan judul: "... akhirnya klausa untuk melakukan pekerjaan setelah kembali ..." adalah salah. Blok akhirnya terjadi sebelum fungsi kembali. Itulah inti akhirnya pada kenyataannya.

Apa yang Anda pelecehkan di sini adalah urutan evaluasi, di mana nilai selanjutnya disimpan untuk dikembalikan sebelum Anda mengubahnya. Ini bukan praktik umum dan menurut saya salah karena membuat Anda kode tidak berurutan dan karenanya jauh lebih sulit untuk diikuti.

Tujuan akhirnya adalah untuk penanganan pengecualian di mana beberapa konsekuensi harus terjadi terlepas dari pengecualian yang dilemparkan misalnya membersihkan beberapa sumber daya, menutup koneksi DB, menutup file / socket dll.


+1, saya akan menambahkan bahwa meskipun spesifikasi bahasa menjamin bahwa nilai disimpan sebelum dikembalikan, hal itu sangat bertentangan dengan harapan pembaca, dan dengan demikian harus dihindari jika memungkinkan. Debugging dua kali lebih sulit daripada coding ...
jmoreno

0

Saya akan sangat berhati-hati, karena (secara umum, bukan dalam contoh Anda) bagian dalam percobaan dapat membuang pengecualian, dan jika itu dibuang, tidak ada yang akan dikembalikan dan jika Anda benar-benar berniat untuk mengeksekusi apa yang ada di akhirnya setelah kembali, itu akan mengeksekusi ketika Anda tidak menginginkannya.

Dan cacing lain adalah, bahwa secara umum, beberapa tindakan yang bermanfaat pada akhirnya dapat membuang pengecualian, sehingga Anda dapat mengakhiri dengan metode yang benar-benar berantakan.

Karena itu, seseorang yang membacanya harus memikirkan masalah ini, sehingga lebih sulit untuk dipahami.

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.