¿Comenzar, rescatar y garantizar en Ruby?

547

Recientemente comencé a programar en Ruby, y estoy buscando manejo de excepciones.

Me preguntaba si ensureera el equivalente de Ruby finallyen C #? Debería tener:

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

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

o debería hacer esto?

#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

¿ ensureMe llaman sin importar qué, incluso si no se genera una excepción?

Lloyd Powell
fuente
1
Tampoco es bueno. Como regla general, cuando se trata de recursos externos, siempre desea que la apertura de recursos esté dentro del beginbloque.
Nowaker

Respuestas:

1181

Sí, ensureasegura que el código siempre se evalúa. Por eso se llama ensure. Por lo tanto, es equivalente a Java y C # finally.

El flujo general de begin/ rescue/ else/ ensure/ se endve así:

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

Puedes dejar afuera rescue, ensureo else. También puede omitir las variables, en cuyo caso no podrá inspeccionar la excepción en su código de manejo de excepciones. (Bueno, siempre puede usar la variable de excepción global para acceder a la última excepción que se generó, pero eso es un poco confuso). Y puede omitir la clase de excepción, en cuyo caso se detectarán todas las excepciones que hereden StandardError. (Tenga en cuenta que esto no significa que todas las excepciones son capturadas, porque hay excepciones que son instancias de Exceptionpero no StandardError. Excepciones mayoría muy graves que ponen en peligro la integridad del programa, tales como SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptiono SystemExit)

Algunos bloques forman bloques de excepción implícitos. Por ejemplo, las definiciones de métodos son implícitamente también bloques de excepción, por lo que en lugar de escribir

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

solo escribes

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

o

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

Lo mismo se aplica a las classdefiniciones y moduledefiniciones.

Sin embargo, en el caso específico sobre el que está preguntando, en realidad hay una expresión mucho mejor. En general, cuando trabajas con algún recurso que necesitas limpiar al final, lo haces pasando un bloque a un método que hace toda la limpieza por ti. Es similar a un usingbloque en C #, excepto que Ruby es lo suficientemente poderoso como para no tener que esperar a que los sumos sacerdotes de Microsoft bajen de la montaña y cambien gentilmente su compilador por usted. En Ruby, puedes implementarlo tú mismo:

# 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

Y qué sabes: esto ya está disponible en la biblioteca principal como File.open. Pero es un patrón general que también puede usar en su propio código, para implementar cualquier tipo de limpieza de recursos (a la usingen C #) o transacciones o cualquier otra cosa que pueda pensar.

El único caso donde esto no funciona, si la adquisición y la liberación del recurso se distribuyen en diferentes partes del programa. Pero si está localizado, como en su ejemplo, entonces puede usar fácilmente estos bloques de recursos.


Por cierto: en C # moderno, en usingrealidad es superfluo, porque puede implementar bloques de recursos de estilo Ruby usted mismo:

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
fuente
81
Tenga en cuenta que, aunque las ensuredeclaraciones se ejecutan en último lugar, no son el valor de retorno.
Chris
30
Me encanta ver contribuciones ricas como esta en SO. Va más allá de lo que el OP solicitó, de modo que se aplica a muchos más desarrolladores, pero sigue siendo un tema. Aprendí algunas cosas de esta respuesta + ediciones. Gracias por no solo escribir "Sí, me ensurellaman sin importar qué".
Dennis
3
Tenga en cuenta que NO se garantiza que se complete. Tome el caso de donde tiene un inicio / asegurar / finalizar dentro de un hilo, y luego llama a Thread.kill cuando se llama a la primera línea del bloque de asegurar. Esto hará que el resto de la garantía no se ejecute.
Teddy
55
@Teddy: asegúrese de que está garantizado que comenzará a ejecutarse, no se garantiza que se complete. Su ejemplo es excesivo: una simple excepción dentro del bloque de seguridad hará que también salga.
Martin Konecny
3
También tenga en cuenta que no hay garantías de garantizar que se llama. Lo digo en serio. Puede ocurrir un corte de energía / error de hardware / bloqueo del sistema operativo, y si su software es crítico, eso también debe considerarse.
EdvardM
37

Para su información, incluso si se vuelve a generar una excepción en la rescuesección, el ensurebloque se ejecutará antes de que la ejecución del código continúe con el siguiente controlador de excepciones. Por ejemplo:

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

Si desea asegurarse de que un archivo esté cerrado, debe usar la forma de bloque de File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Farrel
fuente
3
Supongo que si no quieres manejar el error pero solo lo levantas y cierras el archivo, ¿no necesitas comenzar el rescate aquí?
rogerdpack 01 de
5

Sí, ensureASEGURA que se ejecuta siempre, por lo que no necesita file.closeelbegin bloque.

Por cierto, una buena forma de probar es hacer:

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

Puede comprobar si se imprimirá "========= dentro del bloque de seguridad" cuando haya una excepción. Luego, puede comentar la declaración que genera el error y ver si la ensuredeclaración se ejecuta al ver si se imprime algo.

Aaron Qian
fuente
4

Por eso necesitamos 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
fuente
4

Sí, ensurecomo finally garantiza que el bloque se ejecutará . Esto es muy útil para asegurarse de que los recursos críticos estén protegidos, por ejemplo, cerrar un identificador de archivo por error o liberar un mutex.

Chris McCauley
fuente
Excepto en su caso, no hay garantía de que el archivo se cierre, porque la File.openparte NO está dentro del bloque de inicio- garantía . Solo file.closees pero no es suficiente.
Nowaker