Le novità di C# 2.0

La grossa novità della nuova versione del linguaggio C# è sicuramente l’introduzione dei generics. Ma a cosa servono i generics?

Partiamo con un esempio che chiarirà tutti i dubbi.

Implementiamo una classe stack come si farebbe oggi con l’attuale versione del framework. Premesso che esistono diverse implementazioni ora potremmo scriverla cosi:

 

public class OldStack

{

      Object[] _items;

      int _basePointer;

     

      public OldStack(int size)

      {

            _items = new object[size];

            _basePointer = 0;

      }

 

      public void Push(object value)

      {

            if (_basePointer < _items.Length)

            {

                  _items[_basePointer] = value;

                  _basePointer++;

            }

            else

                  throw new IndexOutOfRangeException("Stack Pieno");

      }

     

      public object Pop()

      {

            if (_basePointer > 0)

                  return _items[--_basePointer];

            else

                  throw new IndexOutOfRangeException("Stack Vuoto");

      }

}

 

In questo caso i dati del nostro stack sono memorizzati in un array di Object, non conoscendo a priori il tipo di dato che dovrà contenere, con Object siamo sicuri che qualsiasi tipo il programmatore decida di usare la nostra classe funzionerà correttamente.

Vorrei farvi notare i due metodi Push e Pop che lavorano su oggetti di tipo Object. Dopo capirete perché ve li ho fatti notare.

Come lo uso?

 

OldStack stack = new OldStack(10);

stack.Push(10);

stack.Push(4);

//…

int valore = (int)stack.Pop();

 

Ok. Ma…

…non ci piace.

Perchè.

  1. Quando faccio una push di un intero il valore verrà trasformato in object (boxing)
  2. Quando eseguo una Pop devo fare il cast a intero visto che il metodo Pop ritorna un Object
  3. Non ho alcun controllo sul tipo di dato inserito. Se il programmatore scrive stack.Push(“Ma che bella giornata”); il compilatore non segnala alcun errore e a runtime probabilmente otterrò un errore.

 

A questo punto, per cercare di risolvere questi tre problemi, se non ci fossero i generics, deriverei o incapsulerei la classe OldStack con una mia classe stack che invece di lavorare su Object lavora su un tipo particolare (ad esempio Int32, String, ecc…).

Ok. Ma…

…anche in questo modo non mi piace

  1. Devo scrivere un sacco di codice “inutile”.
  2. La classe Int32Stack o StringStack ha ancora tutti i problemi elencati prima.

 

Questi sono i problemi che risolvo elegantemente con i generics.

 

Quindi con il framework 2.0 riscriverò la mia classe stack in questo modo:

 

public class NewStack<T>

{

      T[] _items;

      int _basePointer;

 

      public NewStack(int size)

      {

            _items = new T[size];

            _basePointer = 0;

      }

 

      public void Push(T value)

      {

            if (_basePointer < _items.Length)

            {

                  _items[_basePointer] = value;

                  _basePointer++;

            }

            else

                  throw new IndexOutOfRangeException("Stack Pieno");

      }

     

      public T Pop()

      {

            if (_basePointer > 0)

                  return _items[--_basePointer];

            else

                  throw new IndexOutOfRangeException("Stack Vuoto");

      }

}

 

Le differenze visibili sono davvero poche (si potrebbe riscrivere con un find&replace di Object in T), la chiave sta tutta nella dichiarazione della classe:

 

public class NewStack<T>

 

Dichiaro una classe generica NewStack che lavora sul tipo “T”. Cos’è il tipo T? T è un alias per un tipo che verrà definito da chi utilizza la mia classe (l’ho chiamato T ma potrei chiamarlo “Pippo”…anche se le linee guida consigliano di usare T). Quindi ora l’array non sarà di object ma di T e i metodi Push e Pop lavoreranno sul tipo T non su object.

L’utilizzo è semplicissimo, preciso ed elegante:

 

NewStack<int> stack = new NewStack<int>(10);

stack.Push(10);

stack.Push(4);

//…

int valore = stack.Pop();

 

Ora sto dichiarando uno stack di Int32 in cui potrò inserire solo interi, non posso scrivere stack.Push(“Ma che bella giornata”); il compilatore segnalerebbe un errore (sottolineo il compilatore) e non è necessario effettuare il cast sulla chiamata al metodo Pop.

Questo si traduce in:

-          migliori performance: a runtime non ci sono più cast e boxing

-          maggior controllo sugli errori: sono certo che lo stack  conterrà solo interi

-          meno codice da scrivere: non è necessario riscrivere la classe stack per ogni tipo che lo deve utilizzare

 

L’esempio dello stack dovrebbe chiarire cosa sono i generics a cosa servono ma non svela altre cose dei generics.

Ad esempio supponiamo di voler inizializzare una variabile di tipo T all’interno della classe generica. Cosa scriviamo?

 

T obj = null; o T obj = 0;

 

Non possiamo saperlo perché T potrebbe essere un value type o un reference type. Quindi?

Quindi usiamo lo statement default:

 

T obj = default(T)

 

Che altro non fa che valorizzare obj a null o a 0 a seconda del fatto che T sia un reference type o un value type rispettivamente.

Sempre sul tema delle differenze tra value type e reference type supponiamo di dover controllare se un oggetto di tipo T è null:

 

T obj = GetResult()

if (obj == null)

 

L’istruzione if ha significato solo se obj è un reference type se obj è un value type il compilatore darebbe un errore. Con i generics l’istruzione può essere scritta senza ottenere errori a runtime il JITter valuterà se ha senso eseguirla o meno valutando il tipo di obj.

 

Supponiamo ora di voler implementare una classe astratta che contiene una collezione di oggetti ordinabili:

 

public class MySortedCollection<T>

{

//…

}

 

Per poter ordinare una serie di oggetti è necessario trovare un modo per confrontarli, per il framework .NET si traduce che gli oggetti implementino l’interfaccia IComparable.

Ok, ma come faccio ad obbligare gli “utenti” della mia classe ad usarla solo con tipi che implementino l’interfaccia IComparable?

Risposta: usando la clausola where. In questo modo:

 

public class MySortedCollection<T> where T : IComparable

{

//…

}

 

Questa dichiarazione garantisce che gli oggetti che verranno usati con la classe MySortedCollection implementino l’interfaccia specificata e questo vuol dire che all’interno della classe posso scrivere:

 

T obj1 = GetResult();

obj1.CompareTo(obj2);

 

Per ora mi fermo qua, ci sarebbe molto altro da dire, lasciate un commento su cosa vi interessa approfondire, non garantisco che lo farò ma se ho tempo lo faccio volentieri…

Print | posted on Saturday, February 26, 2005 2:44 PM