C#

LE ECCEZIONI

Eccoci ora alle prese con un meccanismo di estrema importanza in questi linguaggio e, più in generale, nell'ambito del framework. Il meccanismo che esamineremo un questo paragrafo in fatti è comunemente utilizzabile con qualsiasi linguaggio che si appoggi su .Net perchè appartiene, è proprio, di Net stesso.
Che cos'è una eccezione? Come dice il nome stesso è qualcosa di anomalo, qualcosa che, in questo caso, che può interrompere il flusso normale di esecuzione del nostro programma. La cosa interessante, è che questa interruzione può presentarsi per vari motivi, che vanno da un semplice input errato, alla mancanza di disponibilità di una risorsa (ad esempio un file in uso da un altro processo) ad un errore hardware. le eccezioni ci permettono di gestire tutto questo e direi che non è poco. Di contro non bisogna esagerare con il loro uso, non è buona cosa infarcire un programma di gestione delle eccezioni, se vi venisse questa tentazione pensate un po' a tutta l'organizzazione della vostro prblema. Alla base di tutto c'è un classe, Exception, che impareremo a conoscere.
Partiamo da un semplice esempio:

  Esempio 19.1
1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;

class program
{
  public static void Main()
  {
    Console.Write("Inserisci un numero: ");
    int x = Int32.Parse(Console.ReadLine());
    Console.WriteLine("Hai inserito il numero: " + x);
  }
}

Il programma è semplice e funziona senza problemi finchè inserite un numero vero e proprio. Ma se distrattamente inserite una lettera o date un invio senza digitare nulla le cose cambiano e vi troverete davanti ad una cosa di questo tipo:

Eccezione non gestita: System.FormatException: Formato della stringa di input non corretto.
in System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
in System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
in program.Main()

La prima riga è la più illuminante: l'input digitato non è nel formato atteso. Seguono poi altre informazioni che ci dicono insomma che non è riuscita la trasformazione da stringa a numero. Questo è un problema che può essere gestito tramite l'uso delle eccezioni, come suggerito nella prima riga stessa dell'errore che abbiamo ottenuto. Naturalmente, come detto, questo è solo uno degli infiniti casi che si possono presentare, altro esempio tipico è la classica divisione per zero.
Il meccanismo sintattico per tenere sotto controllo il tutto fa uso di 4 parole chiave:

throw - ne parleremo più avanti è, se vogliamo, l'istruzione di lancio di una eccezione
try
- è la parola che dà inizio alla sezione che vogliamo tenere sotto controllo.
catch - inizia la sezione che accede all'oggetto Exceptions per ottenerne un' istanza. Ce ne possono essere essere diversi in quanto possiamo avere più eccezioni per lo stesso blocco. E' il "cuore" per la gestione dell'evento inatteso
finally - (opzionale) determina l'inizio di una sezione in cui vengono compiute operazioni a corollario, come la liberazione di risorse. Il blocco che inizia con finally è sicuramente eseguito se l'eccezione è gestita, mentre in caso di eccezioni non gestite, la cosa è un po' più complessa, anche se di solito finally viene eseguito (non sempre... però). 

Lo schema generale quindi è il seguente:

try
{
  codice da sorvegliare
}
catch
{
  gestione eccezione 1
}
catch
{
  gestione eventual eccezione 2
}
......
finally
{
  operazioni a corredo
}

A questo punto, per tirare un po' le fila, ci vuole un esempio e quindi modifichiamo l'esempio precedente:

  Esempio 19.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;

class program
{
  public static void Main()
  {
    try
    {
      Console.Write("Inserisci un numero: ");
      int x = Int32.Parse(Console.ReadLine());
      Console.WriteLine("Hai inserito il numero: " + x);
    }
    catch (Exception e)
    {
      Console.WriteLine("Errore");
      // Console.WriteLine(e);
    }
  }
}

e proviamo ad eseguire il programma, introducendo un errore:

Inserisci un numero: r
Errore

Avete visto? Abbiamo battuto una "r" invece del valore numerico ed il programma ci ha risposto con il nostro messaggio di errore personalizzato (un po' scarno, lo riconosco). Il programma non è crashatoE' un inizio. Per inciso, se eliminate le barre che iniziano la riga 17, riavrete in chiaro il messaggio di eccezione.  Il comportamento del CLR, in caso di eccezione è quello di cercare il blocco catch, relativo al try corrente, che può gestire l'eccezione. Se non lo trova restituisce il controllo al chiamante, se c'è, e ripete l'operazione. Se non trova nulla si ha un'eccezione non gestita. E' possibile usare quanti blocchi try vogliamo nel nostro codice, ad esempio se ci troviamo nella situazione in cui più eventi possono generare la stessa eccezione ma la gestione deve essere diversificata, ecco, quello è un caso tipico in cui dovete usare più blocchi try con i relativi catch.
A questo punto vogliamo vedere all'opera anche finally, quindi ampliamo l'esempio come segue:

  Esempio 19.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;

class program
{
  public static void Main()
  {
  try
    {
    Console.Write("Inserisci un numero: ");
    int x = Int32.Parse(Console.ReadLine());
    Console.WriteLine("Hai inserito il numero: " + x);
    }
    catch (Exception e)
    {
      Console.WriteLine("Errore");
    }
    finally
    {
      Console.WriteLine("Faccio pulizia");
    }
  }
}

Dal momento che l'eccezione è gestita finally viene senz'altro eseguita. Interessante notare che se eliminate le righe dalla 14 alla 17, quindi eliminate il catch, il programma, a seguito di un input errato, va ovviamente in crash ma la sezione introdotta da finally va ugualmente in esecuzione (almeno in casi come questi). Il che ci fa capire la sua importanza o meglio l'importanza che possiamo attribuire ad essa, indicando, nel suo perimetro, le azioni da svolgere che più risultano utili alla nostra applicazione. Di fatto in pratica loop infiniti o brutali interruzioni del programma possono prevenire l'esecuzione di finally. Ogni try può avere più catch ma ci deve essere un solo finally per ogni try. Come detto finally è opzionale, può non esservi nessuna necessità di codice di cleanup; tuttavia se lo userete in applicazioni reali ricordate di inserire al suo interno solo il codice strettamente necessario per terminare ciò che era sospeso quando avete iniziato il try.
Le eccezioni gestite sono veramente tante è vedremo più avanti i riferimenti. In questa fase è utile invece dare un esempio relativo all'uso di più clausole catch, ognuna della quali si rivolge ad una casistica differente. Vediamo l'esempio poi lo commenteremo:

  Esempio 19.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
class Test
{
  public static void Main()
  {
    Console.WriteLine("Questo programma esegue delle divisioni di interi");
    Console.WriteLine("Premi CTRL + C per interrompere\n");
    while (true)
    {
      try
      {
        Console.Write("Inserisci il dividendo: ");
        int dividendo = int.Parse(Console.ReadLine());
        Console.Write("Inserisci il divisore: ");
        int divisore = int.Parse(Console.ReadLine());
        int risultato = dividendo / divisore;
        int resto = dividendo % divisore;
        Console.Write("Risultato: " + risultato.ToString());
        Console.WriteLine(" resto: " + resto.ToString());
      }
      catch (DivideByZeroException e) // intercetta la divisione per 0
      {
        Console.WriteLine();
        Console.WriteLine("Errore: divisione x zero");
        Console.WriteLine();
      }
      catch (FormatException e)
      // intercetta l'inserimento di un carattere errato, o l'invio immediato
      {
        Console.WriteLine();
        Console.WriteLine("Devi inserire solo valori numerici!");
        Console.WriteLine();
      }
    }
  }
}

Come è facile intuire, alla riga 21 iniza una sezione che intercetta la divisione per 0, ovvero se viene inserito 0 come divisore. Alla riga 27 viene invece intercettato l'evento relativo ad un input avente formato non compatibile. E. come si cpaisce, si possono inserire quanti rami "catch" quanti ne vogliamo in base alle nostre necessità.
Una precauzione che è necessario adottare è quella di costruire la catena di catch in modo adeguato. Ovvero le eccezioni di carattere più specifico vanno messe prima di quelle di carattere generico. Infatti se modificassimo la riga 21 in modo che essa esprima una eccezione
di carattere generale ad es.

catch(Exception e)

il compilatore avrebbe da obiettare qualcosa come

error CS0160: A previous catch clause already catches all
exceptions of this or of a super type ('System.Exception')


Che in pratica ci avvisa che esiste già un catch che le cattura tutte. Questo perchè una volta che si verifichi un'eccezione il programma incontrerebbe come primo catch quello che fariferimento ad una eccezione generica e sarebbe pertanto inutile specificarne di ulteriori. Il compilatore come detto ci da una mano in questo senso.
Va detto che il codice precedente non è certo un esempio di bella programmazione, nemmeno e soprattutto da un punto di vista logico ed è stato creato solo per fini didattici. In effetti si sarebbe potuto gestire l'input dell'utente in modo più semplice. Le eccezioni, come dice il loro nome, andrebbero utilizzate per coprire situazioni molto particolari ed imprevedibili, l'input scorretto da parte di un utente non lo è dato che abbiamo sempre l'obbligo di prevedere che possano essere commessi banali errori di digitazione, mentre ad esempio la rottura di un harddisk è un evento già più appropriato per essere gestito da una eccezione. Insomma non bisogna abusare dell'uso delle eccezioni ma applicarle quando vale davvero la pena. Le eccezioni nascono per sostituire molte tecniche usate per gestire situazioni critiche improvvise nel corso del funzionamento del programmi (ad esempio i famosi valori di ritorno) proponendo un modello unificato e corerente. Che però tende ad appesantire e complicare non di poco il codice se si abusa di esso.

Non ci siamo ancora occupati dell'istruzione throw che ici permette di creare delle eccezioni ad alto grado di personalizzazione. Innanzitutto vediamo come funziona:

  Esempio 19.5
1
2
3
4
5
6
7
8
9
using System;
class Test
{
  public static void Main()
  {
    Console.WriteLine("Hello, World!");
    throw new Exception("Sono l'eccezione");
  }
}

Questo programma non fa nulla di male ma, grazie al codice alla riga 7, viene lanciata una nuova eccezione. Throw può essere lanciato al di fuori di un blocco che inizia con try, quindi praticamente ovunque nel nostro codice. Il comportamento del CLR, anche in questo caso, in questo caso è risalire lo stack per trovare un metodo che possa gestire l'eccezione. In difetto si ha una eccezione unhandled, come in questo esempio. Vediamo ora un esempio base che mostra la gestione di throw:

  Esempio 19.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
class Test
{
  public static void Main()
  {
    Console.WriteLine("Chiamo l'eccezione");
    try
    {
      LanciaEccezione();
    }
    catch(Exception e)
    {
      Console.WriteLine("Catturata");
    }

  public static void LanciaEccezione()
  {
    throw new Exception();
  }
}

Che presenta questo output:

Chiamo l'eccezione
Catturata

Quindi:
la riga 8 chiama il metodo che lancia l'eccezione
la riga 18 lancia l'eccezione (come è chiaramente visibile viene creata una nuova istanza della classe Exception)
il CLR esce dal metodo Lancia Eccezione e risale al chiamante in cerca di qualcosa che possa gestire l'eccezione. Questo qualcosa viene trovato alla riga 11.

L'esempio è sicuramente un po' grezzo ma dovrebbe rendere l'idea.
La vera forza di throw, tuttavia, è quella di permetterci di creare eccezioni personalizzate praticamente su qualunque evento di verifichi nel nostro programma. Vediamo l'esempio seguente, tenete ben presente che esso vi risulterà più chiaro dopo che avremo affrontato le classi:

  Esempio 19.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;

public class PremutoH : Exception
{
  public PremutoH(string message) : base(message)
  {}
}

class Test
{
  public static void Main()
  {
    Console.Write("Inserisci un carattere: ");
    string s = Console.ReadLine();
    try
    {
      if (s == "H")
      {
        throw(new PremutoH("Niente H!!"));
      }
    }
    catch(PremutoH e)
    {
      Console.WriteLine("Errore {0}", e);
    }
  }
}

Dalla riga 3 alla 7 abbiamo la creazione di una nuova classe che eredita da Excpetion. Per ora non preoccupatevi se non è chiaro
La nuova eccezione viene richiamata alla riga 19 nel caso in cui l'utente prema una H, nulla succede in tutti gli altri casi. Nel caso in cui l'eccezione sia sollevata (quindi in presenza della digitazione del carattere 'H') essa viene gestite dal codice presente tra le righe 22 e 25. Per quanto riguarda il discorso della classe, creazione ecc... come detto ne parleremo ampiamente in altri paragrafi. Quella indicata alla riga 3 è la classe madre di tutto questo discorso. Come tutte le classi Exception ha proprietà e metodi propri.

Per le proprietà abbiamo:

Proprietà Descrizione
Data Restituisce una coppia chiave valore con informazioni addizionali, definite dall'utente, sull'eccezione
HelpLink Definisce o restituisce un link di help
HResult Definisce o restituisce un codice numerico legato all'eccezione
InnerException restituisce l'istanza di Exception che ha causato l'eccezione
Message Espone un messaggio descrittivo dell'eccezione, come nell'esempio precedente
Source restituisce il nome dell'oggetto o dell'applicazione che ha causato l'eccezione
StackTrace Restuisce una stringa che rappresenta gli strati dello stack al momento dell'eccezione
TargetSite Restituisce il metodo che ha causato l'eccezione

I metodi invece sono quelle già note ereditate da Object, come sempre (Equals, Finalize, MemberWiseClone, GetHashCode, GetType, ToString) alle quali si aggiungono:

-- GetBaseException - che restiuisce l'eccezione radice di una o più eccezioni
-- GetObjectData - che restituisce ulteriori informazioni, se correttamente impostata

Gli esempi non mancano e non avrete problemi a procurarvene curiosando sul Web, se possibile inserirò qualche esempio nella sezione relativa.

La classe Exception è la radice di un enorme albero che contiene una quantità gigantesca di eccezioni, ciascuna delle quali corrisponde, lo ricordiamo, ad una situazione anomala incontrata durante l'esecuzione di un programma. Per avere una panoramica potete andare a visitare l'apposita sezione su MSDN. Vi renderete conto di quanta roba c'è.....

CHECKED e UNCHECKED

le due istruzioni oggetto di questo sottoparagrafo sono di aiuto quando si voglia tenere d'occhio qualche sezione del nostro codice (cheked) per quanto riguarda i possibili overflow, oppure si voglia evitare di controllarne qualche parte (unchecked). Vediamo subito l'esempio:

  Esempio 19.8
1
2
3
4
5
6
7
8
9
10
11
using System;

class Test
{
  public static void Main()
  {
    byte b1 = 100;
    b1 += 200;
    Console.WriteLine(b1);
  }
}

Questo programma compila (non sarebbe così se alla riga 8 avessimo scritto b1 = b1 + 200) ma il risultato, come a questo punto dovrebbe essere facile capire, non è quello atteso, e sarà un bel 44 invece di 300. I motivi li abbiamo già evidenziati a suo tempo. Modifichiamo ora il codice come segue:

  Esempio 19.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

class Test
{
  public static void Main()
  {
    checked
    {
      byte b1 = 100;
      b1 += 200;
      Console.WriteLine(b1);
    }
  }
}

Come si vede l'istruzione checked controlla un blocco di codice che arriva fino alla riga 12. Il nostro programma così cambiato non viene portato a termine ma ci viene presentata un'eccezione come segue:

Eccezione non gestita: System.OverflowException: Overflow di un'operazione aritmetica.
in Test.Main()

Lo stesso risultato si poteva ottenere istruendo il compilatore ad effettuare un check complessivo sugli overflow ovvero compilando il programma con la direttiva checked+

csc /checked+ nomefile.cs

Ad esempio compilando in tal modo l'esempio 19.8 ed eseguendolo si otterrebbe lo stesso risultato, ovvero l'eccezione, che si ha eseguendo il programma 19.9.

L'istruzione unchecked funziona come checked, formalmente, ovvero delimita un certa porzione di codice ed impedisce che su di essa venga effettuato un controllo relativo all'overflow anche se usassimo la direttiva checked+ al compilatore. Non so quante volte vi verrà utile usarla, direi piuttosto raramente.

Relativamente alle eccezioni il discorso si potrebbe ampliare parecchio e, ad esempio, sarebbe interessante spendere un po' di tempo sulle eccezioni non CLS-compliant ma queste non riguardano direttamente C# per cui non le prenderemo mai in esame. Tuttavia materiale ce ne è ancora molto, in questo paragrafo abbiamo visto quelle basi che vi pèermetteranno di lavorare già in maniera proficua.