In Ruby beginnen, retten und sicherstellen?

547

Ich habe vor kurzem angefangen, in Ruby zu programmieren, und ich beschäftige mich mit der Ausnahmebehandlung.

Ich habe mich gefragt, ob ensuredas Ruby-Äquivalent finallyin C # ist. Sollte ich haben:

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

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

oder soll ich das machen

#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

Does ensureerhalten egal genannt , was, auch wenn eine Ausnahme nicht erhoben wird?

Lloyd Powell
quelle
1
Weder ist gut. Wenn Sie mit externen Ressourcen arbeiten, möchten Sie in der Regel immer, dass sich die Ressourcenöffnung innerhalb des beginBlocks befindet.
Nowaker

Antworten:

1181

Ja, ensurestellt sicher, dass der Code immer ausgewertet wird. Deshalb heißt es ensure. Es ist also äquivalent zu Java und C # finally.

Der allgemeine Ablauf von begin/ rescue/ else/ ensure/ endsieht folgendermaßen aus:

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

Sie können weglassen rescue, ensureoder else. Sie können auch die Variablen weglassen. In diesem Fall können Sie die Ausnahme in Ihrem Ausnahmebehandlungscode nicht überprüfen. (Nun, Sie können immer die globale Ausnahmevariable verwenden, um auf die letzte Ausnahme zuzugreifen, die ausgelöst wurde, aber das ist ein bisschen hackig.) Und Sie können die Ausnahmeklasse weglassen. In diesem Fall werden alle Ausnahmen StandardErrorabgefangen , von denen geerbt wird. (Bitte beachten Sie, dass dies nicht bedeutet , dass alle Ausnahmen abgefangen werden, denn es gibt Ausnahmen , die Instanzen sind Exceptionaber nicht StandardError. Meistens ist sehr schwere Ausnahmen , dass ein Kompromiss der Integrität des Programms wie SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionoder SystemExit.)

Einige Blöcke bilden implizite Ausnahmeblöcke. Beispielsweise sind Methodendefinitionen implizit auch Ausnahmeblöcke, anstatt zu schreiben

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

du schreibst nur

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

oder

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

Gleiches gilt für classDefinitionen und moduleDefinitionen.

In dem speziellen Fall, nach dem Sie fragen, gibt es jedoch tatsächlich eine viel bessere Sprache. Wenn Sie mit einer Ressource arbeiten, die Sie am Ende bereinigen müssen, übergeben Sie im Allgemeinen einen Block an eine Methode, die die gesamte Bereinigung für Sie durchführt. Es ähnelt einem usingBlock in C #, nur dass Ruby tatsächlich so mächtig ist, dass Sie nicht darauf warten müssen, dass die Hohepriester von Microsoft vom Berg herunterkommen und ihren Compiler für Sie ändern. In Ruby können Sie es einfach selbst implementieren:

# 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

Und was wissen Sie: Dies ist bereits in der Kernbibliothek als verfügbar File.open. Es ist jedoch ein allgemeines Muster, das Sie auch in Ihrem eigenen Code verwenden können, um jede Art von Ressourcenbereinigung (à la usingin C #) oder Transaktionen oder was auch immer Sie sonst denken , zu implementieren .

Der einzige Fall, in dem dies nicht funktioniert, wenn der Erwerb und die Freigabe der Ressource auf verschiedene Teile des Programms verteilt sind. Wenn es jedoch wie in Ihrem Beispiel lokalisiert ist, können Sie diese Ressourcenblöcke problemlos verwenden.


Übrigens: Im modernen C # usingist dies eigentlich überflüssig, da Sie Ressourcenblöcke im Ruby-Stil selbst implementieren können:

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
quelle
81
Beachten Sie, dass die ensureAnweisungen zwar zuletzt ausgeführt werden, jedoch nicht der Rückgabewert sind.
Chris
30
Ich liebe es, solche reichen Beiträge auf SO zu sehen. Es geht über das hinaus, was das OP verlangt hat, so dass es für viel mehr Entwickler gilt, aber immer noch zum Thema gehört. Ich habe ein paar Dinge aus dieser Antwort + Änderungen gelernt. Vielen Dank, dass Sie nicht nur geschrieben haben: "Ja, ensurewird angerufen, egal was passiert."
Dennis
3
Beachten Sie, dass die Vervollständigung NICHT garantiert ist. Nehmen Sie den Fall, dass Sie einen Anfang / Sicherstellen / Ende innerhalb eines Threads haben, und rufen Sie dann Thread.kill auf, wenn die erste Zeile des Sicherstellungsblocks aufgerufen wird. Dies führt dazu, dass der Rest der Sicherstellung nicht ausgeführt wird.
Teddy
5
@ Teddy: Stellen Sie sicher, dass die Ausführung garantiert beginnt und nicht abgeschlossen wird. Ihr Beispiel ist Overkill - eine einfache Ausnahme innerhalb des Sicherstellungsblocks führt dazu, dass er ebenfalls beendet wird.
Martin Konecny
3
Beachten Sie auch, dass es keine Garantien gibt, die sicherstellen, dass aufgerufen wird. Ich meine es ernst. Ein Stromausfall / Hardwarefehler / Betriebssystemabsturz kann auftreten. Wenn Ihre Software kritisch ist, muss dies ebenfalls berücksichtigt werden.
EdvardM
37

Zu Ihrer Information, selbst wenn eine Ausnahme in dem rescueAbschnitt erneut ensureausgelöst wird , wird der Block ausgeführt, bevor die Codeausführung zum nächsten Ausnahmebehandler fortgesetzt wird. Zum Beispiel:

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

Wenn Sie sicherstellen möchten, dass eine Datei geschlossen ist, verwenden Sie die Blockform von File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Farrel
quelle
3
Ich denke, wenn Sie den Fehler nicht behandeln möchten, sondern ihn nur auslösen und das Dateihandle schließen möchten, brauchen Sie hier nicht die Rettungsaktion?
Rogerdpack
7

Ja, ensurewird unter keinen Umständen aufgerufen. Weitere Informationen finden Sie unter " Ausnahmen, Fangen und Werfen " im Ruby-Programmierbuch. Suchen Sie nach "Sicherstellen".

Milan Novota
quelle
5

Ja, stellt ensuresicher , dass es jedes Mal ausgeführt wird, sodass Sie es nicht file.closeim beginBlock benötigen .

Ein guter Weg zum Testen ist übrigens:

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

Sie können testen, ob "========= inside sure block" gedruckt wird, wenn eine Ausnahme vorliegt. Dann können Sie die Anweisung auskommentieren, die den Fehler auslöst, und prüfen, ob die ensureAnweisung ausgeführt wird, indem Sie prüfen, ob etwas ausgedruckt wird.

Aaron Qian
quelle
4

Deshalb brauchen wir 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
quelle
4

Ja, ensurewie finally garantiert, dass der Block ausgeführt wird . Dies ist sehr nützlich, um sicherzustellen, dass kritische Ressourcen geschützt sind, z. B. das Schließen eines Dateihandles bei Fehlern oder das Freigeben eines Mutex.

Chris McCauley
quelle
Außer in seinem Fall gibt es keine Garantie dafür, dass die Datei geschlossen wird, da sich ein File.openTeil NICHT im Start-Sicherstellungsblock befindet. Nur file.closeist, aber es ist nicht genug.
Nowaker