Mulai, Penyelamatan, dan Pastikan di Ruby?

547

Saya baru-baru ini mulai pemrograman di Ruby, dan saya melihat penanganan pengecualian.

Saya bertanya-tanya apakah ensureRuby sama dengan finallydi C #? Haruskah saya memiliki:

file = File.open("myFile.txt", "w")

begin
  file << "#{content} \n"
rescue
  #handle the error here
ensure
  file.close unless file.nil?
end

atau haruskah saya melakukan ini?

#store the file
file = File.open("myFile.txt", "w")

begin
  file << "#{content} \n"
  file.close
rescue
  #handle the error here
ensure
  file.close unless file.nil?
end

Apakah ensuredipanggil tidak peduli apa, bahkan jika pengecualian tidak diajukan?

Lloyd Powell
sumber
1
Tidak ada yang baik. Sebagai aturan, ketika berurusan dengan sumber daya eksternal, Anda selalu ingin pembukaan sumber daya berada di dalam beginblok.
Sekarang,

Jawaban:

1181

Ya, ensurememastikan bahwa kode selalu dievaluasi. Itu sebabnya disebut ensure. Jadi, itu setara dengan Java dan C # finally.

Aliran umum dari begin/ rescue/ else/ ensure/ endterlihat seperti ini:

begin
  # something which might raise an exception
rescue SomeExceptionClass => some_variable
  # code that deals with some exception
rescue SomeOtherException => some_other_variable
  # code that deals with some other exception
else
  # code that runs only if *no* exception was raised
ensure
  # ensure that this code always runs, no matter what
  # does not change the final value of the block
end

Anda bisa keluar rescue, ensureatau else. Anda juga dapat mengabaikan variabel dalam hal ini Anda tidak akan dapat memeriksa pengecualian dalam kode penanganan pengecualian Anda. (Ya, Anda selalu dapat menggunakan variabel pengecualian global untuk mengakses pengecualian terakhir yang dimunculkan, tapi itu sedikit gila.) Dan Anda dapat meninggalkan kelas pengecualian, dalam hal ini semua pengecualian yang mewarisi dari StandardErrorakan ditangkap. (Harap dicatat bahwa ini tidak berarti bahwa semua pengecualian tertangkap, karena ada pengecualian yang contoh Exceptiontapi tidak StandardError. Pengecualian Kebanyakan sangat parah bahwa kompromi integritas program seperti SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionatau SystemExit.)

Beberapa blok membentuk blok pengecualian implisit. Sebagai contoh, definisi metode secara implisit juga merupakan blok pengecualian, jadi alih-alih menulis

def foo
  begin
    # ...
  rescue
    # ...
  end
end

Anda hanya menulis

def foo
  # ...
rescue
  # ...
end

atau

def foo
  # ...
ensure
  # ...
end

Hal yang sama berlaku untuk classdefinisi dan moduledefinisi.

Namun, dalam kasus spesifik yang Anda tanyakan, sebenarnya ada ungkapan yang jauh lebih baik. Secara umum, ketika Anda bekerja dengan beberapa sumber daya yang Anda perlu bersihkan pada akhirnya, Anda melakukannya dengan melewati blok ke metode yang melakukan semua pembersihan untuk Anda. Ini mirip dengan usingblok di C #, kecuali bahwa Ruby sebenarnya cukup kuat sehingga Anda tidak perlu menunggu imam besar Microsoft turun dari gunung dan dengan ramah mengubah kompiler mereka untuk Anda. Di Ruby, Anda bisa menerapkannya sendiri:

# This is what you want to do:
File.open('myFile.txt', 'w') do |file|
  file.puts content
end

# And this is how you might implement it:
def File.open(filename, mode='r', perm=nil, opt=nil)
  yield filehandle = new(filename, mode, perm, opt)
ensure
  filehandle&.close
end

Dan apa yang Anda ketahui: ini sudah tersedia di perpustakaan inti sebagai File.open. Tetapi ini adalah pola umum yang dapat Anda gunakan dalam kode Anda sendiri, untuk menerapkan segala jenis pembersihan sumber daya (à la usingdalam C #) atau transaksi atau apa pun yang mungkin Anda pikirkan.

Satu-satunya kasus di mana ini tidak berhasil, jika memperoleh dan melepaskan sumber daya didistribusikan di berbagai bagian program. Tetapi jika dilokalkan, seperti dalam contoh Anda, maka Anda dapat dengan mudah menggunakan blok sumber daya ini.


BTW: di C # modern, usingsebenarnya berlebihan, karena Anda dapat mengimplementasikan blok sumber daya gaya-Ruby sendiri:

class File
{
    static T open<T>(string filename, string mode, Func<File, T> block)
    {
        var handle = new File(filename, mode);
        try
        {
            return block(handle);
        }
        finally
        {
            handle.Dispose();
        }
    }
}

// Usage:

File.open("myFile.txt", "w", (file) =>
{
    file.WriteLine(contents);
});
Jörg W Mittag
sumber
81
Perhatikan bahwa, meskipun ensurepernyataan dieksekusi terakhir, itu bukan nilai balik.
Chris
30
Saya suka melihat kontribusi kaya seperti ini di SO. Ini melampaui apa yang diminta OP sehingga berlaku untuk lebih banyak pengembang, namun masih pada topik. Saya belajar beberapa hal dari jawaban ini + suntingan. Terima kasih karena tidak hanya menulis, "Ya, ensuredipanggil apa pun yang terjadi."
Dennis
3
Catatan, memastikan bahwa TIDAK dijamin untuk menyelesaikan. Ambil kasus di mana Anda memiliki awal / pastikan / akhir di dalam utas, dan kemudian Anda menelepon Thread.kill ketika baris pertama dari blok memastikan dipanggil. Ini akan menyebabkan sisa memastikan untuk tidak mengeksekusi.
Teddy
5
@Teddy: memastikan dijamin untuk memulai eksekusi, tidak dijamin selesai. Contoh Anda terlalu banyak - pengecualian sederhana di dalam blok pastikan akan menyebabkannya keluar juga.
Martin Konecny
3
juga perhatikan bahwa tidak ada jaminan memastikan dipanggil. Aku serius. Pemadaman listrik / kesalahan perangkat keras / os crash dapat terjadi, dan jika perangkat lunak Anda sangat penting, itu perlu dipertimbangkan juga.
EdvardM
37

FYI, bahkan jika pengecualian kembali pada rescuebagian, ensureblok akan dieksekusi sebelum eksekusi kode berlanjut ke pengendali pengecualian berikutnya. Contohnya:

begin
  raise "Error!!"
rescue
  puts "test1"
  raise # Reraise exception
ensure
  puts "Ensure block"
end
alup
sumber
14

Jika Anda ingin memastikan file ditutup, Anda harus menggunakan bentuk blok File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Farrel
sumber
3
Saya kira jika Anda tidak ingin menangani kesalahan tetapi hanya meningkatkannya, dan tutup pegangan file, Anda tidak perlu memulai penyelamatan di sini?
rogerdpack
7

Ya, ensuredipanggil dalam keadaan apa pun. Untuk informasi lebih lanjut, lihat " Pengecualian, Tangkapan, dan Lempar " buku Ruby Pemrograman dan cari "pastikan".

Milan Novota
sumber
5

Ya, ensureMEMASTIKAN dijalankan setiap waktu, jadi Anda tidak perlu file.closedi beginblok.

Omong-omong, cara yang baik untuk menguji adalah dengan melakukan:

begin
  # Raise an error here
  raise "Error!!"
rescue
  #handle the error here
ensure
  p "=========inside ensure block"
end

Anda dapat menguji untuk melihat apakah "========= di dalam blok pastikan" akan dicetak ketika ada pengecualian. Kemudian Anda dapat mengomentari pernyataan yang memunculkan kesalahan dan melihat apakah ensurepernyataan itu dijalankan dengan melihat apakah ada yang dicetak.

Aaron Qian
sumber
4

Inilah mengapa kita perlu ensure:

def hoge
  begin
    raise
  rescue  
    raise # raise again
  ensure  
    puts 'ensure' # will be executed
  end  
  puts 'end of func' # never be executed
end  
kuboon
sumber
4

Ya, ensureseperti finally jaminan bahwa blok akan dieksekusi . Ini sangat berguna untuk memastikan bahwa sumber daya kritis dilindungi misalnya menutup file menangani kesalahan, atau melepaskan mutex.

Chris McCauley
sumber
Kecuali dalam kasusnya, tidak ada jaminan file akan ditutup, karena File.openbagian BUKAN di dalam blok begin-pastikan. Hanya file.closetetapi tidak cukup.
Sekarang,