Programming Ruby

The Pragmatic Programmer's Guide

Precedente < Indice ^
Prossimo >

Le Eccezioni, Prendere e Gettare



Tempo addietro abbiamo sviluppato codice a Pleasantville, un bellissimo luogo dove nulla veramente va storto. Ogni chiamata alle librerie andava a buon fine, gli utenti non hanno mai inserito codici errati, e le risorse erano ricche ed economiche. Be, c'è qualcosa da cambiare, benvenuti nel mondo reale!

Nel mondo reale, invece, gli errori succedono. I buoni programmi (e di conseguenza i buoni programmatori) tentano di prevenirli,e tentano di buon grado di gestirli. La cosa, purtroppo, non è cosi' semplice come dovrebbe essere. Spesso il codice che determina un errore non ci fornisce indicazioni per sapere come comportarsi di conseguenza. Per esempio, il tentativo di aprire un file che non esiste potrebbe essere accettato in talune circostanze mentre in altre potrebbe essere causare un errore fatale. Come dovrebbe agire il vostro modulo di manipolazione file?

L'approccio tradizionale ètende a far si che vengano restituiti dei codici. Il metodo open restituisce alcuni valori specifici per indicare che la cosa non è andata a buon fine. Questo valore è poi inviato indietro attraverso le propaggini delle routine di chiamata finchè qualcuno non se ne occupa.

Il problema di questo tipo di approccio è che amministrare tutti questi codici di errore potrebbe essere un tormento. Se una funzione chiama open, poi read, e per ultimo close, ed ognuno restituisce un'indicazione d'errore, come potrebbe la funzione distinguere questi codici d'errore dai risultati delle sua chiamata?

Per grandi porzioni, le eccezioni risolvono questo problema. Le eccezioni vi permettono di comprimere l'informazione circa un errore all'interno di un oggetto. Questo oggetto eccezione verrà poi inviato indietro e sistemato automaticamente in cima allo stack delle chiamate fino a quando il sistema trovera il codice che esplicitamente dichiara di sapere come trattare quel tipo di eccezione.

La classe eccezione

Il pacchetto che contiene l'informazione della eccezione è un oggetto della classe Exception, o di una figlia della classe Exception. Ruby predefinisce un'accurata gerarchia di eccezioni, mostrata nella figura 8.1 a pagina 93. Come vedremo in seguito, questa gerarchia rende il trattamento delle eccezioni considerevolmente più semplice.

Figure not available...

Dovendo evocare un'eccezione, potrete utilizzare una delle classi Exception precostituite, o di crearne una da voi. Se ne creerete una, potreste volerla come sottoclasse di StandardError o una delle sue figlie. Se non lo farete, non verrà attivata di default.

Ogni Exception è associata ad una stringa con un messaggio e ad stack backtrace. Se definite le vostre eccezioni, potrete aggiungere anche informazioni addizionali.

Trattare le Eccezioni

Il nostro jukebox scarica canzoni da internet, utilizzando una connessione TCP. Il codice di base è semplice:

opFile = File.open(opName, "w")
while data = socket.read(512)
  opFile.write(data)
end
        

Che cosa succederebbe se ottenessimo un errore fatale a metà strada dell'operazione di download? Sicuramente non vorremo salvare una canzone incompleta tra quelle riproducibili.

Aggiungeremo alcune eccezioni che possano manipolare il codice e tentare di agevolare l'esecuzione. Racchiudiamo il codice che potrebbe sollevare un eccezione in un blocco begin/end e usiamo una clausola rescue per comunicare a Ruby i tipi di eccezioni che vorremo trattare. Nel nostro caso siamo interessati ad intrappolare le eccezioni grazie a SystemCallError ( e, implicitamente, qualsiasi eccezione che sia sottoclasse di SystemCallError), così è come potrebbe apparire la linea rescue. Nel blocco di trattamento dell'errore, riporteremo l'errore stesso, chiuderemo e elimineremo il file di output, per poi risollevare l'eccezione.

opFile = File.open(opName, "w")
begin
  # Le eccezioni sollevate da questo codice saranno
  # prese dalle seguenti clausole rescue
  while data = socket.read(512)
    opFile.write(data)
  end

rescue SystemCallError
  $stderr.print "IO failed: " + $!
  opFile.close
  File.delete(opName)
  raise
end
        

Quando un'eccezione viene sollevata, indipendentemente dal trattamento dell'eccezione, Ruby posiziona un riferimento all'associato oggetto Exception con l'eccezione nella variabile globale $! ( il punto esclamativo presumibilmente stà a significare la nostra sorpresa nel fatto che il nostro codice contenga degli errori ). Nell'esempio precedente, abbiamo usato questa variabile per formattare il nostro messaggio d'errore.

Dopo aver chiuso e cancellato il file, chiameremo raise senza alcun parametro, che risolleva l'eccezione in $!. Questa è una tecnica utile che vi permette di scrivere il codice che filtra le eccezioni, trasmettendo quelle che non potete trattare a livelli più elevati. Risulta essere come implementare una gerarchia di iterazioni per processare gli errori.

Potrete disporre di numerose clausole rescue in un blocco begin, e ogni clausola rescue sarà in grado di specificare eccezioni multiple da intercettare. Alla fine di ogni clausola rescue potrete dare a Ruby il nome di una variabile locale che debba ricevere l'eccezione trovata. Molte persone trovato questa tecnica più leggibile rispetto ad usare ovunque$!.

begin
  eval string
rescue SyntaxError, NameError => boom
  print "String doesn't compile: " + boom
rescue StandardError => bang
  print "Error running script: " + bang
end
        

Come fà Ruby a decidere quali clausole di rescue eseguire? Scopriremo che il processo è assai simile a quello utilizzato dalla dichiarazione case. Per ogni clausola rescue nel blocco begin, Ruby compara l'eccezione sollevata con ogni parametro da confrontare. Se l'eccezione sollevata ne trova uno, Ruby esegue il corpo del rescue e smette di cercare. Il confronto è fatto utilizzando $!.kind_of?(parameter), e riuscirà se il parametro ha la stessa classe dell'eccezione o è un progenitore dell'eccezione stessa. Se scriverete una clausola di rescue senza alcuna lista di parametri, il parametro verrà meno a StandardError.

Se nessuna clausola rescue viene trovata, o se l'eccezione è sollevata all'esterno di un blocco begin/end, Ruby muove sullo stack e cerca un manipolatore di eccezione nel chiamante, poi nel chiamante del chiamante, e così avanti.

Sebbene i parametri della clausola rescue siano in genere i nomi della classe Exception, potrebbero essere anche espressioni arbitrarie ( incluse le chiamate ai metodi ) che restituiscono una classe Exception.

Tidying Up (legando)

Talvolta potreste avere la necessità di garantire che alcuni processi siano terminati alla fine del blocco di codice, senza tener conto dell'elevazione di una eccezione. Per esempio, potreste avere un file aperto per un'accesso ad un blocco, e vorreste essere sicuri che venga chiuso all'uscita dal blocco.

La clausola ensure fà esattamente questo. ensure và posta dopo l'ultima clausola rescue e contiene un pezzo di codice che sarà sempre eseguito al termine del blocco. Non è importante se il blocco esce normalmente, se solleva e libera un'eccezione, oppure se viene terminato da un'eccezione non attivata --- il bocco ensure sarà comunque avviato.

f = File.open("testfile")
begin
  # .. process
rescue
  # .. handle error
ensure
  f.close unless f.nil?
end
        

La clausola else è un costrutto simile, sebbene meno utile. Se presente, verrà posta dopo la clausola rescue e prima di ensure. Il corpo di una clausola else viene eseguito solamente se nessuna eccezione viene sollevata dal blocco principale di codice.

f = File.open("testfile")
begin
  # .. process 
rescue
  # .. handle error
else
  puts "Congratulations-- no errors!"
ensure
  f.close unless f.nil?
end
        

Fallo nuovamente

Talvolta potreste voler correggere la causa di un'eccezione. In questi casi, potreste fruire dell'asserzione retry all'interno di una clausola rescue per ripetere l'intero blocco begin/end. Chiaramente esiste una tremenda possibilità di rendere quì infinito il ciclo, così questa particolarità dovrà essere utilizzata con attenzione ( e naturalmente rimanendo pronti con un dito sul tasto di interruzione )..

Come esempio di codice che reitera un'eccezione, osservate il seguente, adattato dalla libreria net/smtp.rb di Minero Aoki.

@esmtp = true

begin
  # Dapprima tenta un login esteso. Se fallisce poichè 
  # il server non lo supporta, lo trasforma in un 
  # normale login

  if @esmtp then
    @command.ehlo(helodom)
  else
    @command.helo(helodom)
  end

rescue ProtocolError
  if @esmtp then
    @esmtp = false
    retry
  else
    raise
  end
end
        

Questo codice tenta dapprima di connettersi con un server SMTP utilizzando il comando EHLO, che non è universalmente riconosciuto. Se la connessione fallisce, il codice setta la variabile @esmtp in false e riprova la connessione.Se fallisce nuovamente, viene sollevata un'eccezione al chiamante.

Sollevare le eccezioni

Finora siamo rimasti sulla difensiva, manipolando eccezioni sollevate da altri. E' giunta l'ora di passare all'offensiva (esistono persone che affermano che i vostri autori sono sempre offensivi, ma questo è un altra storia).

Potreste sollevare eccezioni nel vostro codice con il metodo Kernel::raise.

raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
        

Il primo form risolleva semplicemente l'eccezione corrente ( un RuntimeError se non esistono eccezioni). Questo è usato nei manipolatori di eccezioni che hanno necessità di intercettare un'eccezione prima di passarla.

Il secondo form crea una nuova eccezione RuntimeError, settando il proprio messaggio alla stringa data. Questa eccezione è poi sollevata fino allo stack di calcolo.

Il terzo form utilizza il primo argomento per creare una eccezione e poi setta il messaggio associato al secondo argomento e il tracciato di stack al terzo argomento. In genere il primo argomento sarà anche il nome della classe nella gerarchia dell' Exception o un riferimento ad un'istanza oggetto di una di queste classi. [Tecnicamente, questo argomento potrebbe essere qualsiasi oggetto che risponde al messaggio exception restituendo un oggetto purchè object.kind_of?(Exception) sia vero.]Il tracciato di stack è normalmente prodotto utilizzando il metodo Kernel::caller.

Di seguito alcuni esempi tipici di raise in azione.

raise

raise "Missing name" if name.nil?

if i >= myNames.size
  raise IndexError, "#{i} >= size (#{myNames.size})"
end

raise ArgumentError, "Name too big", caller
        

Nell'ultimo esempio, abbiamo rimosso la routine corrente dal tracciato dello stack, che è spesso utilizzato nei moduli delle librerie. Possiamo andare oltre: il codice seguente rimuove due routine dal tracciato.

raise ArgumentError, "Name too big", caller[1..-1]
        

Aggiungere Informazioni alle Eccezioni

Potrete definire le vostre eccezioni conservando qualsiasi informazione che vi serva recuperare dal luogo dell'errore. Per esempio, certi tipi di errori di network potrebbero verificarsi a seconda delle circostanze. Se l'errore avvenisse, e le circostanze corrette, potreste mettere un flag nell'eccezione per comunicare al manipolatore che sarebbe opportuno ripetere l'operazione.

class RetryException < RuntimeError
  attr :okToRetry
  def initialize(okToRetry)
    @okToRetry = okToRetry
  end
end
        

In qualche punto delle profondità del codice, è avvenuto un errore transitorio.

def readData(socket)
  data = socket.read(512)
  if data.nil?
    raise RetryException.new(true), "transient read error"
  end
  # .. normal processing
end
        

In cima allo stack, manipoliamo l'eccezione

begin
  stuff = readData(socket)
  # .. process stuff
rescue RetryException => detail
  retry if detail.okToRetry
  raise
end
        

Prendere e Gettare

Sebbene il meccanismo delle eccezioni di raise e rescue sia troppo importante per abbandonarne l'esecuzione, se le cose dovessero andare male, talvolta potrebbe tornare utile essere in grado di uscire da alcuni costrutti troppo complessi e noiosi durante la normale esecuzione. Questa è la situazione in cui catch e throw risultano utili..

catch (:done)  do
  while gets
    throw :done unless fields = split(/\t/)
    songList.add(Song.new(*fields))
  end
  songList.play
end
        

catch definisce un blocco marcato con un nome preciso (che potrebbe essere Symbol o una String). Il blocco viene eseguito normalmente fino ad incontrare un throw.

Quando Ruby incontra un throw, torna indietro velocemente fino allo stack cercando un blocco catch con un simbolo di confronto. Quando lo trova, Ruby scarica lo stack fino a quel punto e termina il blocco. Se il throw è chiamato con un secondo parametro opzionale, quel valore viene restituito come il valore di catch. In questo modo, nell'esempio precedente, se l'input non contenesse linee formattate correttamente, il throw salterebbe alla fine del corrispondente catch, non solamente terminando il loop while ma anche evitando di mostrare la lista delle canzoni.

L'esempio seguente utilizza throw per terminare l'iterazione con lo user se ``!'' è digitato in risposta a qualsiasi prompt.

def promptAndGet(prompt)
  print prompt
  res = readline.chomp
  throw :quitRequested if res == "!"
  return res
end

catch :quitRequested do
  name = promptAndGet("Name: ")
  age  = promptAndGet("Age:  ")
  sex  = promptAndGet("Sex:  ")
  # ..
  # process information
end
        

Come questo esempio illustra, throw non deve apparire all'interno dell'ambito statico di catch.


Precedente < Indice ^
Prossimo >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0.
This reference is available for download.