Начать, спасти и убедиться в Ruby?

547

Я недавно начал программировать на Ruby, и я смотрю на обработку исключений.

Мне было интересно, ensureбыл ли Ruby эквивалент finallyв C #? Должен ли я иметь:

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

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

или я должен это сделать?

#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

Вам ensureзвонят, несмотря ни на что, даже если исключение не выдвинуто?

Ллойд Пауэлл
источник
1
Ни то, ни другое Как правило, при работе с внешними ресурсами вы всегда хотите, чтобы открытие ресурса находилось внутри beginблока.
Новакер

Ответы:

1181

Да, ensureгарантирует, что код всегда оценивается. Вот почему это называется ensure. Таким образом, это эквивалентно Java и C # finally.

Общий поток begin/ rescue/ else/ ensure/ endвыглядит так:

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

Вы можете оставить rescue, ensureили else. Вы также можете пропустить переменные, в этом случае вы не сможете проверить исключение в своем коде обработки исключений. (Ну, вы всегда можете использовать глобальную переменную исключения для доступа к последнему исключению, которое было сгенерировано, но это немного странно.) И вы можете пропустить класс исключений, и в этом случае все исключения, наследуемые от, StandardErrorбудут перехвачены. (Пожалуйста , обратите внимание , что это не означает , что все исключения пойманы, потому что есть исключения , которые являются экземплярами , Exceptionно не StandardError. В основном очень серьезные исключения , которые ставят под угрозу целостность программы , такие как SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionили SystemExit.)

Некоторые блоки образуют неявные блоки исключений. Например, определения методов неявно являются также блоками исключений, поэтому вместо записи

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

ты пишешь просто

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

или

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

То же самое относится к classопределениям и moduleопределениям.

Однако в конкретном случае, о котором вы спрашиваете, на самом деле существует гораздо лучшая идиома. В общем, когда вы работаете с каким-то ресурсом, который необходимо очистить в конце, вы делаете это, передавая блок методу, который выполняет всю очистку за вас. Это похоже на usingблок в C #, за исключением того, что Ruby на самом деле достаточно мощный, чтобы вам не приходилось ждать, когда первосвященники Microsoft спустятся с горы и милостиво поменяют свой компилятор для вас. В Ruby вы можете просто реализовать это самостоятельно:

# 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

И что вы знаете: это уже доступно в основной библиотеке как File.open. Но это общий шаблон, который вы также можете использовать в своем собственном коде для реализации любого вида очистки ресурсов (как usingв C #) или транзакций или чего-либо еще, о чем вы могли подумать.

Единственный случай, когда это не работает, если приобретение и выпуск ресурса распределены по разным частям программы. Но если он локализован, как в вашем примере, вы можете легко использовать эти блоки ресурсов.


Кстати, в современном C # usingэто на самом деле излишне, потому что вы можете реализовать блоки ресурсов в стиле 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
Обратите внимание, что хотя ensureоператоры выполняются последними, они не являются возвращаемым значением.
Крис
30
Мне нравится видеть такой большой вклад на SO. Он выходит за рамки того, о чем спрашивал ОП, так что он применим ко многим другим разработчикам, но все еще остается темой. Из этого ответа я узнал несколько вещей + правки. Спасибо за то, что вы не просто написали: «Да, вам ensureпозвонят, несмотря ни на что».
Деннис
3
Обратите внимание, что гарантия НЕ гарантированно завершена. Возьмите случай, когда у вас есть начало / обеспечение / конец внутри потока, а затем вы вызываете Thread.kill, когда вызывается первая строка блока обеспечения. Это приведет к тому, что остальная часть обеспечения не будет выполнена.
Тедди
5
@Teddy: гарантированно начнется выполнение, а не завершение. Ваш пример излишний - простое исключение внутри блока обеспечения также приведет к его выходу.
Мартин Конечни
3
Также обратите внимание, что нет никаких гарантий, что это называется. Я серьезно. Может произойти сбой питания / аппаратная ошибка / сбой операционной системы, и если ваше программное обеспечение критично, это тоже необходимо учитывать.
EdvardM
37

К вашему сведению, даже если исключение повторно вызывается в rescueразделе, ensureблок будет выполнен до того, как выполнение кода продолжится до следующего обработчика исключений. Например:

begin
  raise "Error!!"
rescue
  puts "test1"
  raise # Reraise exception
ensure
  puts "Ensure block"
end
ALUP
источник
14

Если вы хотите убедиться, что файл закрыт, вы должны использовать блочную форму File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Фаррел
источник
3
Я думаю, если вы не хотите обрабатывать ошибку, а просто поднять ее и закрыть дескриптор файла, вам не нужно начинать спасение здесь?
rogerdpack
7

Да, ensureназывается при любых обстоятельствах. Для получения дополнительной информации см. « Исключения, поймать и выбросить » книги по программированию на Ruby и выполнить поиск «обеспечить».

Милан Новота
источник
5

Да, ensureгарантирует, что он запускается каждый раз, поэтому вам не нужно file.closeв beginблоке.

Кстати, хороший способ проверить это сделать:

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

Вы можете проверить, будет ли "========= внутри блока гарантированности" распечатываться при возникновении исключения. Затем вы можете закомментировать оператор, который вызывает ошибку, и посмотреть, выполняется ли ensureоператор, посмотрев, распечатано ли что-нибудь.

Аарон Цянь
источник
4

Вот почему нам нужно 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
источник
4

Да, ensureвроде finally гарантирует, что блок будет выполнен . Это очень полезно для обеспечения защиты критических ресурсов, например, закрытия дескриптора файла при ошибке или освобождения мьютекса.

Крис МакКоли
источник
За исключением его / ее случая, нет гарантии, что файл будет закрыт, потому что File.openчасть НЕ находится внутри блока начала-обеспечения. Только file.closeесть, но этого недостаточно.
Новакер