|
|||
Precedente < | Indice ^ |
Prossimo
> |
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.
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.
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
.
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 |
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.
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] |
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 |
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.