Commencer, sauver et assurer dans Ruby?

547

J'ai récemment commencé à programmer dans Ruby et je regarde la gestion des exceptions.

Je me demandais si ensureétait l'équivalent Ruby finallyen C #? Dois-je avoir:

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

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

ou devrais-je faire cela?

#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

Est- ensureil appelé quoi qu'il arrive, même si aucune exception n'est déclenchée?

Lloyd Powell
la source
1
Ni l'un ni l'autre n'est bon. En règle générale, lorsque vous traitez avec des ressources externes, vous souhaitez toujours que l'ouverture des ressources se trouve à l'intérieur du beginbloc.
Nowaker

Réponses:

1181

Oui, ensuregarantit que le code est toujours évalué. Voilà pourquoi ça s'appelle ensure. Donc, c'est équivalent à Java et C # finally.

Le flux général de begin/ rescue/ else/ ensure/ endressemble à ceci:

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

Vous pouvez laisser de côté rescue, ensureou else. Vous pouvez également laisser de côté les variables, auquel cas vous ne pourrez pas inspecter l'exception dans votre code de gestion des exceptions. (Eh bien, vous pouvez toujours utiliser la variable d'exception globale pour accéder à la dernière exception qui a été déclenchée, mais c'est un peu hacky.) Et vous pouvez laisser de côté la classe d'exception, auquel cas toutes les exceptions qui héritent de StandardErrorseront interceptées. (S'il vous plaît noter que cela ne signifie pas que toutes les exceptions sont prises, car il y a des exceptions qui sont des instances de Exceptionnon StandardError. La plupart du temps des exceptions très graves qui compromettent l'intégrité du programme tel que SystemStackError, NoMemoryError,SecurityError , NotImplementedError, LoadError, SyntaxError, ScriptError, Interrupt,SignalExceptionou SystemExit.)

Certains blocs forment des blocs d'exception implicites. Par exemple, les définitions de méthode sont implicitement aussi des blocs d'exception, donc au lieu d'écrire

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

tu écris juste

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

ou

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

Il en va de même pour les classdéfinitions et moduledéfinitions.

Cependant, dans le cas spécifique que vous posez, il existe en fait un bien meilleur idiome. En général, lorsque vous travaillez avec une ressource que vous devez nettoyer à la fin, vous le faites en passant un bloc à une méthode qui fait tout le nettoyage pour vous. C'est similaire à un usingbloc en C #, sauf que Ruby est en fait assez puissant pour que vous n'ayez pas à attendre que les grands prêtres de Microsoft descendent de la montagne et changent gracieusement leur compilateur pour vous. Dans Ruby, vous pouvez simplement l'implémenter vous-même:

# 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

Et que savez-vous: c'est déjà disponible dans la bibliothèque de base en tant que File.open. Mais c'est un modèle général que vous pouvez également utiliser dans votre propre code, pour implémenter tout type de nettoyage de ressources (à la usingen C #) ou de transactions ou tout ce que vous pourriez penser.

Le seul cas où cela ne fonctionne pas, si l'acquisition et la libération de la ressource sont réparties sur différentes parties du programme. Mais s'il est localisé, comme dans votre exemple, vous pouvez facilement utiliser ces blocs de ressources.


BTW: en C # moderne, usingest en fait superflu, car vous pouvez implémenter vous-même des blocs de ressources de style 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
la source
81
Notez que, bien que les ensureinstructions soient exécutées en dernier, elles ne sont pas la valeur de retour.
Chris
30
J'adore voir de riches contributions comme celle-ci sur SO. Il va au-delà de ce que le PO a demandé, de sorte qu'il s'applique à de nombreux autres développeurs, mais est toujours d'actualité. J'ai appris quelques choses de cette réponse + modifications. Merci de ne pas avoir simplement écrit "Oui, ensureon t'appelle quoi qu'il arrive".
Dennis
3
Notez que cette garantie n'est PAS garantie de se terminer. Prenez le cas où vous avez un début / assurer / fin à l'intérieur d'un thread, puis vous appelez Thread.kill lorsque la première ligne du bloc assure est appelée. Cela entraînera le reste de la garantie à ne pas s'exécuter.
Teddy
5
@Teddy: assurez-vous que le début de l'exécution est garanti, et non la fin. Votre exemple est excessif - une simple exception à l'intérieur du bloc assure entraînera également sa sortie.
Martin Konecny
3
noter également qu'il n'y a aucune garantie que l'appel soit appelé. Je suis serieux. Une panne de courant / une erreur matérielle / un blocage du système d'exploitation peut se produire, et si votre logiciel est critique, cela doit également être pris en compte.
EdvardM
37

Pour info, même si une exception est re-levée dans la rescuesection, le ensurebloc sera exécuté avant que l'exécution du code ne continue vers le gestionnaire d'exceptions suivant. Par exemple:

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

Si vous voulez vous assurer qu'un fichier est fermé, vous devez utiliser la forme de bloc de File.open:

File.open("myFile.txt", "w") do |file|
  begin
    file << "#{content} \n"
  rescue
  #handle the error here
  end
end
Farrel
la source
3
Je suppose que si vous ne voulez pas gérer l'erreur mais simplement la soulever et fermer le descripteur de fichier, vous n'avez pas besoin de commencer le sauvetage ici?
rogerdpack
7

Oui, ensureest appelé en toutes circonstances. Pour plus d'informations, voir « Exceptions, Catch et Throw » du livre Programming Ruby et recherchez «sure».

Milan Novota
la source
5

Oui, ensureASSURE qu'il est exécuté à chaque fois, vous n'avez donc pas besoin de le file.closedans le beginbloc.

Au fait, une bonne façon de tester est de faire:

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

Vous pouvez tester pour voir si "========= intérieur assurez-vous que le bloc" sera imprimé en cas d'exception. Vous pouvez ensuite commenter l'instruction qui déclenche l'erreur et voir si l' ensureinstruction est exécutée en voyant si quelque chose est imprimé.

Aaron Qian
la source
4

C'est pourquoi nous avons besoin de 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
la source
4

Oui, ensurecomme finally garantit que le bloc sera exécuté . Ceci est très utile pour s'assurer que les ressources critiques sont protégées, par exemple en fermant un descripteur de fichier en cas d'erreur ou en libérant un mutex.

Chris McCauley
la source
Sauf dans son cas, il n'y a aucune garantie que le fichier soit fermé, car une File.openpartie n'est PAS dans le bloc begin-sure. Seulement, file.closemais ce n'est pas suffisant.
Nowaker