Inizia, salva e assicurati in Ruby?


547

Di recente ho iniziato a programmare in Ruby e sto esaminando la gestione delle eccezioni.

Mi chiedevo se ensurefosse l'equivalente di Ruby finallyin C #? Dovrei avere:

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

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

o dovrei farlo?

#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

Non ensureottenere chiamato non importa cosa, anche se un'eccezione non è sollevato?


1
Né va bene. Di norma, quando si ha a che fare con risorse esterne, si desidera sempre che l'apertura delle risorse sia all'interno del beginblocco.
Nowaker,

Risposte:


1181

Sì, ensuregarantisce che il codice sia sempre valutato. Ecco perché si chiama ensure. Quindi, è equivalente a Java e C # finally.

Il flusso generale di begin/ rescue/ else/ ensure/ endè simile al seguente:

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

Puoi lasciarlo fuori rescue, ensureoppure else. Puoi anche tralasciare le variabili, nel qual caso non sarai in grado di ispezionare l'eccezione nel tuo codice di gestione delle eccezioni. (Bene, puoi sempre usare la variabile di eccezione globale per accedere all'ultima eccezione che è stata sollevata, ma è un po 'confusa.) E puoi tralasciare la classe di eccezione, nel qual caso tutte le eccezioni che ereditano StandardErrorverranno colte. (Si noti che questo non significa che tutte le eccezioni vengono catturati, perché ci sono delle eccezioni, che sono istanze di Exception, ma non StandardError. Per lo più molto gravi eccezioni che compromettono l'integrità del programma come SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptiono SystemExit.)

Alcuni blocchi formano blocchi di eccezioni impliciti. Ad esempio, le definizioni dei metodi sono implicitamente anche blocchi di eccezioni, quindi invece di scrivere

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

scrivi solo

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

o

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

Lo stesso vale per le classdefinizioni e le moduledefinizioni.

Tuttavia, nel caso specifico di cui ti stai chiedendo, in realtà esiste un linguaggio molto migliore. In generale, quando lavori con alcune risorse che devi ripulire alla fine, lo fai passando un blocco a un metodo che esegue tutta la pulizia per te. È simile a un usingblocco in C #, tranne per il fatto che Ruby è in realtà abbastanza potente da non dover aspettare che i sommi sacerdoti di Microsoft scendano dalla montagna e cambino gentilmente il loro compilatore per te. In Ruby, puoi semplicemente implementarlo da solo:

# 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

E cosa sai: questo è già disponibile nella libreria principale come File.open. Ma è un modello generale che puoi usare anche nel tuo codice, per implementare qualsiasi tipo di pulizia delle risorse (à la usingin C #) o transazioni o qualsiasi altra cosa ti venga in mente.

L'unico caso in cui questo non funziona, se l'acquisizione e il rilascio della risorsa sono distribuiti su diverse parti del programma. Ma se è localizzato, come nel tuo esempio, puoi facilmente usare questi blocchi di risorse.


A proposito: nel moderno C #, in usingrealtà è superfluo, perché puoi implementare tu stesso blocchi di risorse in stile Ruby:

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

81
Si noti che, sebbene le ensureistruzioni vengano eseguite per ultime, non sono il valore restituito.
Chris,

30
Adoro vedere ricchi contributi come questo su SO. Va al di là di ciò che l'OP ha chiesto in modo tale da applicarsi a molti più sviluppatori, ma è ancora in discussione. Ho imparato alcune cose da questa risposta + modifiche. Grazie per non aver semplicemente scritto "Sì, ensureviene chiamato qualunque cosa."
Dennis,

3
Si noti che assicurarsi che NON sia garantito il completamento. Prendi il caso in cui hai un inizio / assicurati / fine all'interno di un thread e poi chiami Thread.kill quando viene chiamata la prima riga del blocco di sicurezza. Ciò farà sì che il resto dell'assicurazione non venga eseguito.
Teddy

5
@Teddy: assicurati che inizi l'esecuzione, non è garantito il completamento. Il tuo esempio è eccessivo: una semplice eccezione all'interno del blocco di sicurezza farà sì che anche questo venga chiuso.
Martin Konecny,

3
si noti inoltre che non ci sono garanzie assicurarsi che venga chiamato. Sono serio. Può verificarsi un'interruzione dell'alimentazione / errore hardware / arresto anomalo del sistema operativo e, se il software è critico, anche questo deve essere considerato.
EdvardM,

37

Cordiali saluti, anche se viene sollevata nuovamente un'eccezione nella rescuesezione, il ensureblocco verrà eseguito prima che l'esecuzione del codice continui al gestore delle eccezioni successivo. Per esempio:

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

14

Se vuoi assicurarti che un file sia chiuso, dovresti usare il modulo a blocchi di File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end

3
Immagino che se non si desidera gestire l'errore ma solo sollevarlo e chiudere l'handle del file, non è necessario iniziare il salvataggio qui?
rogerdpack,


5

Sì, ensureASSICURA che venga eseguito ogni volta, quindi non è necessario file.closeil beginblocco.

A proposito, un buon modo per testare è fare:

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

È possibile verificare se "========= all'interno del blocco di sicurezza" verrà stampato in caso di eccezione. Quindi puoi commentare l'affermazione che genera l'errore e vedere se l' ensureistruzione viene eseguita vedendo se qualcosa viene stampato.


4

Ecco perché abbiamo bisogno di ensure:

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

4

Sì, ensurecome finally garantisce che il blocco verrà eseguito . Questo è molto utile per assicurarsi che le risorse critiche siano protette, ad esempio chiudendo un handle di file in caso di errore o rilasciando un mutex.


Tranne nel suo caso, non vi è alcuna garanzia che il file venga chiuso, poiché la File.openparte NON si trova all'interno del blocco iniziale. Solo file.closeè ma non è abbastanza.
Nowaker,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.