Skip to content

5. Classi .NET di uso comune

Presentiamo qui alcune classi della piattaforma .NET di uso frequente. In precedenza, mostriamo come ottenere informazioni sulle centinaia di classi disponibili. Questa guida è indispensabile per lo sviluppatore C#, anche esperto. Il livello di qualità di una guida (facilità di accesso, organizzazione chiara, pertinenza delle informazioni, ecc.) può determinare il successo o il fallimento di un ambiente di sviluppo.

5.1. Cercare la documentazione sulle classi .NET

Di seguito forniamo alcune indicazioni per trovare la guida con Visual Studio.NET

5.1.1. Aiuto/Indice

  • in [1], selezionare l’opzione Help/Contents dal menu.
  • in [2], selezionare l'opzione Visual C# Express Edition
  • in [3], l'albero della guida su C#
  • in [4], un'altra opzione utile è .NET Framework, che consente di accedere a tutte le classi del framework .NET.

Diamo un'occhiata ai titoli dei capitoli della guida di C#:

  • [1]: una panoramica su C#
  • [2]: una serie di esempi su alcuni aspetti di C#
  • [3]: un corso su C# – potrebbe sostituire vantaggiosamente il presente documento…
  • [4]: per approfondire i dettagli di C#
  • [5]: utile per gli sviluppatori C++ o Java. Permette di evitare alcune insidie.
  • [6]: quando cercate degli esempi, potete iniziare da qui.
  • [7]: cosa occorre sapere per creare interfacce grafiche
  • [8]: per utilizzare al meglio IDE Visual Studio Express
  • [9]: SQL Server Express 2005 è un SGBD di qualità distribuito gratuitamente. Lo useremo in questo corso.

La guida di C# è solo una parte di ciò di cui ha bisogno lo sviluppatore. L’altra parte è la guida sulle centinaia di classi del framework .NET che gli faciliteranno il lavoro.

  • [1]: selezioniamo la guida sul framework .NET
  • [2]: la guida si trova nel ramo .NET Framework SDK
  • [3]: il ramo .NET Framework Class Library presenta tutte le classi .NET in base allo spazio dei nomi a cui appartengono
  • [4]: lo spazio dei nomi System che è stato utilizzato più spesso negli esempi dei capitoli precedenti
  • [5]: nello spazio dei nomi System, un esempio, in questo caso la struttura DateTime
  • [6]: la guida sulla struttura DateTime

5.1.2. Help/Indice/Ricerca

La guida fornita da MSDN è molto ampia e potrebbe risultare difficile orientarsi. In tal caso, è possibile utilizzare l’indice della guida:

  • in [1], utilizzare l'opzione [Help/Index] se la finestra della guida non è già aperta, altrimenti utilizzare [2] in una finestra della guida già aperta.
  • in [3], specificare l'ambito in cui deve essere effettuata la ricerca
  • in [4], specificare ciò che si sta cercando, in questo caso una classe
  • in [5], la risposta

Un altro modo per cercare aiuto è utilizzare la funzione di ricerca della guida:

  • in [1], utilizzare l'opzione [Help/Search] se la finestra della guida non è già aperta, altrimenti utilizzare [2] in una finestra della guida già aperta.
  • in [3], specificare cosa si sta cercando
  • in [4], filtrare i campi di ricerca
  • in [5], la risposta sotto forma di diversi argomenti in cui è stato trovato il testo cercato.

5.2. Le stringhe di caratteri

5.2.1. La classe System.String

La classe System.String è identica al tipo stringa semplice. Presenta numerose proprietà e metodi. Eccone alcuni:


public int Length { get; }
nombre de caractères de la chaîne

public bool EndsWith(string value)

rend vrai si la chaîne se termine par value

public bool StartsWith(string value)

rend vrai si la chaîne commence par value
public virtual bool Equals(object obj)

rend vrai si la chaînes est égale à obj - équivalent chaîne==obj

public int IndexOf(string value, int startIndex)

rend la première position dans la chaîne de la
chaîne value - la recherche commence à partir du
caractère n° startIndex

public int IndexOf(char value, int startIndex)

idem mais pour le caractère value

public string Insert(int startIndex, string value)

insère la chaîne value dans chaîne en position
startIndex
public static string Join(string separator, string[] value)

méthode de classe - rend une chaîne de caractères,
résultat de la concaténation des valeurs du tableau
value avec le séparateur separator

public int LastIndexOf(char value, int startIndex, int count)

public int LastIndexOf(string value, int startIndex, int count)

idem indexOf mais rend la dernière position au lieu
de la première

public string Replace(char oldChar, char newChar)

rend une chaîne copie de la chaîne courante où le
caractère oldChar a été remplacé par le caractère
newChar

public string[] Split(char[] separator)

la chaîne est vue comme une suite de champs séparés
par les caractères présents dans le tableau
separator. Le résultat est le tableau de ces champs

public string Substring(int startIndex, int length)

sous-chaîne de la chaîne courante commençant à la
position startIndex et ayant length caractères
public string ToLower()
rend la chaîne courante en minuscules
public string ToUpper()
rend la chaîne courante en majuscules
public string Trim()
rend la chaîne courante débarrassée de ses espaces
de début et fin

È importante notare che quando un metodo restituisce una stringa, questa è diversa dalla stringa a cui è stato applicato il metodo. Pertanto, S1.Trim() restituisce la stringa S2, mentre S1 e S2 sono due stringhe diverse.

Una stringa C può essere considerata come un array di caratteri. Pertanto

  • C[i] è il carattere i di C
  • C.Length è il numero di caratteri di C

Consideriamo il seguente esempio:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            string uneChaine = "l'oiseau vole au-dessus des nuages";
            affiche("uneChaine=" + uneChaine);
            affiche("uneChaine.Length=" + uneChaine.Length);
            affiche("chaine[10]=" + uneChaine[10]);
            affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole"));
            affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x"));
            affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a'));
            affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x'));
            affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7));
            affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper());
            affiche("uneChaine.ToLower()=" + uneChaine.ToLower());
            affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A'));
            string[] champs = uneChaine.Split(null);
            for (int i = 0; i < champs.Length; i++) {
                affiche("champs[" + i + "]=[" + champs[i] + "]");
            }//for
            affiche("Join(\":\",champs)=" + System.String.Join(":", champs));
            affiche("(\"  abc  \").Trim()=[" + "  abc  ".Trim() + "]");
        }//Main

        public static void affiche(string msg) {
            // visualizza messaggio
            Console.WriteLine(msg);
        }//visualizza
    }//classe
}//spazio dei nomi

L'esecuzione fornisce i seguenti risultati:

uneChaine=l'oiseau vole au-dessus des nuages
uneChaine.Length=34
chaine[10]=o
uneChaine.IndexOf("vole")=9
uneChaine.IndexOf("x")=-1
uneChaine.LastIndexOf('a')=30
uneChaine.LastIndexOf('x')=-1
uneChaine.Substring(4,7)=seau vo
uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES
uneChaine.ToLower()=l'oiseau vole au-dessus des nuages
uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges
champs[0]=[l'oiseau]
champs[1]=[vole]
champs[2]=[au-dessus]
champs[3]=[des]
champs[4]=[nuages]
Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages
("  abc  ").Trim()=[abc]

Consideriamo un nuovo esempio:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // la riga da analizzare
            string ligne = "un:deux::trois:";
            // i separatori di campo
            char[] séparateurs = new char[] { ':' };
            // split
            string[] champs = ligne.Split(séparateurs);
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("Champs[" + i + "]=" + champs[i]);
            }
            // join
            Console.WriteLine("join=[" + System.String.Join(":", champs) + "]");
        }
    }
}

e i risultati dell'esecuzione:

1
2
3
4
5
6
Champs[0]=un
Champs[1]=deux
Champs[2]=
Champs[3]=trois
Champs[4]=
join=[un:deux::trois:]

Il metodo Split della classe String consente di inserire in un array gli elementi di una stringa di caratteri. La definizione del metodo Split qui utilizzato è la seguente:


    public string[] Split(char[] separator);
separator
tabella dei caratteri. Questi caratteri rappresentano quelli utilizzati per separare i campi della stringa. Pertanto, se la stringa è "champ1, champ2, champ3"", si potrà utilizzare separator=new char[] {','}. Se il separatore è una sequenza di spazi, si utilizzerà separator=null.
résultat
array di stringhe in cui ogni elemento dell'array è un campo della stringa.

Il metodo Join è un metodo statico della classe String:


    public static string Join(string separator, string[] value);
value
array di stringhe
separator
una stringa che fungerà da separatore di campi
résultat
una stringa formata dalla concatenazione degli elementi dell'array value separati dalla stringa separator.

5.2.2. La classe System.Text.StringBuilder

In precedenza abbiamo detto che i metodi della classe String applicati a una stringa di caratteri S1 restituivano un’altra stringa S2. La classe System.Text.StringBuilder consente di manipolare S1 senza dover creare una stringa S2. Ciò migliora le prestazioni evitando la moltiplicazione di stringhe con una durata molto limitata.

La classe supporta diversi costruttori:

StringBuilder()
constructeur par défaut
StringBuilder(String value)

construction et initialisation avec value
StringBuilder(String value, int capacité)

construction et initialisation avec value avec une taille de
bloc de capacité caractères.

Un oggetto StringBuilder utilizza blocchi di capacité caratteri per memorizzare la stringa sottostante. Per impostazione predefinita, capacité è pari a 16. Il terzo costruttore sopra indicato consente di specificare la capacità dei blocchi. Il numero di blocchi di caratteri capacité necessari per memorizzare una stringa S viene regolato automaticamente dalla classe StringBuilder. Esistono costruttori per impostare il numero massimo di caratteri in un oggetto StringBuilder. Per impostazione predefinita, questa capacità massima è 2 147 483 647.

Ecco un esempio che illustra questo concetto di capacité:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str
            StringBuilder str = new StringBuilder("test");
            Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            for (int i = 0; i < 10; i++) {
                str.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            }
            // str2
            StringBuilder str2 = new StringBuilder("test",10);
            Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            for (int i = 0; i < 10; i++) {
                str2.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            }
        }
    }
}
  • riga 7: creazione di un oggetto StringBuilder con una dimensione del blocco di 16 caratteri
  • riga 8: str.Length è il numero attuale di caratteri della stringa str. str.Capacity è il numero di caratteri che la stringa str attuale può contenere prima della riassegnazione di un nuovo blocco.
  • riga 10: str.Append(String S) consente di concatenare la stringa S di tipo String alla stringa str di tipo StringBuilder.
  • riga 14: creazione di un oggetto StringBuilder con una capacità di blocco di 10 caratteri

Risultato dell'esecuzione:

taille=4, capacité=16
taille=8, capacité=16
taille=12, capacité=16
taille=16, capacité=16
taille=20, capacité=32
taille=24, capacité=32
taille=28, capacité=32
taille=32, capacité=32
taille=36, capacité=64
taille=40, capacité=64
taille=44, capacité=64
taille=4, capacité=10
taille=8, capacité=10
taille=12, capacité=20
taille=16, capacité=20
taille=20, capacité=20
taille=24, capacité=40
taille=28, capacité=40
taille=32, capacité=40
taille=36, capacité=40
taille=40, capacité=40
taille=44, capacité=80

Questi risultati dimostrano che la classe segue un proprio algoritmo per allocare nuovi blocchi quando la sua capacità è insufficiente:

  • righe 4-5: aumento della capacità di 16 caratteri
  • righe 8-9: aumento della capacità di 32 caratteri, mentre ne sarebbero bastati 16.

Ecco alcuni dei metodi della classe:


public StringBuilder Append(string value)

ajoute la chaîne value à l'objet StringBuilder. Rend
l'objet StringBuilder. Cette méthode est surchargée
 pour admettre différents types pour value : byte,
int, float, double, decimal, ... 

public StringBuilder Insert(int index,
string value)

insère value à la position index. Cette méthode est
surchargée comme la précédente pour accepter
différents types pour value.

public StringBuilder Remove(int index, int length)

supprime length caractères à partir de la position
index.

public StringBuilder Replace(string oldValue,
string newValue)

remplace dans StringBuilder, la chaîne oldValue par
la chaîne newValue. Il existe une version surchargée
(char oldChar, char newChar).
public String ToString()

convertit l'objet StringBuilder en un objet de type
String.

Ecco un esempio:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str3
            StringBuilder str3 = new StringBuilder("test");
            Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));
        }
    }
}

e i relativi risultati:

XYZTstabCD

5.3. Gli array

Le tabelle derivano dalla classe Array:

La classe Array dispone di vari metodi per ordinare un array, cercare un elemento in un array, ridimensionare un array, ... Presentiamo alcune proprietà e metodi di questa classe. Sono quasi tutti sovramodificati, c.a.d, in quanto esistono in diverse varianti. Ogni array ne eredita le proprietà.

Proprietà

public int Length {get;}
nombre total d'éléments du tableau, quelque soit son nombre de dimensions
public int Rank {get;}
nombre total de dimensions du tableau

Metodi

public static int BinarySearch<T>(T[] tableau,
 value)
rend la position de [value] dans tableau.
public static int BinarySearch<T>(T[] tableau,
nt index, int length, T value)
idem mais cherche dans tableau à partir de la
position [index] et sur [length] éléments
public static void Clear(Array tableau, int index,
int length)
met les [length] éléments de tableau commençant au
 n° [index] à 0 si numériques, false si booléens, null si références
public static void Copy(Array source,
Array destination, int length)
copie [length] éléments de source dans destination
public int GetLength(int i)
nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i)
indice du 1er élément de la dimension n° i
public int GetUpperBound(int i)
indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau,
T valeur)
rend la position de valeur dans tableau ou -1 si
valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau,
int n)
redimensionne tableau à n éléments. Les éléments
déjà présents sont conservés.
public static void Sort<T>(T[] tableau,
IComparer<T> comparateur)
trie tableau selon un ordre défini par comparateur.
Cette méthode a été présentée au paragraphe Erreur : source de la référence non trouvée.

Il seguente programma illustra l'utilizzo di alcuni metodi della classe Array:


using System;

namespace Chap3 {
    class Program {
        // tipo di ricerca
        enum TypeRecherche { linéaire, dichotomique };

        // metodo principale
        static void Main(string[] args) {
            // lettura degli elementi di un array digitati dalla tastiera
            double[] éléments;
            Saisie(out éléments);
            // visualizzazione dell'array non ordinato
            Affiche("Tableau non trié", éléments);
            // Ricerca lineare nell'array non ordinato
            Recherche(éléments, TypeRecherche.linéaire);
            // ordinamento dell'array
            Array.Sort(éléments);
            // Visualizzazione dell'array ordinato
            Affiche("Tableau trié", éléments);
            // Ricerca dicotomica nella tabella ordinata
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // Inserimento dei valori dell'array «elementi»
        // elementi: riferimento alla tabella creata dal metodo
        static void Saisie(out double[] éléments) {
            bool terminé = false;
            string réponse;
            bool erreur;
            double élément = 0;
            int i = 0;
            // Inizialmente, la tabella non esiste
            éléments = null;
            // Ciclo di inserimento degli elementi dell'array
            while (!terminé) {
                // domanda
                Console.Write("Elément (réel) " + i + " du tableau (rien pour terminer) : ");
                // lettura della risposta
                réponse = Console.ReadLine().Trim();
                // fine dell'inserimento se la stringa è vuota
                if (réponse.Equals(""))
                    break;
                // verifica dell'inserimento
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.Error.WriteLine("Saisie incorrecte, recommencez");
                    erreur = true;
                }//try-catch
                // se non ci sono errori
                if (!erreur) {
                    // un elemento in più nell'array
                    i += 1;
                    // ridimensionamento dell'array per accogliere il nuovo elemento
                    Array.Resize(ref éléments, i);
                    // inserimento del nuovo elemento
                    éléments[i - 1] = élément;
                }
            }//while
        }

        // metodo generico per visualizzare gli elementi di un array
        static void Affiche<T>(string texte, T[] éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // ricerca di un elemento nell'array
        // elementi: array di numeri reali
        // TypeRecherche: dicotomico o lineare
        static void Recherche(double[] éléments, TypeRecherche type) {
            // Ricerca
            bool terminé = false;
            string réponse = null;
            double élément = 0;
            bool erreur = false;
            int i = 0;
            while (!terminé) {
                // domanda
                Console.WriteLine("Elément cherché (rien pour arrêter) : ");
                // lettura-verifica della risposta
                réponse = Console.ReadLine().Trim();
                // finito?
                if (réponse.Equals(""))
                    break;
                // verifica
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.WriteLine("Erreur, recommencez...");
                    erreur = true;
                }//try-catch
                // se non ci sono errori
                if (!erreur) {
                    // si cerca l'elemento nell'array
                    if (type == TypeRecherche.dichotomique)
                        // ricerca dicotomica
                        i = Array.BinarySearch(éléments, élément);
                    else
                        // ricerca lineare
                        i = Array.IndexOf(éléments, élément);
                    // Visualizzazione della risposta
                    if (i >= 0)
                        Console.WriteLine("Trouvé en position " + i);
                    else
                        Console.WriteLine("Pas dans le tableau");
                }//if
            }//while
        }
    }
}
  • righe 27-62: il metodo Saisie inserisce gli elementi di un array éléments digitati dalla tastiera. Poiché non è possibile dimensionare l'array a priori (non se ne conosce la dimensione finale), si è costretti a ridimensionarlo ad ogni nuovo elemento (riga 57). Un algoritmo più efficiente sarebbe stato quello di allocare spazio all'array per gruppi di N elementi. Tuttavia, un array non è progettato per essere ridimensionato. In questo caso è preferibile utilizzare una lista (ArrayList, List<T>).
  • righe 75-113: il metodo Recherche consente di cercare nell’array éléments un elemento digitato dalla tastiera. La modalità di ricerca varia a seconda che l’array sia ordinato o meno. Per un array non ordinato, si esegue una ricerca lineare con il metodo IndexOf alla riga 106. Per un array ordinato, si esegue una ricerca dicotomica con il metodo BinarySearch alla riga 103.
  • riga 18: si ordina la tabella éléments. Qui si utilizza una variante di Sort che ha un solo parametro: la tabella da ordinare. La relazione di ordinamento utilizzata per confrontare gli elementi dell’array è quindi quella implicita di tali elementi. In questo caso, gli elementi sono numerici. Viene utilizzato l’ordinamento naturale dei numeri.

I risultati visualizzati sullo schermo sono i seguenti:

Elément (réel) 0 du tableau (rien pour terminer) : 3,6
Elément (réel) 1 du tableau (rien pour terminer) : 7,4
Elément (réel) 2 du tableau (rien pour terminer) : -1,5
Elément (réel) 3 du tableau (rien pour terminer) : -7
Elément (réel) 4 du tableau (rien pour terminer) :
Tableau non trié----------------------------------
3,6
7,4
-1,5
-7
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 1
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

Tableau trié--------------------------------------
-7
-1,5
3,6
7,4
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 3
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

5.4. Le collezioni generiche

Oltre all'array, esistono diverse classi per memorizzare collezioni di elementi. Esistono versioni generiche nello spazio dei nomi System.Collections.Generic e versioni non generiche in System.Collections. Presenteremo due collezioni generiche di uso frequente: la lista e il dizionario.

L'elenco delle collezioni generiche è il seguente:

Image

5.4.1. La classe generica List<T>

La classe System.Collections.Generic.List<T> consente di implementare collezioni di oggetti di tipo T la cui dimensione varia durante l’esecuzione del programma. Un oggetto di tipo List<T> si gestisce quasi come un array. Pertanto, l’elemento i di una lista l è indicato come l[i].

Esiste anche un tipo di lista non generico: ArrayList, in grado di memorizzare riferimenti a oggetti di qualsiasi tipo. ArrayList è funzionalmente equivalente a List<Object>.. Un oggetto ArrayList ha il seguente aspetto:

Nell’esempio sopra riportato, gli elementi 0, 1 e i dell’elenco puntano a oggetti di tipi diversi. È necessario che un oggetto venga prima creato prima di aggiungere il suo riferimento alla lista ArrayList. Sebbene un ArrayList memorizzi riferimenti a oggetti, è possibile memorizzarvi anche dei numeri. Ciò avviene tramite un meccanismo denominato Boxing: il numero viene incapsulato in un oggetto O di tipo Object ed è il riferimento a O che viene memorizzato nella lista. Si tratta di un meccanismo trasparente per lo sviluppatore. È quindi possibile scrivere:

ArrayList liste=new ArrayList();
liste.Add(4);

Ciò produrrà il seguente risultato:

Nell’esempio sopra riportato, il numero 4 è stato incapsulato in un oggetto O e il riferimento O è memorizzato nella lista. Per recuperarlo, si potrà scrivere:


            int i = (int)liste[0];

L'operazione Object -> int è denominata Unboxing. Se una lista è interamente composta da tipi int, dichiararla come List<int> migliora le prestazioni. Infatti, i numeri di tipo int vengono quindi memorizzati nella lista stessa e non in tipi Object esterni alla lista. Le operazioni di boxing/unboxing non vengono più eseguite.

Se un oggetto List<T> o T è una classe, l’elenco memorizza anche in questo caso i riferimenti agli oggetti di tipo T:

Ecco alcune delle proprietà e dei metodi delle liste generiche:

Proprietà

public int Count {get;}
nombre d'éléments de la liste
public int Capacity {get;}
nombre d'éléments que la liste peut contenir avant d'être redimensionnée. Ce
redimensionnement se fait automatiquement. Cette notion de capacité de liste
est analogue à celle de capacité décrite pour la classe StringBuilder paragraphe Erreur : source de la référence non trouvée.

Metodi

public void Add(T item)
ajoute item à la liste
public int BinarySearch<T>(T item)
rend la position de item dans la liste s'il s'y
trouve sinon un nombre <0
public int BinarySearch<T>(T item,
IComparer<T> comparateur)
idem mais le 2ième paramètre permet de comparer deux
éléments de la liste. L'interface IComparer<T> a été
présentée au paragraphe Erreur : source de la référence non trouvée.
public void Clear()
supprime tous les éléments de la liste
public bool Contains(T item)
rend True si item est dans la liste, False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public int IndexOf(T item)
rend la position de item dans tableau ou -1 si
valeur n'est pas trouvée.
public void Insert(T item, int index)
insère item à la position index de la liste
public bool Remove(T item)
supprime item de la liste. Rend True si l'opération
réussit, False sinon.
public void RemoveAt(int index)
supprime l'élément n° index de la liste
public void Sort(IComparer<T> comparateur)
trie la liste selon un ordre défini par comparateur.
 Cette méthode a été présentée paragraphe Erreur : source de la référence non trouvée.
public void Sort()
trie la liste selon l'ordre défini par le type des
éléments de la liste
public T[] ToArray()
rend les éléments de la liste sous forme de tableau

Riprendiamo l'esempio trattato in precedenza con un oggetto di tipo Array e trattiamolo ora con un oggetto di tipo List<T>.. Poiché l'elenco è un oggetto simile a un array, il codice cambia di poco. Presentiamo solo le modifiche significative:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        // tipo di ricerca
        enum TypeRecherche { linéaire, dichotomique };

        // metodo principale
        static void Main(string[] args) {
            // lettura degli elementi di un elenco digitati dalla tastiera
            List<double> éléments;
            Saisie(out éléments);
            // numero di elementi
            Console.WriteLine("La liste a {0} éléments et une capacité de {1} éléments", éléments.Count, éléments.Capacity);
            // visualizzazione dell'elenco non ordinato
            Affiche("Liste non triée", éléments);
            // Ricerca lineare nell'elenco non ordinato
            Recherche(éléments, TypeRecherche.linéaire);
            // ordinamento dell'elenco
            éléments.Sort();
            // visualizzazione dell'elenco ordinato
            Affiche("Liste triée", éléments);
            // Ricerca dicotomica nell'elenco ordinato
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // Inserimento dei valori dell'elenco degli elementi
        // elementi: riferimento all'elenco creato dal metodo
        static void Saisie(out List<double> éléments) {
...
            // Inizialmente, l'elenco è vuoto
            éléments = new List<double>();
            // Ciclo di inserimento degli elementi dell'elenco
            while (!terminé) {
...
                // se non ci sono errori
                if (!erreur) {
                    // un elemento in più nell'elenco
                    éléments.Add(élément);
                }
            }//while
        }

        // metodo generico per visualizzare gli elementi di un oggetto enumerabile
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // ricerca di un elemento nell'elenco
        // elementi: elenco di numeri reali
        // TypeRecherche: dicotomico o lineare
        static void Recherche(List<double> éléments, TypeRecherche type) {
...
            while (!terminé) {
...
                // se non ci sono errori
                if (!erreur) {
                    // si cerca l'elemento nell'elenco
                    if (type == TypeRecherche.dichotomique)
                        // ricerca dicotomica
                        i = éléments.BinarySearch(élément);
                    else
                        // ricerca lineare
                        i = éléments.IndexOf(élément);
                    // Visualizzazione della risposta
...
                }//if
            }//while
        }
    }
}
  • righe 46-51: il metodo generico Affiche<T> accetta due parametri:
  • il primo parametro è un testo da scrivere
  • il secondo parametro è un oggetto che implementa l’interfaccia generica IEnumerable<T>:
1
2
3
4
public interface IEnumerable<T>{
    IEnumerator GetEnumerator();
    IEnumerator<T> GetEnumerator();
}

La struttura foreach( T elemento in elementi) della riga 48 è valida per qualsiasi oggetto éléments che implementi l'interfaccia IEnumerable. Le tabelle (Array) e le liste (List<T>) implementano l'interfaccia IEnumerable<T>. Pertanto, il metodo Affiche è adatto sia per visualizzare tabelle che elenchi.

I risultati dell’esecuzione del programma sono gli stessi dell’esempio che utilizza la classe Array.

5.4.2. La classe Dictionary<TKey,TValue>

La classe System.Collections.Generic.Dictionary<TKey,TValue> consente di implementare un dizionario. È possibile considerare un dizionario come un array a due colonne:

chiave
valore
chiave1
valore1
chiave2
valore2
..
...

Nella classe Dictionary<TKey,TValue> le chiavi sono di tipo Tkey, i valori di tipo TValue. Le chiavi sono univoche, c.a.d, quindi non possono esserci due chiavi identiche. Un dizionario di questo tipo potrebbe apparire così se i tipi TKey e TValue indicassero delle classi:

Il valore associato alla chiave C di un dizionario D si ottiene con la notazione D[C]. Questo valore è leggibile e scrivibile. Pertanto, è possibile scrivere:

1
2
3
4
5
TValue v=...;
TKey c=...;
Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>();
D[c]=v;
v=D[c];

Se la chiave c non esiste nel dizionario D, la notazione D[c] genera un'eccezione.

I metodi e le proprietà principali della classe Dictionary<TKey,TValue> sono i seguenti:

Costruttori

public Dictionary<TKey,TValue>()
constructeur sans paramètres - construit un dictionnaire vide.
Il existe plusieurs autres constructeurs.

Proprietà

public int Count {get;}
nombre d'entrées (clé, valeur) dans le dictionnaire
public Dictionary<TKey,TValue>.KeyCollection Keys {get;}
collection des clés du dictionnaire.
public Dictionary<TKey,TValue>.ValueCollection Values {get;}
collection des valeurs du dictionnaire.

Metodi

public void Add(TKey key, TValue value)
ajoute le couple (key, value) au dictionnaire
public void Clear()
supprime tous les couples du dictionnaire
public bool ContainsKey (TKey key)
rend True si key est une clé du dictionnaire,
False sinon
public bool ContainsValue (TValue value)
rend True si value est une valeur du dictionnaire,
False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public bool Remove(TKey key)
supprime du dictionnaire le couple de clé key.
Rend True si l'opération réussit, False sinon.
public bool TryGetValue(TKey key,
out TValue value)
rend dans value, la valeur associée à la clé key si
cette dernière existe, sinon rend la valeur par
défaut du type TValue (0 pour les nombres, false
pour les booléens, null pour les références d'objet)

Consideriamo il seguente programma di esempio:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // creazione di un dizionario <string,int>
            string[] liste = { "jean:20", "paul:18", "mélanie:10", "violette:15" };
            string[] champs = null;
            char[] séparateurs = new char[] { ':' };
            Dictionary<string,int> dico = new Dictionary<string,int>();
            for (int i = 0; i <liste.Length; i++) {
                champs = liste[i].Split(séparateurs);
                dico[champs[0]]= int.Parse(champs[1]);
            }//for
            // numero di elementi nel dizionario
            Console.WriteLine("Le dictionnaire a " + dico.Count + " éléments");
            // elenco delle chiavi
            Affiche("[Liste des clés]",dico.Keys);
            // elenco dei valori
            Affiche("[Liste des valeurs]", dico.Values);
            // elenco delle chiavi e dei valori
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // si elimina la chiave "paul"
            Console.WriteLine("[Suppression d'une clé]");
            dico.Remove("paul");
            // elenco delle chiavi e dei valori
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // ricerca nel dizionario
            String nomCherché = null;
            Console.Write("Nom recherché (rien pour arrêter) : ");
            nomCherché = Console.ReadLine().Trim();
            int value;
            while (!nomCherché.Equals("")) {
                dico.TryGetValue(nomCherché, out value);
                if (value!=0) {
                    Console.WriteLine(nomCherché + "," + value);
                } else {
                    Console.WriteLine("Nom " + nomCherché + " inconnu");
                }
                // ricerca successiva
                Console.Out.Write("Nom recherché (rien pour arrêter) : ");
                nomCherché = Console.ReadLine().Trim();
            }//while
        }

        // metodo generico per visualizzare gli elementi di un tipo enumerabile
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

    }
}
  • riga 8: un array di string che servirà a inizializzare il dizionario <string,int>
  • riga 11: il dizionario <string,int>
  • righe 12-15: la sua inizializzazione a partire dall'array di string della riga 8
  • riga 17: numero di voci del dizionario
  • riga 19: le chiavi del dizionario
  • riga 21: i valori del dizionario
  • riga 29: eliminazione di una voce dal dizionario
  • riga 41: ricerca di una chiave nel dizionario. Se non esiste, il metodo TryGetValue inserirà 0 in value, poiché value è di tipo numerico. Questa tecnica è utilizzabile in questo caso solo perché si sa che il valore 0 non è presente nel dizionario.

I risultati dell’esecuzione sono i seguenti:

Le dictionnaire a 4 éléments
[Liste des clés]----------------------------------
jean
paul
mélanie
violette
[Liste des valeurs]-------------------------------
20
18
10
15
[Liste des clés & valeurs]
clé=jean valeur=20
clé=paul valeur=18
clé=mélanie valeur=10
clé=violette valeur=15
[Suppression d'une clé]
[Liste des clés & valeurs]
clé=jean valeur=20
clé=mélanie valeur=10
clé=violette valeur=15
Nom recherché (rien pour arrêter) : violette
violette,15
Nom recherché (rien pour arrêter) : x
Nom x inconnu

5.5. I file di testo

5.5.1. La classe StreamReader

La classe System.IO.StreamReader consente di leggere il contenuto di un file di testo. È infatti in grado di gestire flussi che non sono file. Ecco alcune delle sue proprietà e dei suoi metodi:

Costruttori

public StreamReader(string path)
construit un flux de lecture à partir du fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Proprietà

public bool EndOfStream {get;}
True si le flux a été lu entièrement

Metodi

public void Close()
ferme le flux et libère les ressources allouées pour
sa gestion. A faire obligatoirement après
exploitation du flux.
public override int Peek()
rend le caractère suivant du flux sans le consommer.
Un Peek supplémentaire rendrait donc le même
caractère.
public override int Read()
rend le caractère suivant du flux et avance d'un
caractère dans le flux.
public override int Read(char[] buffer,
int index, int count)
lit count caractères dans le flux et les met dans
buffer à partir de la position index. Rend le nombre
de caractères lus - peut être 0.
public override string ReadLine()
rend la ligne suivante du flux ou null si on était à
la fin du flux.
public override string ReadToEnd()
rend la fin du flux ou "" si on était à la fin du
flux.

Ecco un esempio:


using System;
using System.IO;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // directory di esecuzione
            Console.WriteLine("Répertoire d'exécution : "+Environment.CurrentDirectory);
            string ligne = null;
            StreamReader fluxInfos = null;
            // lettura del contenuto del file infos.txt
            try {
                // lettura 1
                Console.WriteLine("Lecture 1----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    ligne = fluxInfos.ReadLine();
                    while (ligne != null) {
                        Console.WriteLine(ligne);
                        ligne = fluxInfos.ReadLine();
                    }
                }
                // lettura 2
                Console.WriteLine("Lecture 2----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    Console.WriteLine(fluxInfos.ReadToEnd());
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • riga 8: visualizza il nome della directory di esecuzione
  • righe 12, 27: un try/catch per gestire un'eventuale eccezione.
  • riga 15: la struttura using flux=new StreamReader(...) è un espediente per evitare di dover chiudere esplicitamente il flusso dopo il suo utilizzo. Tale chiusura avviene automaticamente non appena si esce dall'ambito di using.
  • riga 15: il file letto si chiama infos.txt. Trattandosi di un nome relativo, verrà cercato nella directory di esecuzione visualizzata dalla riga 8. Se non è presente, verrà generata un'eccezione che verrà gestita dal try/catch.
  • righe 16-20: il file viene letto riga per riga
  • riga 25: il file viene letto in un'unica volta

Il file infos.txt è il seguente:

12620:0:0
13190:0,05:631
15640:0,1:1290,5

ed è collocato nella seguente cartella del progetto C#:

Si noterà che bin/Release è la cartella di esecuzione quando il progetto viene eseguito con Ctrl-F5.

L'esecuzione produce i seguenti risultati:

1
2
3
4
5
6
7
8
9
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5
Lecture 2----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5

Se alla riga 15 si inserisce il nome del file xx.txt, si ottengono i seguenti risultati:

1
2
3
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

5.5.2. La classe StreamWriter

La classe System.IO.StreamReader consente di scrivere in un file di testo. Come la classe StreamReader, è infatti in grado di gestire flussi che non sono file. Ecco alcune delle sue proprietà e dei suoi metodi:

Costruttori

public StreamWriter(string path)
construit un flux d'écriture dans le fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Proprietà

public virtual bool AutoFlush
{get;set;}
fixe le mode d'écriture dans le fichier du buffer associé au flux. Si
égal à False, l'écriture dans le flux n'est pas immédiate : il y a
d'abord écriture dans une mémoire tampon puis dans le fichier lorsque la
mémoire tampon est pleine sinon l'écriture dans le fichier est immédiate
(pas de tampon intermédiaire). Par défaut c'est le mode tamponné qui est
utilisé. Le tampon n'est écrit dans le fichier que lorsqu'il est plein ou
bien lorsqu'on le vide explicitement par une opération Flush ou encore
lorsqu'on ferme le flux StreamWriter par une opération Close. Le mode
AutoFlush=False est le plus efficace lorsqu'on travaille avec des
fichiers parce qu'il limite les accès disque. C'est le mode par défaut
pour ce type de flux. Le mode AutoFlush=False ne convient pas à tous les
flux, notamment les flux réseau. Pour ceux-ci, qui souvent prennent place
dans un dialogue entre deux partenaires, ce qui est écrit par l'un des
partenaires doit être immédiatement lu par l'autre. Le flux d'écriture
doit alors être en mode AutoFlush=True.
public virtual string NewLine {get;set;}
les caractères de fin de ligne. Par défaut "\r\n". Pour un système Unix,
il faudrait utiliser "\n".

Metodi

public void Close()
ferme le flux et libère les ressources allouées pour sa
gestion. A faire obligatoirement après exploitation du flux.
public override void Flush()
écrit dans le fichier, le buffer du flux, sans attendre qu'il
soit plein.
public virtual void Write(T value)
écrit value dans le fichier associé au flux. Ici T n'est pas
un type générique mais symbolise le fait que la méthode
Write accepte différents types de paramètres (string, int,
object, ...). La méthode value.ToString est utilisée pour
produire la chaîne écrite dans le fichier.
public virtual void WriteLine(T value)
même chose que Write mais avec la marque de fin de ligne
(NewLine) en plus.

Consideriamo il seguente esempio:


using System;
using System.IO;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // directory di esecuzione
            Console.WriteLine("Répertoire d'exécution : " + Environment.CurrentDirectory);
            string ligne = null;                        // una riga di testo
            StreamWriter fluxInfos = null;    // il file di testo
            try {
                // creazione del file di testo
                using (fluxInfos = new StreamWriter("infos2.txt")) {
                    Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush);
                    // lettura della riga digitata dalla tastiera
                    Console.Write("ligne (rien pour arrêter) : ");
                    ligne = Console.ReadLine().Trim();
                    // ciclo finché la riga inserita non è vuota
                    while (ligne != "") {
                        // scrittura della riga nel file di testo
                        fluxInfos.WriteLine(ligne);
                        // lettura della nuova riga digitata dalla tastiera
                        Console.Write("ligne (rien pour arrêter) : ");
                        ligne = Console.ReadLine().Trim();
                    }//while
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • riga 13: ancora una volta, utilizziamo la sintassi using(flux) per evitare di dover chiudere esplicitamente il flusso con un'operazione Close. Questa chiusura avviene automaticamente all'uscita da using.
  • Perché un try/catch, righe 11 e 27? Alla riga 13, potremmo specificare un nome di file nel formato /rep1/rep2/ .../fichier con un percorso /rep1/rep2/... che non esiste, rendendo così impossibile la creazione di fichier. Verrebbe quindi generata un'eccezione. Esistono altri possibili casi di eccezione (disco pieno, diritti insufficienti, ...)

I risultati dell’esecuzione sono i seguenti:

1
2
3
4
5
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Mode AutoFlush : False
ligne (rien pour arrêter) : 1ère ligne
ligne (rien pour arrêter) : 2ième ligne
ligne (rien pour arrêter) :

Il file infos2.txt è stato creato nella cartella bin/Release del progetto:

 

5.6. I file binari

Le classi System.IO.BinaryReader e System.IO.BinaryWriter servono a leggere e scrivere file binari.

Consideriamo la seguente applicazione:

// sintassi pg testo bin logs
// si legge un file di testo (text) e se ne memorizza il contenuto in un file binario (bin
// il file di testo contiene righe della forma nome : età che verranno memorizzate in una struttura string, int
// (logs) è un file di log di testo

Il file di testo ha il seguente contenuto:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

Il programma è il seguente:


using System;
using System.IO;

// sintassi pg testo bin log
// si legge un file di testo (testo) e se ne memorizza il contenuto in un file binario (bin)
// il file di testo contiene righe della forma nome : età che verranno memorizzate in una struttura string, int
// (logs) è un file di log di testo

namespace Chap3 {
    class Program {
        static void Main(string[] arguments) {
            // sono necessari 3 argomenti
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg texte binaire log");
                Environment.Exit(1);
            }//if

            // variabili
            string ligne=null;
            string nom=null;
            int age=0;
            int numLigne = 0;
            char[] séparateurs = new char[] { ':' };
            string[] champs=null;
            StreamReader input = null;
            BinaryWriter output = null;
            StreamWriter logs = null;
            bool erreur = false;
            // lettura di un file di testo - scrittura di un file binario
            try {
                // apertura del file di testo in lettura
                input = new StreamReader(arguments[0]);
                // apertura del file binario in scrittura
                output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
                // apertura del file di log in scrittura
                logs = new StreamWriter(arguments[2]);
                // elaborazione del file di testo
                while ((ligne = input.ReadLine()) != null) {
                    // un'altra riga
                    numLigne++;
                    // riga vuota?
                    if (ligne.Trim() == "") {
                        // ignora
                        continue;
                    }
                    // una riga nome: età
                    champs = ligne.Split(séparateurs);
                    // ci servono 2 campi
                    if (champs.Length != 2) {
                        // registriamo l'errore
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]);
                        // riga successiva
                        continue;
                    }//if
                    // il primo campo deve essere non vuoto
                    erreur = false;
                    nom = champs[0].Trim();
                    if (nom == "") {
                        // si registra l'errore
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]);
                        erreur = true;
                    }
                    // il secondo campo deve essere un numero intero >=0
                    if (!int.TryParse(champs[1],out age) || age<0) {
                        // si registra l'errore
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un âge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim());
                        erreur = true;
                    }//se
                    // se non ci sono errori, si scrivono i dati nel file binario
                    if (!erreur) {
                        output.Write(nom);
                        output.Write(age);
                    }
                    // riga successiva
                }//while
            }catch(Exception e){
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // chiusura dei file
                if(input!=null) input.Close();
                if(output!=null) output.Close();
                if(logs!=null) logs.Close();
            }
        }
    }
}

Soffermiamoci sulle operazioni relative alla classe BinaryWriter:

  • riga 34: l'oggetto BinaryWriter viene aperto dall'operazione

            output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

L'argomento del costruttore deve essere uno stream (Stream). In questo caso si tratta di uno stream creato a partire da un file (FileStream) di cui si specificano:

  • (continua)
    • il nome
    • l'operazione da eseguire, in questo caso FileMode.Create per creare il file
    • il tipo di accesso, in questo caso FileAccess.Write per un accesso in scrittura al file
  • righe 70-73: le operazioni di scrittura
             // si scrivono i dati nel file binario
            output.Write(nom);
            output.Write(age);

La classe BinaryWriter dispone di diversi metodi Write sovrascritti per scrivere i diversi tipi di dati semplici

  • riga 81: l'operazione di chiusura del flusso
        output.Close();

I tre argomenti del metodo Main vengono specificati nel progetto (tramite le sue proprietà) [1] e il file di testo da elaborare viene inserito nella cartella bin/Release [2]:

Con il seguente file [personnes1.txt]:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

i risultati dell’esecuzione sono i seguenti:

  • in [1], il file binario [personnes1.bin] così creato e il file di log [logs.txt]. Quest'ultimo ha il seguente contenuto:
1
2
3
La ligne n° [6] du fichier [personnes1.txt] a un âge [-1] incorrect
La ligne n° [8] du fichier [personnes1.txt] a un nombre de champs incorrect
La ligne n° [9] du fichier [personnes1.txt] a un âge [yy] incorrect

Il contenuto del file binario [personnes1.bin] ci verrà fornito dal programma riportato di seguito. Anche questo accetta tre argomenti:

// sintassi pg bin testo log
// si legge un file binario bin e se ne salva il contenuto in un file di testo (testo)
// il file binario ha una struttura string, int
// il file di testo contiene righe della forma nome : età
// logs è un file di testo contenente i log

Si esegue quindi l'operazione inversa. Si legge un file binario per creare un file di testo. Se il file di testo generato è identico al file originale, si avrà la certezza che la conversione testo --> binario --> testo è avvenuta correttamente. Il codice è il seguente:


using System;
using System.IO;

// sintassi pg bin testo logs
// si legge un file binario bin e se ne salva il contenuto in un file di testo (testo)
// il file binario ha una struttura string, int
// il file di testo contiene righe della forma nome : età
// logs è un file di testo contenente i log

namespace Chap3 {
    class Program2 {
        static void Main(string[] arguments) {
            // sono necessari 3 argomenti
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg binaire texte log");
                Environment.Exit(1);
            }//if

            // variabili
            string nom = null;
            int age = 0;
            int numPersonne = 1;
            BinaryReader input = null;
            StreamWriter output = null;
            StreamWriter logs = null;
            bool fini;
            // lettura file binario - scrittura file di testo
            try {
                // apertura del file binario in lettura
                input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
                // apertura del file di testo in scrittura
                output = new StreamWriter(arguments[1]);
                // apertura del file di log in scrittura
                logs = new StreamWriter(arguments[2]);
                // elaborazione del file binario
                fini = false;
                while (!fini) {
                    try {
                        // lettura del nome
                        nom = input.ReadString().Trim();
                        // lettura dell'età
                        age = input.ReadInt32();
                        // scrittura nel file di testo
                        output.WriteLine(nom + ":" + age);
                        // persona successiva
                        numPersonne++;
                    } catch (EndOfStreamException) {
                        fini = true;
                    } catch (Exception e) {
                        logs.WriteLine("L'erreur suivante s'est produite à la lecture de la personne n° {0} : {1}", numPersonne, e.Message);
                    }
                }//while
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // chiusura dei file
                if (input != null)
                    input.Close();
                if (output != null)
                    output.Close();
                if (logs != null)
                    logs.Close();
            }
        }
    }
}

Soffermiamoci sulle operazioni relative alla classe BinaryReader:

  • riga 30: l'oggetto BinaryReader viene aperto dall'operazione

            input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

L’argomento del costruttore deve essere uno stream. In questo caso si tratta di uno stream creato a partire da un file (FileStream) di cui si specificano:

  • (continua)
    • il nome
    • l'operazione da eseguire, in questo caso FileMode.Open per aprire un file esistente
    • il tipo di accesso, in questo caso FileAccess.Read per un accesso in lettura al file
  • righe 40, 42: le operazioni di lettura
nom=input.ReadString().Trim();
age=input.ReadInt32();

La classe BinaryReader dispone di diversi metodi ReadXX per leggere i diversi tipi di dati semplici

  • riga 60: l'operazione di chiusura del flusso
        input.Close();

Se si eseguono i due programmi in sequenza, trasformando personnes1.txt in personnes1.bin e poi personnes1.bin in personnes2.txt2, si ottengono i seguenti risultati:

  • in [1], il progetto è configurato per eseguire la seconda applicazione
  • in [2], gli argomenti passati a Main
  • in [3], i file generati dall'esecuzione dell'applicazione.

Il contenuto di [personnes2.txt] è il seguente:

1
2
3
4
paul:10
helene:15
jacques:11
sylvain:12

5.7. Le espressioni regolari

La classe System.Text.RegularExpressions.Regex consente l'uso di espressioni regolari. Queste permettono di verificare il formato di una stringa di caratteri. In questo modo è possibile verificare che una stringa che rappresenta una data sia effettivamente nel formato gg/mm/aa. A tal fine si utilizza un modello e si confronta la stringa con tale modello. Pertanto, in questo esempio, j, m e a devono essere cifre. Il modello di un formato di data valido è quindi "\d\d/\d\d/\d\d", dove il simbolo \d indica una cifra. I simboli utilizzabili in un modello sono i seguenti:

Caractère
Descrizione
\ 
Contrassegna il carattere seguente come carattere speciale o letterale. Ad esempio, "n" corrisponde al carattere "n". "\n" corrisponde a un carattere di nuova riga. La sequenza "\\" corrisponde a "\", mentre "\(" corrisponde a "(".
^ 
Corrisponde all'inizio dell'inserimento.
$ 
Corrisponde alla fine dell'inserimento.
* 
Corrisponde al carattere precedente zero o più volte. Pertanto, "zo*" corrisponde a "z" o a "zoo".
+ 
Corrisponde al carattere precedente una o più volte. Pertanto, "zo+" corrisponde a "zoo", ma non a "z".
? 
Corrisponde al carattere precedente zero o una volta. Ad esempio, "a?ve?" corrisponde a "ve" in "lever".
.
Corrisponde a qualsiasi carattere singolo, eccetto il carattere di nuova riga.
(modèle) 
Cerca il modèle e memorizza la corrispondenza. La sottostringa corrispondente può essere estratta dalla collezione Matches ottenuta, utilizzando Item [0]...[n]. Per trovare corrispondenze con caratteri tra parentesi ( ), utilizzare "\(" o "\)".
x|y
Corrisponde sia a x sia a y. Ad esempio, "z|foot" corrisponde a "z" o a "foot". "(z|f)oo" corrisponde a "zoo" o a "foo".
{n}
n è un numero intero non negativo. Corrisponde esattamente a n moltiplicato per il carattere. Ad esempio, "o{2}" non corrisponde alla "o" in "Bob," ma alle prime due "o" in "fooooot".
{n,} 
n è un numero intero non negativo. Corrisponde ad almeno n volte il carattere. Ad esempio, "o{2,}" non corrisponde alla "o" in "Bob", ma a tutte le "o" in "fooooot". "o{1,}" equivale a "o+" e "o{0,}" equivale a "o*".
{n,m} 
m e n sono numeri interi non negativi. Corrisponde ad almeno n e al massimo m volte il carattere. Ad esempio, "o{1,3}" corrisponde alle prime tre "o" in "foooooot" e "o{0,1}" equivale a "o?".
[xyz] 
Set di caratteri. Corrisponde a uno dei caratteri indicati. Ad esempio, "[abc]" corrisponde alla "a" in "plat".
[^xyz] 
Set di caratteri negativo. Corrisponde a qualsiasi carattere non specificato. Ad esempio, "[^abc]" corrisponde alla "p" in "plat".
[a-z] 
Intervallo di caratteri. Corrisponde a qualsiasi carattere compreso nella serie specificata. Ad esempio, "[a-z]" corrisponde a qualsiasi carattere alfabetico minuscolo compreso tra "a" e "z".
[^m-z] 
Intervallo di caratteri negativo. Corrisponde a qualsiasi carattere non presente nella serie specificata. Ad esempio, "[^m-z]" corrisponde a qualsiasi carattere non compreso tra "m" e "z".
\b 
Corrisponde a un limite che rappresenta una parola, in altre parole, alla posizione tra una parola e uno spazio. Ad esempio, "er\b" corrisponde a "er" in "lever", ma non a "er" in "verbe".
\B 
Corrisponde a un limite che non rappresenta una parola. «en*t\B» corrisponde a «ent» in «bien entendu».
\d 
Corrisponde a un carattere che rappresenta una cifra. Equivale a [0-9].
\D 
Corrisponde a un carattere che non rappresenta una cifra. Equivale a [^0-9].
\f 
Corrisponde a un carattere di fine pagina.
\n 
Corrisponde a un carattere di nuova riga.
\r 
Corrisponde a un carattere di ritorno a capo.
\s 
Corrisponde a qualsiasi spazio bianco, inclusi spazio, tabulazione, salto di pagina, ecc. Equivale a "[ \f\n\r\t\v]".
\S 
Corrisponde a qualsiasi carattere di spazio non bianco. Equivale a "[^ \f\n\r\t\v]".
\t 
Corrisponde a un carattere di tabulazione.
\v 
Corrisponde a un carattere di tabulazione verticale.
\w 
Corrisponde a qualsiasi carattere che rappresenti una parola e che includa un trattino basso. Equivale a "[A-Za-z0-9_]".
\W 
Corrisponde a qualsiasi carattere che non rappresenti una parola. Equivale a "[^A-Za-z0-9_]".
\num 
Corrisponde a num, dove num è un numero intero positivo. Fa riferimento alle corrispondenze memorizzate. Ad esempio, «(.)\1» corrisponde a due caratteri identici consecutivi.
\n
Corrisponde a n, dove n è un valore di escape ottale. I valori di escape ottali devono essere composti da 1, 2 o 3 cifre. Ad esempio, sia "\11" che "\011" corrispondono a un carattere di tabulazione. "\0011" equivale a "\001" e "1". I valori di escape ottali non devono superare 256. In caso contrario, nell’espressione verrebbero prese in considerazione solo le prime due cifre. Consente di utilizzare i codici ASCII nelle espressioni regolari.
\xn
Corrisponde a n, dove n è un valore di escape esadecimale. I valori di escape esadecimali devono obbligatoriamente comprendere due cifre. Ad esempio, "\x41" corrisponde a "A". "\x041" equivale a "\x04" e "1". Consente di utilizzare i codici ASCII nelle espressioni regolari.

Un elemento in un modello può essere presente una o più volte. Consideriamo alcuni esempi relativi al simbolo \d, che rappresenta una cifra:

modello
significato
\d
una cifra
\d?
0 o 1 cifra
\d*
0 o più cifre
\d+
1 o più cifre
\d{2}
2 cifre
\d{3,}
almeno 3 cifre
\d{5,7}
da 5 a 7 cifre

Immaginiamo ora il modello in grado di descrivere il formato previsto per una stringa di caratteri:

stringa ricercata
modello
una data nel formato gg/mm/aa
\d{2}/\d{2}/\d{2}
un'ora nel formato hh:mm:ss
\d{2}:\d{2}:\d{2}
un numero intero senza segno
\d+
una sequenza di spazi, eventualmente vuota
\s*
un numero intero senza segno che può essere preceduto o seguito da spazi
\s*\d+\s*
un numero intero che può essere con segno e preceduto o seguito da spazi
\s*[+|-]?\s*\d+\s*
un numero reale senza segno che può essere preceduto o seguito da spazi
\s*\d+(.\d*)?\s*
un numero reale che può essere con segno e preceduto o seguito da spazi
\s*[+|]?\s*\d+(.\d*)?\s*
una stringa contenente la parola juste
\bjuste\b
  

È possibile specificare dove cercare il modello nella stringa:

modello
significato
^modello
il modello si trova all'inizio della stringa
modello$
il modello conclude la stringa
^modello$
il modello inizia e termina la stringa
modello
il modello viene cercato in tutta la stringa a partire dall'inizio della stessa.
stringa cercata
modello
una stringa che termina con un punto esclamativo
!$
una stringa che termina con un punto
\.$
una stringa che inizia con la sequenza //
^//
una stringa composta da una sola parola, eventualmente preceduta o seguita da spazi
^\s*\w+\s*$
una stringa contenente due parole, eventualmente precedute o seguite da spazi
^\s*\w+\s*\w+\s*$
una stringa contenente la parola secret
\bsecret\b

I sottoinsiemi di un modello possono essere "recuperati". In questo modo, non solo è possibile verificare che una stringa corrisponda a un modello specifico, ma è anche possibile recuperare all'interno di tale stringa gli elementi corrispondenti ai sottoinsiemi del modello che sono stati racchiusi tra parentesi. Pertanto, se si analizza una stringa contenente una data gg/mm/aa e si desidera inoltre estrarre gli elementi gg, mm, aa di tale data, si utilizzerà il modello (\d\d)/(\d\d)/(\d\d).

5.7.1. Verificare che una stringa corrisponda a un modello dato

Un oggetto di tipo Regex si costruisce nel modo seguente:

public Regex(string pattern)
construit un objet "expression régulière" à partir d'un modèle passé
en paramètre (pattern)

Una volta costruita l'espressione regolare modello, è possibile confrontarla con stringhe di caratteri utilizzando il metodo IsMatch:

public bool IsMatch(string input)
vrai si la chaîne input correspond au modèle de l'expression
régulière

Ecco un esempio:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // un'espressione regolare modello
            string modèle1 = @"^\s*\d+\s*$";
            Regex regex1 = new Regex(modèle1);
            // confronto di un'istanza con il modello
            string exemplaire1 = "  123  ";
            if (regex1.IsMatch(exemplaire1)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire1, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire1, modèle1);
            }//if
            string exemplaire2 = "  123a  ";
            if (regex1.IsMatch(exemplaire2)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire2, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire2, modèle1);
            }//if
        }

    }
}

e i risultati dell'esecuzione:

[  123  ] correspond au modèle [^\s*\d+\s*$]
[  123a  ] ne correspond pas au modèle [^\s*\d+\s*$]

5.7.2. Trovare tutte le occorrenze di un modello in una stringa

Il metodo Matches consente di recuperare gli elementi di una stringa che corrispondono a un modello:

public MatchCollection Matches(string input)
rend la collection des éléments de la chaîne input
correspondant au modèle

La classe MatchCollection ha una proprietà Count che rappresenta il numero di elementi della collezione. Se résultats è un oggetto MatchCollection, risultati[i] è l'elemento i di questa collezione ed è di tipo Match. La classe Match ha diverse proprietà, tra cui le seguenti:

  • Value: il valore dell’oggetto Match,, ovvero un elemento corrispondente al modello
  • Index: la posizione in cui l'elemento è stato trovato nella stringa analizzata

Esaminiamo il seguente esempio:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // più occorrenze del modello nell'esempio
            string modèle2 = @"\d+";
            Regex regex2 = new Regex(modèle2);
            string exemplaire3 = "  123  456  789 ";
            MatchCollection résultats = regex2.Matches(exemplaire3);
            Console.WriteLine("Modèle=[{0}],exemplaire=[{1}]", modèle2, exemplaire3);
            Console.WriteLine("Il y a {0} occurrences du modèle dans l'exemplaire ", résultats.Count);
            for (int i = 0; i < résultats.Count; i++) {
                Console.WriteLine("[{0}] trouvé en position {1}", résultats[i].Value, résultats[i].Index);
            }//for
        }
    }
}
  • riga 8: il modello ricercato è una sequenza di cifre
  • riga 10: la stringa in cui si cerca questo modello
  • riga 11: vengono recuperati tutti gli elementi di exemplaire3 che soddisfano il modello modèle2
  • righe 14-16: li visualizziamo

I risultati dell'esecuzione del programma sono i seguenti:

1
2
3
4
5
Modèle=[\d+],exemplaire=[  123  456  789 ]
Il y a 3 occurrences du modèle dans l'exemplaire
[123] trouvé en position 2
[456] trouvé en position 7
[789] trouvé en position 12

5.7.3. Recuperare parti di un modello

È possibile "recuperare" sottoinsiemi di un modello. In questo modo, non solo è possibile verificare che una stringa corrisponda a un modello specifico, ma è anche possibile recuperare all'interno di tale stringa gli elementi corrispondenti ai sottoinsiemi del modello racchiusi tra parentesi. Pertanto, se si analizza una stringa contenente una data gg/mm/aa e si desidera inoltre estrarre gli elementi gg, mm, aa di tale data, si utilizzerà il modello (\d\d)/(\d\d)/(\d\d).

Esaminiamo il seguente esempio:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program3 {
        static void Main(string[] args) {
            // acquisizione di elementi nel modello
            string modèle3 = @"(\d\d):(\d\d):(\d\d)";
            Regex regex3 = new Regex(modèle3);
            string exemplaire4 = "Il est 18:05:49";
            // verifica del modello
            Match résultat = regex3.Match(exemplaire4);
            if (résultat.Success) {
                // l'istanza corrisponde al modello
                Console.WriteLine("L'exemplaire [{0}] correspond au modèle [{1}]",exemplaire4,modèle3);
                // vengono visualizzati i gruppi di parentesi
                for (int i = 0; i < résultat.Groups.Count; i++) {
                    Console.WriteLine("groupes[{0}]=[{1}] trouvé en position {2}",i, résultat.Groups[i].Value,résultat.Groups[i].Index);
                }//for
            } else {
                // l'esemplare non corrisponde al modello
                Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modèle [{1}]", exemplaire4, modèle3);
            }
        }
    }
}

L'esecuzione di questo programma produce i seguenti risultati:

1
2
3
4
5
L'exemplaire [Il est 18:05:49] correspond au modèle [(\d\d):(\d\d):(\d\d)]
groupes[0]=[18:05:49] trouvé en position 7
groupes[1]=[18] trouvé en position 7
groupes[2]=[05] trouvé en position 10
groupes[3]=[49] trouvé en position 13

La novità si trova nelle righe 12-19:

  • riga 12: la stringa exemplaire4 viene confrontata con il modello regex3 tramite il metodo Match. Quest'ultimo restituisce un oggetto Match già presentato in precedenza. Qui utilizziamo due nuove proprietà di questa classe:
  • Success (riga 13): indica se è stata trovata una corrispondenza
  • Groups (righe 17, 18): collezione in cui
    • Groups[0] corrisponde alla parte della stringa corrispondente al modello
    • Groups[i] (i>=1) corrisponde al gruppo di parentesi n. i

Se résultat è di tipo Match, résultats.Groups è di tipo GroupCollection e résultats.Groups[i] è di tipo Group. La classe Group presenta due proprietà che utilizziamo in questo contesto:

  • Value (riga 18): il valore dell’oggetto Group, che è l’elemento corrispondente al contenuto di una parentesi
  • Index (riga 18): la posizione in cui l’elemento è stato trovato nella stringa analizzata

5.7.4. Un programma di esercitazione

Trovare l’espressione regolare che permette di verificare se una stringa corrisponde effettivamente a un determinato modello è talvolta una vera sfida. Il programma seguente permette di esercitarsi. Richiede un modello e una stringa e indica se la stringa corrisponde o meno al modello.


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program4 {
        static void Main(string[] args) {
            // dati
            string modèle, chaine;
            Regex regex = null;
            MatchCollection résultats;
            // all'utente vengono richiesti i modelli e gli esemplari da confrontare con questo
            while (true) {
                // viene richiesto il modello
                Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
                modèle = Console.In.ReadLine();
                // finito?
                if (modèle.Trim() == "")
                    break;
                // si crea l'espressione regolare
                try {
                    regex = new Regex(modèle);
                } catch (Exception ex) {
                    Console.WriteLine("Erreur : " + ex.Message);
                    continue;
                }
                // si chiede all'utente quali copie confrontare con il modello
                while (true) {
                    Console.Write("Tapez la chaîne à comparer au modèle [{0}] ou rien pour arrêter :", modèle);
                    chaine = Console.ReadLine();
                    // finito?
                    if (chaine.Trim() == "")
                        break;
                    // si esegue il confronto
                    résultats = regex.Matches(chaine);
                    // Operazione riuscita?
                    if (résultats.Count == 0) {
                        Console.WriteLine("Je n'ai pas trouvé de correspondances");
                        continue;
                    }//if
                    // vengono visualizzati gli elementi corrispondenti al modello
                    for (int i = 0; i < résultats.Count; i++) {
                        Console.WriteLine("J'ai trouvé la correspondance [{0}] en position [{1}]", résultats[i].Value, résultats[i].Index);
                        // dei sottoelementi
                        if (résultats[i].Groups.Count != 1) {
                            for (int j = 1; j < résultats[i].Groups.Count; j++) {
                                Console.WriteLine("\tsous-élément [{0}] en position [{1}]", résultats[i].Groups[j].Value, résultats[i].Groups[j].Index);
                            }
                        }
                    }
                }
            }
        }
    }
}

Ecco un esempio di esecuzione:

Tapez le modèle à tester ou rien pour arrêter :\d+
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :123 456 789
J'ai trouvé la correspondance [123] en position [0]
J'ai trouvé la correspondance [456] en position [4]
J'ai trouvé la correspondance [789] en position [8]
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :(\d{2}):(\d\d)
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :14:15 abcd 17:18 xyzt
J'ai trouvé la correspondance [14:15] en position [0]
        sous-élément [14] en position [0]
        sous-élément [15] en position [3]
J'ai trouvé la correspondance [17:18] en position [11]
        sous-élément [17] en position [11]
        sous-élément [18] en position [14]
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*\d+\s*$
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :   1456
J'ai trouvé la correspondance [   1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*(\d+)\s*$
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :1456
J'ai trouvé la correspondance [1456] en position [0]
        sous-élément [1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :abcd 1456
Je n'ai pas trouvé de correspondances
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :

5.7.5. Il metodo Split

Abbiamo già visto questo metodo nel corso String:


public string[] Split(char[] separator)
la chaîne est vue comme une suite de champs séparés par les
caractères présents dans le tableau separator. Le résultat est
le tableau de ces champs

Il metodo Split della classe Regex ci permette di specificare il separatore in base a un modello:


public string[] Split(string input)
La chaîne input est décomposée en champs, ceux-ci étant séparés
par un séparateur correspondant au modèle de l'objet Regex
courant.

Supponiamo, ad esempio, di avere in un file di testo delle righe della forma campo1, campo2, ..., campo n. I campi sono separati da una virgola, ma questa può essere preceduta o seguita da spazi. Il metodo Split della classe string non è quindi adatto. La soluzione è fornita dal metodo RegEx. Se ligne è la riga letta, i campi potranno essere ottenuti tramite

string[] champs=new Regex(@"s*,\s*").Split(ligne);

come mostra il seguente esempio:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program5 {
        static void Main(string[] args) {
            // una riga
            string ligne = "abc  , def  , ghi";
            // un modello
            Regex modèle = new Regex(@"\s*,\s*");
            // scomposizione della riga in campi
            string[] champs = modèle.Split(ligne);
            // visualizzazione
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]);
            }
        }
    }
}

Risultati dell’esecuzione:

1
2
3
champs[0]=[abc]
champs[1]=[def]
champs[2]=[ghi]

5.8. Applicazione di esempio - V3

Riprendiamo l'applicazione esaminata nei paragrafi 3.6 (versione 1) e 4.10 (versione 2).

Nell’ultima versione esaminata, il calcolo dell’imposta veniva effettuato nella classe astratta AbstractImpot:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // le fasce di imposta necessarie per il calcolo dell'imposta
        // provengono da una fonte esterna

        protected TrancheImpot[] tranchesImpot;

        // calcolo dell'imposta
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // calcolo del numero di quote
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // calcolo del reddito imponibile e del quoziente familiare
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // calcolo dell'imposta
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // restituzione del risultato
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//calcolare
    }//classe

}

Il metodo calculer alla riga 38 utilizza l'array tranchesImpot alla riga 35, array non inizializzato dalla classe AbstractImpot. Per questo motivo è astratta e deve essere derivata per essere utilizzabile. Tale inizializzazione veniva effettuata dalla classe derivata HardwiredImpot:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // tabelle dei dati necessari per il calcolo dell'imposta
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

        public HardwiredImpot() {
                // creazione della tabella delle fasce di imposta
            tranchesImpot = new TrancheImpot[limites.Length];
                // compilazione
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// classe
}// namespace

In precedenza, i dati necessari al calcolo dell’imposta erano inseriti in modo «fisso» nel codice della classe. La nuova versione dell’esempio li inserisce in un file di testo:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Poiché l'elaborazione di questo file potrebbe generare delle eccezioni, creiamo una classe speciale per gestirle:


using System;

namespace Chap3 {
    class FileImpotException : Exception {
        // codici di errore
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
    
        // codice di errore
        public CodeErreurs Code { get; set; }

        // costruttori
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message,e) {
        }
    }
}
  • riga 4: la classe FileImpotException deriva dalla classe Exception. Servirà a memorizzare qualsiasi errore che si verifichi durante l'elaborazione del file di testo contenente i dati.
  • riga 7: un'enumerazione che rappresenta i codici di errore:
    • Acces: errore di accesso al file di testo contenente i dati
    • Ligne: riga priva dei tre campi previsti
    • Champ1: il campo n. 1 è errato
    • Champ2: il campo n. 2 è errato
    • Champ3: il campo n. 3 è errato

Alcuni di questi errori possono presentarsi in combinazione (Champ1, Champ2, Champ3). Pertanto, l’enumerazione CodeErreurs è stata annotata con l’attributo [Flags], il che implica che i diversi valori dell’enumerazione debbano essere potenze di 2. Un errore nei campi 1 e 2 comporterà quindi il codice di errore Champ1 | Champ2.

  • riga 10: la proprietà automatica Code memorizzerà il codice dell'errore.
  • riga 15: un costruttore che consente di creare un oggetto FileImpotException passando come parametro un messaggio di errore.
  • riga 18: un costruttore che consente di creare un oggetto FileImpotException passando come parametri un messaggio di errore e l'eccezione all'origine dell'errore.

La classe che inizializza l'array tranchesImpot della classe AbstractImpot è ora la seguente:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Chap3 {
    class FileImpot : AbstractImpot {

        public FileImpot(string fileName) {
            // dati
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // eccezione
            FileImpotException fe = null;
            // lettura del contenuto del file fileName, riga per riga
            Regex pattern = new Regex(@"s*:\s*");
            // inizialmente nessun errore
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(fileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // riga corrente
                        string ligne = input.ReadLine().Trim();
                        // le righe vuote vengono ignorate
                        if (ligne == "") continue;
                        // riga suddivisa in tre campi separati da:
                        string[] champsLigne = pattern.Split(ligne);
                        // ci sono 3 campi?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // conversioni dei 3 campi
                        decimal limite = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1;
                            if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2;
                            if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ;
                        }
                        // errore?
                        if (code != 0) {
                            // si registra l'errore
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // si memorizza la nuova fascia di imposta
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // riga successiva
                            numLigne++;
                        }
                    }
                }
                // si trasferisce l'elenco listImpot nella tabella tranchesImpot
                if (code == 0) {
                    // si trasferisce l'elenco listImpot nella tabella tranchesImpot
                    tranchesImpot = listTranchesImpot.ToArray();
                }
            } catch (Exception e) {
                // si rileva l'errore
                fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // errore da segnalare?
            if (fe != null) throw fe;
        }
    }
}
  • riga 7: la classe FileImpot deriva dalla classe AbstractImpot, così come nella versione 2 derivava la classe HardwiredImpot.
  • riga 9: il costruttore della classe FileImpot ha il compito di inizializzare il campo trancheImpot della sua classe base AbstractImpot. Accetta come parametro il nome del file di testo contenente i dati.
  • riga 11: il campo tranchesImpot della classe base AbstractImpot è un array che deve essere popolato con i dati del file filename passato come parametro. La lettura di un file di testo è sequenziale. Il numero di righe è noto solo dopo aver letto l’intero file. Pertanto, non è possibile dimensionare l’array tranchesImpot. I dati verranno temporaneamente memorizzati nella lista generica listTranchesImpot.

Si ricorda che il tipo TrancheImpot è una struttura:


namespace Chap3 {
    // una fascia di imposta
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
  • riga 14: fe, dello stesso tipo di FileImpotException, serve a incapsulare un eventuale errore di elaborazione del file di testo.
  • riga 16: l'espressione regolare del separatore di campi in una riga champ1:champ2:champ3 del file di testo. I campi sono separati dal carattere : preceduto e seguito da un numero qualsiasi di spazi.
  • riga 18: il codice di errore in caso di errore
  • riga 20: elaborazione del file di testo con un StreamReader
  • riga 21: si esegue un ciclo finché rimane una riga da leggere e non si è verificato alcun errore
  • riga 27: la riga letta viene suddivisa in campi tramite l'espressione regolare della riga 16
  • righe 29-31: si verifica che la riga contenga effettivamente tre campi; si registra un eventuale errore
  • righe 33-38: conversione delle tre stringhe in tre numeri decimali; si registrano eventuali errori
  • righe 40-43: se si è verificato un errore, viene generata un'eccezione di tipo FileImpotException.
  • righe 44-47: se non si è verificato alcun errore, si passa alla lettura della riga successiva del file di testo dopo aver memorizzato i dati della riga corrente.
  • righe 52-55: all'uscita dalla bocca while, i dati della lista generica listTranchesImpot vengono ricopiati nella tabella tranchesImpot della classe base AbstractImpot. Si ricorda che questo era l’obiettivo del costruttore.
  • righe 56-59: gestione di un’eventuale eccezione. Questa viene incapsulata in un oggetto di tipo FileImpotException.
  • riga 61: se l’eccezione fe della riga 18 è stata inizializzata, allora viene generata.

L'intero progetto C# è il seguente:

  • in [1]: l'intero progetto
  • in [2,3]: le proprietà del file [DataImpot.txt] [2]. La proprietà [Copy to Output Directory] [3] è impostata su "always". In questo modo, il file [DataImpot.txt] verrà copiato nella cartella bin/Release (modalità Release) o bin/Debug (modalità Debug) ad ogni esecuzione. È lì che viene cercato dall’eseguibile.
  • In [4]: si procede allo stesso modo con il file [DataImpotInvalide.txt].

Il contenuto di [DataImpot.txt] è il seguente:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Il contenuto di [DataImpotInvalide.txt] è il seguente:

a:b:c

Il programma di test [Program.cs] non è cambiato: è quello della versione 2, paragrafo 4.10, con la seguente differenza:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
...
             // creazione di un oggetto IImpot
            IImpot impot = null;
            try {
                // creazione di un oggetto IImpot
                impot = new FileImpot("DataImpot.txt");
            } catch (FileImpotException e) {
                // visualizzazione errore
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // arresto del programma
                Environment.Exit(1);
            }

            // ciclo infinito
            while (true) {
...
            }//while
        }
    }
}
  • riga 8: oggetto impot del tipo dell'interfaccia IImpot
  • riga 11: istanziazione dell'oggetto impot con un oggetto di tipo FileImpot. Ciò può generare un'eccezione che viene gestita dal blocco try/catch alle righe 9, 12 e 18.

Ecco alcuni esempi di esecuzione:

Con il file [DataImpot.txt]

1
2
3
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :

Con un file [xx] inesistente

L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

Con il file [DataImpotInvalide.txt]

L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n° 1 incorrecte]