La Torre di BaBasic - 6
La Torre di BaBasic - 6
La prevedibile scoperta del professor Boehm ci serve ad introdurre un argomento spesso lasciato in penombra: il trattamento delle situazioni di errore dentro un programma, meglio conosciuto nel gergo informatico come trattamento delle eccezioni.
Se il vostro videogioco preferito - L'Invasione delle Tartarughe Teenager Radioattive Ninja e Mutanti - decidesse all'improvviso di bloccarsi proprio all'entrata del diciottesimo schermo, quando il vostro personaggio sta per affrontare l'ultimo avversario che lo separa dalla vittoria, probabilmente potreste emettere più di un mugolio di disappunto, e non è escluso che finiate per assestare al vostro joystick il colpo finale che lo separava da una meritatissima pensione. Tuttavia dovete riconoscere che il danno sarebbe più serio se il programma che si inceppa sul più bello fosse quello che controlla i processi di fissione atomica nella centrale nucleare più vicina a casa vostra.
Quel programma non deve incepparsi, mai. Non deve incepparsi se il gatto del guardiano ha deciso di fare quattro passi sulla consolle dicomando, né se uno dei termostati, guastandosi, gli fornisce dati errati, e neppure se uno o due dei dischi rigidi terminano di funzionare in una nuvoletta di polvere magnetica. Scrivere un programma in grado di procedere anche in quei casi - anzi, anche se a guastarsi è la stessa Cpu, il microprocessore che controlla l'intero sistema - è estremamente difficile, ma non è impossibile.
Ovviamente, non tutto l'onore e l'onere del compito ricade sul programma. C'è bisogno di un generatore indipendente di energia che entri in azione quando e se la corrente elettrica di rete viene a mancare. C'è bisogno di un grande numero di unità a disco di supporto, perchè tutti i dati vanno memorizzati due o tre volte in modo che la perdita di una copia non possa minare le funzionalità dell'intero sistema (un sistema hardware che resiste agli errori si dice in gergo sistema fault-tolerant, cioé "in grado di sopportare i fallimenti"). Ma comunque è necessario che il programma faccia la sua parte.
La più semplice delle forma di protezione contro l'errore è conosciuta ai programmatori Basic, ed è realizzata dalla istruzione onerr goto (qualche volta onerr gosub).
L'istruzione va messa all'inizio del programma, ed ha lo scopo di dirottare il controllo del programma in caso di errore su una speciale routine, che tratta l'errore. La routine viene informata (in qualche modo che varia ampiamente da Basic a Basic) di quale sia stato l'errore con uno speciale codice numerico. Nell'esempio del listato 1 abbiamo supposto che il codice venga ottenuto leggendo la variabile chiamata Err.
Listato 1.
10 Onerr Goto 1000
20 Input "Batti un numero ";A
30 Input "Batti un secondo numero ";B
40 Print "Il quoziente è "; A / B
50 End
1000 Rem trattamento dell'errore
1010 If Err = 20 Then Print "Numero troppo grande"
1020 If Err = 21 Then Print "Numero troppo piccolo"
1030 If Err = 75 Then Print "Divisione per zero!"
1100 End
Questa forma di trattamento delle eccezioni è piuttosto primitiva, ma efficace in diverse situazioni. Tra le sue limitazioni - alcune delle quali vengono risolte nei Basic più recenti - abbiamo l'impossibilità di ritornare alla normale esecuzione del programma una volta trattato l'errore, la impossibilità di identificare il punto del programma e il momento in cui è avvenuto l'errore, e le numerose difficoltà che sorgono se vogliamo predisporre più routine differenti che trattino le eccezioni, ciascuna delle quali venga attivata e disattivata nel momento più opportuno. Notate che non esiste un modo, neppure semplice come in Basic, per il trattamento dell'eccezione nel Pascal o nel linguaggio C, e se volete saperne di più consultate il riquadro 1.
Qualche volta ci capita di fare affermazioni sulla presenza o l'assenza di una caratteristica che lasciano stupefatto il lettore. Quando si afferma, per esempio, che "il Pascal non dispone del tipo di dati 'stringa'" o che "il Basic richiede i numeri di linea" si va incontro a un piccolo diluvio di lettere che affermano il contrario. |
Per trovare un linguaggio che compia un trattamento delle eccezioni a prova di bomba veniamo una volta di più ad Ada: in Ada sono possibili tutte le sofisticazioni che abbiamo dovuto escludere nel Basic, e - come al solito - ancora qualcosa in più.
Se ci pensate, noterete che gli errori che possono verificarsi sono facilmente divisibili per classi; la prima classe è quella degli errori di sintassi, come per esempio la scrittura di a :7= invece di a := 7 perché è scivolato un dito al programmatore mentre batteva il codice.
Questa classe di errore non è rilevante, perchè gli errori di sintassi vengono scoperti durante la compilazione del programma: gli errori di sintassi sono rilevanti solo in un linguaggio che non viene compilato, come il Basic, perchè con un linguaggio compilato come Ada, il C o Pascal gli errori di sintassi sono tutti trovati dal compilatore. Anche per Basic è possibile creare degli strumenti software che li scoprono e li segnalano sistematicamente prima che il programma venga fatto girare.
La seconda classe è quella degli errori hardware, come per esempio il fatidico Errore di I/O, che segnala un guasto del mezzo fisico dei dischetti, oppure l'errore Fine della Memoria, ottenuto quando il programma richiede un ulteriore spazio in memoria, ma tutta la memoria è stata esaurita. La terza classe è quella degli errori cosiddetti di runtime (dall'inglese "al tempo dell'esecuzione"); per esempio si causa un errore simile quando l'utente risponde "Pippo" alla domanda "Quanti anni hai?". Questo tipo di errore è squisitamente software, ma non è sempre possibile prevederlo e prevenirlo quando si scrive il programma.
Ada permette l'introduzione di una altra classe di errori: è possibile aggiungere agli errori come quelli che abbiamo citato, altri errori definiti dal software del programmatore, che verranno trattati in modo analogo. Torneremo tra poco su questo punto, dopo aver visto un esempio di trattamento dell'eccezione in Ada.
Per un esempio, osserviamo una funzione scritta in quel linguaggio, creata per calcolare il fattoriale di un numero. Per i lettori che non fossero al corrente di cosa sia un fattoriale, basterà dare una occhiata al riquadro 2.
Riquadro 2, dove si spiega cosa sia un numero fattoriale
Il fattoriale di un numero intero e positivo è definito come il prodotto di tutti i numeri interi da uno a quel numero. Per esempio, il fattoriale di 2 (che si indica con il numero seguito da un punto esclamativo: 2!) vale sempre 2, perchè 1 * 2 = 1. Il fattoriale di 3 vale 6, perchè 1 * 2 * 3 = 6, e il fattoriale di 4 vale 24. Abbiamo poi 5! = 120 e 6! = 720. |
Function fattoriale (n: natural) return natural is
t: natural := 1;
begin
for i in 1..n loop
t := t * i;
end;
return t;
exception
when numeric_error =>
return natural'last;
end fattoriale;
Per comprendere perfettamente il funzionamento della routine Ada, notate che natural è un tipo di dati equivalente allo unsigned integer di molti Pascal e del Linguaggio C; si tratta di un numero intero non negativo, cioé 0, 1, 2, 3... Natural'last è il modo di Ada per indicare il numero più grande possibile esprimibile con un natural (un concetto simile al maxint di Pascal); in modo analogo avremo integer'last e real'last. Abbiamo già incontrato questa espressione parlando dei vettori: forse ricorderete che se il vettore A ha dodici elementi numerati da 17 a 28, Ada definisce automaticamente le due costanti A'first, dal valore 17, ed A'last dal valore 28.
Dato che i numeri naturali assumono valori molto alti, possiamo ragionevolmente aspettarci che in qualche occasione il risultato della funzione ecceda la massima quantità esprimibile con il tipo natural; per esempio, questo accadrà (se natural'last vale 4.294.967.296 per il compilatore Ada che stiamo usando) quando chiediamo quanto vale il fattoriale di 13.
In quel caso, si avrà un errore di tipo numeric_error (uno dei tipi d'errore predefiniti che possono accadere) e il controllo del programma passerà al codice che si trova dopo la parola chiave exception. Pertanto, nel caso dell'esempio, la funzione restituirà il valore natural'last, considerandolo la migliore approssimazione possibile alla quantità richiesta.
Ada permette un controllo delle eccezioni estremamente flessibile. La parte exception è consentita in ogni blocco, ma non è obbligatoria. Per blocco intendiamo una serie di istruzioni comprese tra un begin ed un end; questo significa che una singola procedura può contenere più di una clausola exception per il trattamento delle eccezioni.
Se accade una eccezione in un blocco che non contiene il codice di trattamento delle eccezioni, o che non prevede la particolare eccezione che è effettivamente accaduta, l'eccezione "risale": il controllo torna alla procedura o funzione che ha chiamato la procedura dove è accaduta l'eccezione. Osservate il listato
Procedure A is
Procedure B is
-- omissis
for i in 1..10 loop
--omissis
exception
when numeric_error =>
--trattamento 1
end loop;
end B;
exception
when numeric_error =>
--trattamento 2
when others =>
-- codice per le altre eccezioni
end A;
Nel listato 3 notiamo che solo una parte della procedura B, quella compresa tra "loop" ed "end loop", dispone di controllo delle eccezioni. Se avviene una eccezione di tipo numeric_error in un punto compreso tra quelle parole chiave, l'eccezione subisce il trattamento 1; una eccezione che accade entro la procedura B ma al di fuori del ciclo for dovrebbe venire trattata dal blocco exception della procedura B, ma la procedura B non dispone di questo blocco. Pertanto, l'eccezione "risale la china", e arriva all'attenzione del blocco exception della procedura A, che possiede la procedura B; l'eccezione subità dunque il trattamento 2.
Una eccezione dunque, se non trattata, risale la catena delle chiamate procedurali sino a risalire eventualmente al main, alla parte principale del codice. Se neppure il main dispone di una routine di trattamento per quella eccezione, Ada fa arrestare il programma con un messaggio d'errore.
Veniamo a questo punto alle eccezioni definite dall'utente, che avevamo accennato poco sopra. E' possibile dichiarare un nuovo tipo di eccezione, che si aggiunge a quelli predefiniti, in modo analogo alla dichiarazione di una variabile:
un_numero: natural;
una_eccezione, un_altra: exception;
L'utilità di questo costrutto non è immediatamente visibile. Pensate di star scrivendo una libreria (un package, in termini Ada) che permette l'utilizzo di un collegamento via fibre ottiche del vostro computer con una telecamera (visto che stiamo solo immaginando, possiamo anche fare i gradassi, no?)
Il codice della nostra libreria deve però fare qualcosa quando un errore hardware impedisce la corretta lettura dei dati attraverso il cavo, in modo da segnalare il problema al programma che userà la libreria. Possiamo scrivere qualcosa come:
immagine_illeggibile: exception;
if dati_incomprensibili then
raise immagine_illeggibile;
In questo modo il programma ospite che sta usando il nostro package riceverà l'eccezione immagine_illeggibile, e la potrà trattare insieme alle altre eccezioni più comuni nel suo blocco di trattamento delle eccezioni.
Se pensate alla convenzione seguita nel linguaggio C, vi rendete immediatamente conto che si tratta di un notevole miglioramento. Nel linguaggio C le eccezioni sono segnalate restituendo valori impossibili. Per esempio, la funzione read () viene utilizzata per leggere dei dati da un file su disco, e restituisce al chiamante un numero intero, che corrisponde al numero di caratteri effettivamente letti. Un errore di lettura viene segnalato dalla funzione read restituendo un numero negativo.
Un'altra interessante possibilità fornita da Ada sta nelcosiddetto Retry, ovvero la possibilità di ripetere l'esecuzione dell'istruzione che ha causato l'eccezione. Immaginiamo di star scrivendo un sistema operativo del disco, e di essere occupati con la scrittura del pezzo di codice che si occupa del recupero di un blocco di dati dal disco. Dobbiamo trattare l'eccezione I/O Error, che si presenta quando lo hardware non è in grado di leggere il blocco per un motivo qualunque.
Un sistema operativo non può buttare la spugna alla prima avversità, dato che un insuccesso del genere può spesso venire rimediato da un ulteriore tentativo a breve distanza: pertanto possiamo scrivere
Malfunzione: exception;
B: Array (0..511) of Byte;
for i in 1..3 loop begin
LeggiBlocco (B);
exit; -- esce dal ciclo for
exception
when IO_Error =>
if i = 3 then
raise Malfunzione;
end loop
Un altro esempio in cui la ripetizione del tentativo può dare successo è la richiesta di memoria. Il programma può mettersi a dormire per un tempo prefissato e riprovare quando quel tempo è scaduto, sperando che la terminazione di un altro programma, avvenuta nel frattempo, abbia liberato dello spazio in memoria.
Ada è l'unico tra i linguaggi che ci siamo prefissi di seguire a fornire meccanismi avanzati per il trattamento delle eccezioni. Vi sono altri linguaggi moderni che permettono simili sofisticazioni, ma si tratta di linguaggi poco usati, o usati solo per compiti particolari, linguaggi specializzati. Tra le proposte alternative spicca quella di permettere la risalita delle eccezioni solo se esplicitamente chiesto dal programmatore. Per esempio, la procedura "Pippo" potrebbe permettere la risalita dell'eccezione "Overflow" che si genera al suo interno solamente se la sua dichiarazione stabilisce:
Procedure Pippo (a: integer; b: char) signals overflow;
E questo è tutto anche per questa eccezionale puntata.