Comece, salve e garanta no Ruby?

547

Recentemente, comecei a programar em Ruby e estou olhando para o tratamento de exceções.

Eu queria saber se ensureera o equivalente Ruby finallyem c #? Eu deveria ter:

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

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

ou devo fazer isso?

#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

É ensurechamado, não importa como, mesmo que uma exceção não seja levantada?

Lloyd Powell
fonte
1
Nem é bom. Como regra geral, ao lidar com recursos externos, você sempre deseja que a abertura do recurso esteja dentro do beginbloco.
Nowaker

Respostas:

1181

Sim, ensuregarante que o código seja sempre avaliado. É por isso que é chamado ensure. Portanto, é equivalente a Java e C # 's finally.

O fluxo geral de begin/ rescue/ else/ ensure/ endé assim:

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

Você pode deixar de fora rescue, ensureou else. Você também pode deixar de fora as variáveis ​​e, nesse caso, não poderá inspecionar a exceção no seu código de manipulação de exceções. (Bem, você sempre pode usar a variável de exceção global para acessar a última exceção que foi gerada, mas isso é um pouco hacky.) E você pode deixar de fora a classe de exceção. Nesse caso, todas as exceções herdadas StandardErrorserão capturadas. (Por favor note que este não significa que todas as exceções são capturadas, porque há exceções que são instâncias de Exception, mas não StandardError. Exceções Principalmente muito graves que comprometem a integridade do programa, como SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionou SystemExit.)

Alguns blocos formam blocos de exceção implícitos. Por exemplo, definições de método são implicitamente também blocos de exceção, portanto, em vez de escrever

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

você escreve apenas

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

ou

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

O mesmo se aplica às classdefinições e moduledefinições.

No entanto, no caso específico que você está perguntando, existe realmente um idioma muito melhor. Em geral, quando você trabalha com algum recurso que precisa limpar no final, faz isso passando um bloco para um método que faz toda a limpeza para você. É semelhante a um usingbloco em C #, exceto que Ruby é realmente poderoso o suficiente para que você não precise esperar que os sumos sacerdotes da Microsoft desçam da montanha e graciosamente alterem seu compilador para você. No Ruby, você pode simplesmente implementá-lo:

# 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 o que você sabe: isso está disponível na biblioteca principal como File.open. Mas é um padrão geral que você também pode usar em seu próprio código, para implementar qualquer tipo de limpeza de recurso (à la usingem C #) ou transações ou qualquer outra coisa que você possa imaginar.

O único caso em que isso não funciona, se a aquisição e a liberação do recurso forem distribuídas por diferentes partes do programa. Mas se estiver localizado, como no seu exemplo, você poderá usar facilmente esses blocos de recursos.


BTW: no C # moderno, usingé realmente supérfluo, porque você pode implementar os blocos de recursos no estilo 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);
});
Jörg W Mittag
fonte
81
Observe que, embora as ensureinstruções sejam executadas por último, elas não são o valor de retorno.
21413 Chris
30
Adoro ver contribuições ricas como esta no SO. Ele vai além do que o OP pediu, de modo que se aplique a muito mais desenvolvedores, mas ainda está no tópico. Aprendi algumas coisas com esta resposta + edições. Obrigado por não apenas escrever "Sim, ensureé chamado, não importa o quê."
Dennis
3
Observe que essa garantia NÃO é garantida para ser concluída. Tome o caso de onde você tem um início / garantia / fim dentro de um encadeamento e chame Thread.kill quando a primeira linha do bloco de garantir estiver sendo chamada. Isso fará com que o restante da garantia não seja executado.
Teddy
5
@Teddy: garantir que é garantido o início da execução e não a conclusão. Seu exemplo é um exagero - uma exceção simples dentro do bloco de garantia fará com que ele saia também.
Martin Konecny
3
Observe também que não há garantias de garantir que seja chamado. Estou falando sério. Pode ocorrer uma falha de energia / erro de hardware / falha do sistema operacional e, se o seu software for crítico, isso também deverá ser considerado.
EdvardM 2/10
37

Para sua informação, mesmo que uma exceção seja levantada novamente na rescueseção, o ensurebloco será executado antes que a execução do código continue no próximo manipulador de exceções. Por exemplo:

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

Se você deseja garantir que um arquivo seja fechado, use o formato de bloco de File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Farrel
fonte
3
Eu acho que se você não quiser lidar com o erro, mas apenas aumentá-lo e fechar o identificador de arquivo, não precisa do resgate inicial aqui?
Rogerdpack #
7

Sim, ensureé chamado em qualquer circunstância. Para obter mais informações, consulte " Exceções, capturas e lançamentos " do livro Ruby de programação e procure por "garantir".

Milan Novota
fonte
5

Sim, ensureASSEGURA que seja executado sempre, para que você não precise file.closedo beginbloco.

A propósito, uma boa maneira de testar é fazer:

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

Você pode testar para ver se "========= dentro garantir bloco" será impresso quando houver uma exceção. Em seguida, você pode comentar a instrução que gera o erro e verificar se a ensureinstrução é executada, verificando se alguma coisa é impressa.

Aaron Qian
fonte
4

É por isso que precisamos 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
fonte
4

Sim, ensurecomo finally garantias de que o bloco será executado . Isso é muito útil para garantir a proteção de recursos críticos, como fechar um identificador de arquivo por erro ou liberar um mutex.

Chris McCauley
fonte
Exceto no caso dele / dela, não há garantia de que o arquivo seja fechado, porque File.openparte NÃO está dentro do bloco de garantia inicial. Só file.closeé, mas não é suficiente.
Nowaker