Rozpocząć, uratować i zapewnić w Ruby?


547

Ostatnio zacząłem programować w Ruby i patrzę na obsługę wyjątków.

Zastanawiałem się, czy ensureto odpowiednik Rubiegofinally w C #? Czy powinienem mieć:

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

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

czy powinienem to zrobić?

#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

Czy ensurezostanie wywołany bez względu na wszystko, nawet jeśli wyjątek nie zostanie zgłoszony?


1
Nic nie jest dobre. Z reguły, mając do czynienia z zasobami zewnętrznymi, zawsze chcesz, aby otwarcie zasobu znajdowało się wewnątrz beginbloku.
Nowaker,

Odpowiedzi:


1181

Tak, ensurezapewnia, że ​​kod jest zawsze analizowany. Dlatego się nazywa ensure. Jest to więc odpowiednik Javy i C # finally.

Ogólny przepływ begin/ rescue/ else/ ensure/ endwygląda następująco:

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

Można pominąć rescue, ensurelub else. Możesz także pominąć zmienne, w którym to przypadku nie będziesz mógł sprawdzić wyjątku w kodzie obsługi wyjątków. (Cóż, zawsze możesz użyć globalnej zmiennej wyjątku, aby uzyskać dostęp do ostatniego zgłoszonego wyjątku, ale to jest trochę zhackowane.) I możesz pominąć klasę wyjątków, w którym to przypadku StandardErrorwychwycone zostaną wszystkie wyjątki, które dziedziczą . (Należy pamiętać, że to nie oznacza, że wszystkie wyjątki zostały złowione, ponieważ istnieją wyjątki, które są przypadki Exception, ale nie StandardError. Głównie bardzo poważne wyjątki że kompromis integralność programu, takie jak SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, lubInterrupt ,SignalExceptionSystemExit.)

Niektóre bloki tworzą niejawne bloki wyjątków. Na przykład definicje metod są domyślnie także blokami wyjątków, więc zamiast pisania

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

piszesz tylko

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

lub

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

To samo dotyczy classdefinicji i moduledefinicji.

Jednak w konkretnym przypadku, o który pytasz, istnieje o wiele lepszy idiom. Ogólnie rzecz biorąc, gdy pracujesz z jakimś zasobem, który musisz oczyścić na końcu, robisz to, przekazując blok metodzie, która wykonuje za Ciebie całe czyszczenie. Jest podobny do usingbloku w języku C #, z tym wyjątkiem, że Ruby jest wystarczająco potężny, abyś nie musiał czekać na najwyższych kapłanów Microsoftu, którzy zejdą z góry i łaskawie zmienią dla ciebie kompilator. W Ruby możesz to po prostu zaimplementować:

# 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

A co wiesz: jest to już dostępne w podstawowej bibliotece jako File.open. Ale jest to ogólny wzorzec, którego można również użyć we własnym kodzie, do implementacji wszelkiego rodzaju czyszczenia zasobów (à la usingw języku C #) lub transakcji lub cokolwiek innego, co możesz wymyślić.

Jedyny przypadek, w którym to nie działa, jeśli pozyskiwanie i zwalnianie zasobów jest rozłożone na różne części programu. Ale jeśli jest zlokalizowane, jak w twoim przykładzie, możesz łatwo użyć tych bloków zasobów.


BTW: we współczesnym języku C # usingjest w rzeczywistości zbędny, ponieważ możesz samodzielnie implementować bloki zasobów w stylu 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
Należy zauważyć, że chociaż ensureinstrukcje są wykonywane jako ostatnie, nie są one wartością zwracaną.
Chris

30
Uwielbiam widzieć tak bogaty wkład w SO. Wykracza poza to, o co poprosił PO, dlatego ma zastosowanie do wielu innych programistów, ale wciąż jest na ten temat. Nauczyłem się kilku rzeczy z tej odpowiedzi + edycji. Dziękuję, że nie napisałeś tylko: „Tak, ensuredzwonię bez względu na wszystko”.
Dennis

3
Pamiętaj, że zagwarantowanie tego NIE jest gwarantowane. Weźmy przypadek, w którym masz początek / zapewnij / koniec w wątku, a następnie wywołujesz Thread.kill, gdy wywoływana jest pierwsza linia bloku upewnienia się. Spowoduje to, że reszta programu nie zostanie wykonana.
Teddy

5
@Teddy: upewnij się, że rozpoczęcie wykonywania nie jest gwarantowane, a nie ukończenie. Twój przykład to przesada - prosty wyjątek w bloku upewnienia się, że również wyjdzie.
Martin Konecny

3
Zwróć także uwagę, że nie ma żadnych gwarancji, że zadzwonisz. Jestem poważny. Może wystąpić awaria zasilania / błąd sprzętowy / awaria systemu operacyjnego, a jeśli oprogramowanie jest krytyczne, należy to również wziąć pod uwagę.
EdvardM,

37

Do Twojej wiadomości, nawet jeśli wyjątek zostanie ponownie zgłoszony w rescuesekcji, ensureblok zostanie wykonany, zanim wykonanie kodu przejdzie do następnego modułu obsługi wyjątków. Na przykład:

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

14

Jeśli chcesz upewnić się, że plik jest zamknięty, powinieneś użyć formy blokowej File.open:

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

3
Myślę, że jeśli nie chcesz poradzić sobie z błędem, ale po prostu go podnieś i zamknij uchwyt pliku, nie potrzebujesz tutaj ratowania?
rogerdpack


5

Tak, ensureZAPEWNIA, że jest uruchamiany za każdym razem, więc nie potrzebujesz go file.closew beginbloku.

Nawiasem mówiąc, dobrym sposobem na przetestowanie jest wykonanie:

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

Możesz sprawdzić, czy „========= wewnątrz upewnij się, że blok” zostanie wydrukowany, gdy wystąpi wyjątek. Następnie możesz skomentować instrukcję, która powoduje błąd, i sprawdzić, czy ensureinstrukcja jest wykonywana, sprawdzając, czy coś zostanie wydrukowane.


4

Dlatego potrzebujemy 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

Tak, ensurepodobnie jak finally gwarancje, że blok zostanie wykonany . Jest to bardzo przydatne do zapewnienia ochrony krytycznych zasobów, np. Zamknięcia dojścia do pliku w przypadku błędu lub zwolnienia muteksu.


Z wyjątkiem jego / jej przypadku, nie ma gwarancji, że plik zostanie zamknięty, ponieważ File.openczęść NIE znajduje się w bloku początkowym. Tylko file.closejest, ale to nie wystarczy.
Nowaker
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.