Tutorial
Usare le collezioni con C#


Data: Settembre-2002
Autore: (luKa)
Ambiente: .NET Framework v1.0

Download Esempi

Introduzione

Il .NET Framework dispone di una varietà di collezioni da utilizzare come contenitori di oggetti, questo tutorial introduce all'utilizzo consapevole di queste collezioni.
Le collezioni vengono descritte in modo estremamente pratico: il tutorial è da leggere tenendo il codice degli esempi citati costantemente a portata di mano per poterlo leggere ed eseguire passo-passo.
Ciò a cui punta il tutorial è spiegare il perché lasciando alla documentazione di riferimento ufficiale approfondire il come.

Una volta letto il tutorial e compreso gli esempi si sarà in grado di impiegare le potenzialità delle collezioni in progetti reali e di scegliere per ogni caso specifico la collezione che permette di ottenere prestazioni adeguate ed evita la scrittura di codice inutile.

Scegliere una collezione

Tutte le collezioni implementano l'interfaccia IEnumerable che viene estesa da ICollection. In particolare si distinguono due gruppi di collezioni che implementano due interfacce derivate da ICollection: IDictionary e IList.

Gerarchia delle classi

L'interfaccia IList è dedicata a collezioni di singoli valori, siano essi Value Type (quei tipi dato che se passati come parametro o assegnati vengono copiati per valore) o Reference Type (quei tipi dato che vengono passati come parametro o assegnati per riferimento), a cui si accede tramite indice. Ad implementarla sono le collezioni:
L'interfaccia IDictionary è dedicata a collezioni di coppie (Chiave, Valore) a cui si può accedere tramite chiave. Ad implementarla sono le collezioni:
Le altre collezioni che implementano l'interfaccia base ICollection invece sono:
Infine c'è la collezione System.Collections.Specialized.StringDictionary che implementa solo IEnumerable  e la collezione System.Collections.Specialized.BitVector32 che è una collezione "impropria" di bit/Boolean.

Questa classificazione divide le collezioni in base alle loro funzionalità e modalità di accesso (per indice, per chiave, etc.) e quindi è un primo passo per individuare quale collezione meglio soddisfa una particolare esigenza.
Inoltre ogni singola collezione si distingue per i tempi di accesso, i tempi di modifica (inserimento, cancellazione) e lo spazio occupato (memoria) al crescere del numero di elementi.

Se il numero di elementi non è elevato (una decina di migliaia), il seguente criterio di scelta è sufficiente:
  1. Se hai bisogno di un accesso sequenziale di tipo LIFO o FIFO usa Stack o Queue
  2. Se hai bisogno di un accesso per indice a singoli valori considera ArrayList e StringCollection 
    • se l'elemento è di tipo string usa StringCollection altrimenti...
    • usa ArrayList
  3. Se hai bisogno di un accesso per chiave a coppie chiave-valore considera HashtableListDictionary, e StringDictionary  
    • se l'elemento è di tipo string usa StringDictionary altrimenti...
    • se sono pochi gli elementi (poche decine) usa ListDictionary altrimenti...
    • usa Hashtable
  4. Se hai bisogno di un accesso sia per indice che per chiave considera SortedList e NameValueCollection
    • se ad una chiave possono essere associati più di un valore usa NameValueCollection altrimenti...
    • usa SortedList

Accedere agli elementi di una collezione 

foreach, IEnumerator e IEnumerable

Lo statement foreach permette di iterare tra gli elementi di una collezione.
L'iterazione deve avvenire in sola lettura (se non esplicitamente indicato il contrario). Aggiungere, rimuovere o spostare un elemento durante l'iterazione può causare effetti collaterali.
Si veda:
    Microsoft .NET Framework SDK Documentation - C# Programmer's Reference - foreach, in

La collezione deve essere una Interfaccia, una Classe o una Struttura, inoltre deve implementare l'interfaccia IEnumeberable da cui ottenere un iteratore cioè un oggetto che implementa la classe IEnumerator .
A proposito di IEnumerable si veda:
    MS .NET Framework SDK Doc - .NET Framework Class Library - IEnumerable Interface
A proposito di IEnumerator si veda:
    MS .NET Framework SDK Doc - .NET Framework Class Library - IEnumerator Interface

L'interfaccia ICollection eredita IEnumerable estendendola con metodi per la sincronizzazione e le dimensioni della collezione.

Anche gli array del C# ossia System.Array implementano tali interfaccie e quindi possono essere considerati collezioni.

Si veda l'esempio allegato FOREACH.CS che mostra come accedere alla collezione con il foreach. Lo stesso esempio mostra, a solo scopo dimostrativo, come accedere alla collezione usando direttamente le interfaccie IEnumerable e IEnumerator .

Collezioni Strong Typed

L'iteratore accessibile attraverso l'interfaccia IEnumerator è del tutto generico in quanto restituisce l'elemento corrente di tipo Object. E quindi è debolmente tipizzato.
In C# è possibile utilizzare collezioni fortemente tipizzate ossia che implementano una versione "specializzata" dell'interfaccia IEnumerator che restituisce l'elemento corrente del tipo specifico.
Questo ha due vantaggi:
  1. Al momento della compilazione è possibile verificare la corretta corrispondenza tra il tipo trattato dalla collezione e le variabili che sono l'origine o sono destinate ad accogliere gli elementi estratti dalla collezione;
  2. A Run-Time per le migliori prestazioni che diventano rilevanti qualora gli elementi della collezione siano Value-Type nel qual caso la tipizzazione evita continui Boxing e Unboxing (cioè le conversioni necessarie per poter trattare i Value-Type come oggetti).
A tale proposito si veda:
    MS .NET Framework SDK Doc - C# Programmer's Reference - Collection Classes Tutorial

Un esempio di collezione fortemente tipizzata è la StringCollection che ha un iteratore di tipo StringEnumerator la cui proprietà Current restituisce appunto un oggetto di tipo string ; a tale proposito si veda:
   MS .NET Framework SDK Doc - .NET Framework Class Library - StringEnumerator Class

Si veda l'esempio allegato FOREACH_STRONGTYPED.CS che evidenzia la differenza tra collezioni debolmente tipizzate e fortemente tipizzate.

Specificare l'ordinamento

Alcune collezioni prevedono e/o richiedono la disponibilità di un metodo di orinamento per:
  • inserire in maniera ordinata gli elementi, verificare eventuali inserimenti doppi, accedere ad un elemento della collezione;
  • iterare tra gli elementi di una collezione in maniera ordinata con lo statement foreach;
  • ordinare su richiesta una collezione o effettuare una ricerca su di una collezione ordinata.
L'ordinamento, a seconda della collezione usata, può dipendere da vari fattori:
1 - Può dipendere dal valore della chiave Hash (valore numerico ottenuto a partire dalla chiave che determina la posizione dove inserire o cercare l'elemento) restituita dal metodo GetHashCode() dell'elemento della collezione (sia esso l'elemento usato come chiave o valore; questo dipende dalla collezione impiegata); a tale proposito si veda:
MS .NET Framework SDK Doc - .NET Framework Class Lib - Object.GetHashCode Method
le collezioni che ne fanno uso sono: HashTable (per gli elementi usati come Key);
2 - In deroga al metodo GetHashCode() alcune collezioni prevedono la possibilità di indicare una funzione Hash alternativa ottenuta tramite una classe che implementa l'interfaccia IHashCodeProvider ; a tale proposito si veda:
MS .NET Framework SDK Doc - .NET Framework Class Lib - IHashCodeProvider Interface
le collezioni che ne fanno uso sono: HashTable (per gli elementi usati come Key) e NameValueCollection ;

3 - Può dipendere dalla funzione di confronto fornita attraverso l'interfaccia IComparable implementata dall'elemento della collezione (sia esso l'elemento usato come chiave o valore; questo dipende dalla collezione impiegata); a tale proposito si veda:
MS .NET Framework SDK Doc - .NET Framework Class Lib - IComparable Interface  
le collezioni che ne fanno uso sono: System.Array (per i metodi BinarySearch e Sort), ArrayList (per i metodi BinarySearch e Sort) e SortedList ;

4 - In deroga alla funzione di confronto fornita attraverso l'interfaccia IComparable alcune collezioni prevedono la possibilità di indicare criteri di confronto alternativi tramite una classe che implementa l'interfaccia IComparer; a tale proposito si veda:
MS .NET Framework SDK Doc - .NET Framework Class Lib - IComparer Interface
le collezioni che ne fanno uso sono: System.Array (per i metodi BinarySearch e Sort), Array List (per i metodi BinarySearch e Sort), HashTable (per gli elementi usati come Key), SortedList , ListDictionary e NameValueCollection;


La chiave Hash restituita (tramite la funzione GetHashCode ) ed il criterio di confronto (effettuato tramite IComparable ) devono essere definiti coerentemente col criterio di confronto implementato facendo l'overrite del metodo Equals e l'overloading dell' operatore di ugualianza ==.
A tale proposito si veda:
MS .NET Framework SDK Doc - .NET Framework General Reference - Guidelines for Implementing Equals and the Equality Operator (==)  

Si veda l'esempio HASH.CS  che mostra l'implementazione e l'utilizzo del metodo GetHashCode() .
Si vedano i seguenti esempi:
- SORT0.CS che evidenzia la necessità dell'interfaccia IComparable per l'ordinamento
- SORT1.CS che implementa IComparable con ordinamento alfabetico (Cognome, Nome)
- SORT2.CS che implementa il metodo Equals coerentemente con  IComparable
- SORT3.CS che implementa il metodo GetHashCode() coerentemente con  Equals
- SORT4.CS che implementa l'operatore di uguaglianza == e disugualianza != coerentemente con Equals
- SORT5.CS che implementa tramite IComparer un criterio di ordinamento alternativo

foreach e l'ordine di accesso

Il foreach accede agli elementi di una collezione attraverso un IEnumerator.
L'ordine con cui un IEnumerator fornisce gli elementi è casuale (è determinato da dettagli implementativi che possono variare senza preavviso) se non diversamente dichiarato esplicitamente nella documentazione di una collezione.

Per le collezioni di tipo (Chiave, Valore) ossia IDictionary è necessario esista un ordinamento per le chiavi e quindi è possibile che IEnumerator restituisca gli elementi secondo tale ordine (verificare nella documentazione della singola collezione).

Per le collezioni di valori ossia IList eventuali ordinamenti disponibili sugli elementi possono essere utilizzati da metodi quali ad esempio BinarySearch e Sort, ma questi non hanno effetto diretto sull'ordine con cui foreach accede agli elementi.

Per le collezioni quali System.Array (o che sono basate sull'Array come ArrayList) con quale ordine il foreach accede agli elementi dell'array? Il criterio è il seguente:

  • per gli Array a singola dimensione gli elementi vengono acceduti paretendo dall'indice 0 sino all'indice Length -1;
  • per gli Array a più dimensione gli elementi vengono acceduti incrementando progressivamente gli indici partendo da quelli più a destra.

Se si desidera un comportamento differente è sempre possibile implementare degli IEnumerator specializzati, come nell'esempio allegato (di Eric Gunnerson in persona!) iter.zip .