INTRODUZIONE AI GENERICS

In C# esistono due categorie su cui possiamo applicare il meccanismo dei Generics. I Tipi generici e i metodi.

  • Tipi generici (class, interface, struct, delegate)
  • Metodi generici

C sharpCon i generics in C Sharp possiamo definire dei parametri di tipo. Convenzionalmente un parametro di tipo si indica con la lettera T, essa è semplicemente un segnaposto che ci consente di rimandare la creazione dei veri e propri oggetti o l’invocazione di un metodo con un tipo che sarà introdotto nel momento in cui utilizziamo i tipi generici o i metodi generici.

METODI IN OVERLOAD

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            int a = 4, b = 60;
            Console.WriteLine($"Prima della chiamata, a: {a + " b: "+ b}");
            mc.Swap(ref a,ref b);
            Console.WriteLine($"Dopo la chiamata, a: {a + " b: "+ b}");
        }
    }
    public class MyClass
    {
        public void Swap(ref int a, ref int b)
        {
            int temp;
            temp = a;
            a=b;
            b=temp;
        }
        public void Swap(ref double a, ref double b)
        {
            double temp;
            temp = a;
            a=b;
            b=temp;
        }
    }
}

Come vedi dal codice sopra riportato, fino adesso si è proceduto utilizzando l’overload dei metodi, cioè metodi aventi lo stesso nome ma parametri differenti. Questo è un grosso beneficio perché invece di ricorrere a metodi con nome SwapInt o SwapDouble o simili abbiamo riutilizzato lo stesso nome di metodo. Il codice sopra riportato definisce una classe MyClass con dei metodi Swap che non fanno altro che scambiare i valori delle variabili passate in input. Per far ciò noterai che ho usato la keyword ref sia nel metodo che nell’invocazione. Come vedremo più avanti la parola chiave ref definita nel metodo fa si che i tipi primitivi siano trattati come reference types, che è quello che ci serve in questo caso per poter fare lo scambio dei reference. Vediamo come i Generics ci vengono incontro per migliorare ulteriormente il metodo Swap.

METODI GENERICI

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            int a = 4, b = 60;
            Console.WriteLine($"Prima della chiamata, a: {a + " b: "+ b}");
            mc.Swap<int>(ref a,ref b);
            Console.WriteLine($"Dopo la chiamata, a: {a + " b: "+ b}");
        }
    }
    public class MyClass
    {
        public void Swap<T>(ref T a, ref T b)  
        {  
            T temp;  
            temp = a;  
            a = b;  
            b = temp;  
        } 
    }
}

ANALISI DEL METODO GENERICO

Il segnaposto T introdotto tra le parentesi angolari indica al compilatore che stiamo definendo un metodo generico, cosa che poi abbiamo fatto nei parametri del metodo indicando che due parametri di tipo T verranno passati per riferimento. T non è un vero tipo di dato ma un segnaposto che indica che nel momento in cui invochiamo il metodo ci impegniamo a fornire un parametro di qualche tipo, int, double, string, bool, byte etc.

I GENERICS IN C SHARP L’INVOCAZIONE DEL METODO GENERICO

Come vedi dal codice, per invocare un metodo generico si racchiude tra le parentesi angolari il tipo come dichiarato nella definizione del metodo generico, in questo caso <int> e i parametri che devono rispettare l’indicazione del metodo Swap<T>, ossia devono essere due interi passati per riferimento. Di fatto abbiamo eliminato l’overload dei metodi, definendone uno soltanto, inoltre il compilatore è in grado di inferire il tipo dai tipi di parametri passati, quindi possiamo scrivere semplicemente mc.Swap(ref a, ref b). Riporto il codice di una classe stampante che ha definito al suo interno un metodo public Stampa<T> generico.

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            Stampante s = new Stampante();
            s.Stampa<int>(10);
            s.Stampa<string>("Mario");
        }
    }
    public class Stampante
    {
        public void Stampa<T>(T a)  
        {  
            Console.WriteLine(a);
        } 
    }
}

Possiamo specificare più segnaposti per data types differenti vediamo un esempio.

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            Stampante s = new Stampante();
            s.Stampa<int,string>(10,"Mario");
            s.Stampa<string,char>("Corso C#",'A');
        }
    }
    public class Stampante
    {
        public void Stampa<T,S>(T a,S b)  
        {  
            Console.WriteLine($"{a + " " + b}");
        } 
    }
}

Inoltre possiamo usare un solo parametro di tipo e all’interno dei parametri specificare più argomenti.

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            Stampante s = new Stampante();
            s.Stampa<int>(10,30);
            s.Stampa<string>("Corso C#","Mario");
        }
    }
    public class Stampante
    {
        public void Stampa<T>(T a,T b)  
        {  
            Console.WriteLine($"{a + " " + b}");
        } 
    }
}

Possiamo inoltre specificare un valore di ritorno.

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            Stampante s = new Stampante();
            int result1 = s.Stampa<int>(10,30);
            string result2 = s.Stampa<string>("Corso C#","Mario");
        }
    }
    public class Stampante
    {
        public T Stampa<T>(T a,T b)  
        {  
            Console.WriteLine($"{a + " " + b}");
            return a;
        } 
    }
}

I GENERICS IN C SHARP LE CLASSI GENERICHE

Le classi generiche vengono usate spesso per gestire Collezioni di dati come Array, Liste, Dictionary etc. In questo esempio di codice ti introduco una lista generica List<T> una classe fortemente tipizzata e di largo uso che vedremo in un post successivo.  Come vedi per dichiarare una classe generica dopo il nome tra parentesi angolari va specificato il tipo T che indica al compilatore che stiamo definendo una classe generica.

using System;
using System.Collections;
using System.Collections.Generic;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                MyGenericList<string> list = new MyGenericList<string>();
                list.Add("Mario");
                list.Add("Corso C#");
                list.Sort();
                foreach(var item in list)
                {
                    Console.WriteLine(item);
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    public class MyGenericArray<T>
    {
        private T [] array;
        public MyGenericArray (int size)
        {
            array = new T [size + 1];
        }
        public T getItem (int index)
        {
            return array [index];
        }
        public void setItem (int index, T value)
        {
            array [index] = value;
       

        }
    }
    public class MyGenericList<T>
    {
        private T item; 
        private readonly List<T> lista = new();
        public MyGenericList()
        {
            item = default;
        }        
        public void Add(T item)
        {
            try
            {
               lista.Add(item);
            }
            catch(Exception)
            {
                throw new Exception();
            } 
        }
        public void Remove(T item)
        {
            try
            {
               lista.Remove(item);
            }
            catch(Exception)
            {
                throw new Exception();
            }
            
        }
        public void Sort()
        {
            lista.Sort();
        }
        public IEnumerator GetEnumerator()
        {
            return lista.GetEnumerator();
        }
    }
}

LA PAROLA CHIAVE DEFAULT

 

A volte è necessario impostare un valore predefinito per un attributo generico. Nella classe MyGenericList per farti vedere come si inizializza un attributo generico ho dichiarato un membro privato private T item; non conoscendo a priori quale sarà il tipo T per inizializzarlo correttamente nel costruttore della classe usiamo la parola chiave default. Con tale keyword i reference type vengono impostati a null, i valori numerici a zero e per le struct i membri generici vengono impostati a zero se sono value type o a null se sono reference type.

I GENERICS IN C SHARP I CONSTRAINT

Spesso è necessario impostare dei vincoli (constraint) sui i tipi generici che dichiariamo classi, struct, interface e come vedremo i delegate. Nella pratica impostare un vincolo significa dire al compilatore che il tipo generico che intendiamo usare dovrà soddisfare determinati requisiti. Per far ciò si usa la keyword where immediatamente dopo la dichiarazione del tipo. Ad esempio:

class MyGenericList<T> where T: struct

Con questa istruzione vincoliamo il compilatore e quindi l’utilizzatore della classe a fornire per il parametro generico T solo dei tipi valore. Ma vediamo ciò che possiamo indicare con la clausola where.

  • where T:struct con questo constraint restringiamo i possibili valori per il segnaposto T ai soli value type.
  • where T:class Il segnaposto T dovrà essere un reference type.
  • where T:new() La classe generica dovrà possedere un costruttore pubblico e senza parametri. Se introduciamo questo particolare vincolo deve essere specificato per ultimo nel caso ce ne siano altri. Infatti si possono avere più vincoli separati da una virgola.
  • where T:nome della classe base Il parametro formale T deve corrispondere ad una classe base o a una sua sottoclasse.

I GENERICS IN C SHARP LE INTERFACE GENERICHE

Vediamo come si dichiara una interfaccia generica con all’interno un metodo generico.

public interface IMyInterface<T>

{

        T MyMethod(T item);

}

Vediamo un esempio di codice riguardante le interface generiche e le struct.

using System;
using System.Collections;
using System.Collections.Generic;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                MyClass<int> mc = new MyClass<int>();
                mc.Print(12,24);
                MyStruct<MyClass<int>> myStruct = new MyStruct<MyClass<int>>();
                myStruct.item = mc;
                Console.WriteLine(myStruct.ToString());

            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    public interface IInterface<T> where T:struct
    {
        void Print(T arg1, T arg2);
    }
    public class MyClass<T>:IInterface<T> where T:struct
    {
        public void Print(T arg1, T arg2)
        {
            Console.WriteLine($"{arg1 + " " + arg2}");
        }
    }
    public struct MyStruct<T> where T:class
    {
        public T item;
        public override string ToString()
        {
            return item.ToString();
        }
    }
}

NULLABLE TYPES

Come sappiamo I tipi primitivi sono dei value types e non ammettono valori null come invece succede per i reference types. A volte però potrebbe far comodo assegnare un valore null ad esempio ad un boolean in quanto può essere in uno stato ancora non definito, ossia il suo valore non è né true né false. Per venire incontro a queste esigenze il linguaggio C# ci mette a disposizione i Nullable types. Vediamo un esempio di codice.

using System;
namespace Generics
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
               /*--NULLABLE TYPES*/ 
               int myInt1 = 0; //SE PROVIAMO AD ASSEGNARE NULL A myInt1 OTTENIAMO UN ERRORE.
               //DEFINIZIONE DI UN NULLABLE TYPES
               int? myInt2 = 0;/*--PER FAR SI CHE IL TIPO INT DICHIARATO
                              POSSA ASSUMERE VALORI NULL OCCORRE METTERE UN ?
                              DOPO LA KEYWORD int*/
               myInt2=null;
               /*--TALE DICHIARAZIONE E' IN REALTA' UNA SCORCIATOIA, ESISTE UNA STRUTTURA
               NULLABLE<T> CHE E' EQUIVALENTE ALLA DICHIARAZIONE SOPRA RIPORTATA.*/
               Nullable<int> myInt = 0; //EQUIVALE A int? myInt = 0.
               myInt=null;
               if (myInt.HasValue)//QUESTO TEST FALLISCE IN QUANTO myInt = null.
                     Console.WriteLine(myInt.Value);
               myInt=100; 
               if (myInt.HasValue)
                     Console.WriteLine(myInt.Value);  
                myInt=null;     
                /*--NULL COALESCING OPERATOR ?? CI CONSENTE DI ASSEGNARE IL VALORE EFFETTIVO
                DI UN NULLABLE TYPES SE QUESTO E' PRESENTE OPPURE ASSEGNARE NOI UN VALORE
                DI DEFAULT SE PRESENTE NULL. */ 
                myInt1 = myInt ?? 0; /*--IN QUESTO CASO IL VALORE DI myInt è null 
                                    QUIDNI myInt1 ASSUMERA' IL VALORE 0. 
                                    SE myInt AVEVA VALORE 100 ALLORA myInt1
                                    AVREBBE ASSUNTO LO STESSO VALORE.*/

            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

I GENERICS IN C SHARP CODICE DEGLI ESEMPI UTILIZZATI

using System;
using System.Collections;
using System.Collections.Generic;

namespace Generics
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            try
            {
                /*--NULLABLE TYPES*/
                var myInt1 = 0; //SE PROVIAMO AD ASSEGNARE NULL A myInt1 OTTENIAMO UN ERRORE.
                //DEFINIZIONE DI UN NULLABLE TYPES
                int? myInt2 = 0; /*--PER FAR SI CHE IL TIPO INT DICHIARATO
                               POSSA ASSUMERE VALORI NULL OCCORRE METTERE UN ?
                               DOPO LA KEYWORD int*/
                myInt2 = null;
                /*--TALE DICHIARAZIONE E' IN REALTA' UNA SCORCIATOIA, ESISTE UNA STRUTTURA
                NULLABLE<T> CHE E' EQUIVALENTE ALLA DICHIARAZIONE SOPRA RIPORTATA.*/
                int? myInt = 0; //EQUIVALE A int? myInt = 0.
                myInt = null;
                if (myInt.HasValue) //QUESTO TEST FALLISCE IN QUANTO myInt = null.
                    Console.WriteLine(myInt.Value);
                myInt = 100;
                if (myInt.HasValue)
                    Console.WriteLine(myInt.Value);
                myInt = null;
                /*--NULL COALESCING OPERATOR ?? CI CONSENTE DI ASSEGNARE IL VALORE EFFETTIVO
                DI UN NULLABLE TYPES SE QUESTO E' PRESENTE OPPURE ASSEGNARE NOI UN VALORE
                DI DEFAULT SE PRESENTE NULL. */
                myInt1 = myInt ?? 0; /*--IN QUESTO CASO IL VALORE DI myInt è null
                                    QUIDNI myInt1 ASSUMERA' IL VALORE 0.
                                    SE myInt AVEVA VALORE 100 ALLORA myInt1
                                    AVREBBE ASSUNTO LO STESSO VALORE.*/
                var s = new Stampante();
                var result1 = s.Stampa(10, 30);
                var result2 = s.Stampa("Corso C#", "Mario");
                var list = new MyGenericList<string>();
                list.Add("Mario Rossi");
                list.Add("Corso C#");
                list.Add("Mario Verdi");
                foreach (var item in list) Console.WriteLine(item);
                var mc = new MyClass<int>();
                mc.Print(12, 24);
                var myStruct = new MyStruct<MyClass<int>>();
                myStruct.item = mc;
                Console.WriteLine(myStruct.ToString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

    public class Stampante
    {
        public T Stampa<T>(T a, T b)
        {
            Console.WriteLine($"{a + " " + b}");
            return a;
        }
    }

    public class MyGenericArray<T>
    {
        private readonly T[] array;

        public MyGenericArray(int size)
        {
            array = new T [size + 1];
        }

        public T getItem(int index)
        {
            return array[index];
        }

        public void setItem(int index, T value)
        {
            array[index] = value;
        }
    }

    public class MyGenericList<T>
    {
        private T item; /*--QUANDO VOGLIAMO INIZIALIZZARE L'ELEMENTO GENERICO ITEM
                       NON SAPENDO A PRIORI SE T E' UN VALUE TYPE O UN REFERENCE TYPE
                       SI USA LA KEYWORD default CHE INIZIALIZZA A NULL I REFERENCE TYPE
                       A ZERO I VALORI NUMERICI E A NULL O A 0 I MEMBRI DELLE STRUCT, A 0
                       SE SONO VALUE TYPE A NULL SE SONO REFERENCE TYPE.
                       */

        private List<T> lista = new();

        public MyGenericList()
        {
            item = default;
        }

        public void Add(T item)
        {
            try
            {
                lista.Add(item);
            }
            catch (Exception)
            {
                throw new Exception();
            }
        }

        public void Remove(T item)
        {
            try
            {
                lista.Remove(item);
            }
            catch (Exception)
            {
                throw new Exception();
            }
        }

        public void Sort()
        {
            lista.Sort();
        }

        public IEnumerator GetEnumerator()
        {
            return lista.GetEnumerator();
        }
    }

    public interface IInterface<T> where T : struct
    {
        void Print(T arg1, T arg2);
    }

    public class MyClass<T> : IInterface<T> where T : struct
    {
        public void Print(T arg1, T arg2)
        {
            Console.WriteLine($"{arg1 + " " + arg2}");
        }
    }

    public struct MyStruct<T> where T : class
    {
        public T item;

        public override string ToString()
        {
            return item.ToString();
        }
    }
}

APPROFONDIMENTO

Generics in C# (Generici in C#)

I generics sono una funzionalità potente e flessibile di C# che permette di definire classi, metodi, interfacce e delegati con un parametro di tipo, permettendo loro di operare su qualsiasi tipo specificato al momento dell’uso. Questo permette di creare codice più riutilizzabile e sicuro, riducendo la necessità di effettuare conversioni di tipo (cast) e migliorando la sicurezza del tipo a tempo di compilazione.

Vantaggi dei Generics

1. Riutilizzabilità del Codice: I generics permettono di creare classi e metodi che funzionano con qualsiasi tipo specificato dall’utente.

2. Sicurezza dei Tipi: I generics forniscono il controllo del tipo a tempo di compilazione, riducendo la possibilità di errori di runtime dovuti a conversioni di tipo non valide.

3. Prestazioni: L’uso dei generics evita il boxing e unboxing, migliorando le prestazioni quando si lavora con tipi di valore.

Sintassi Base dei Generics

Classi Generiche:

public class MyGenericClass<T>
{
         private T data;

         public MyGenericClass(T value)
         {
             data = value;
         }

        public T GetData()
        {
             return data;
        }
}

Metodi Generici:

public class MyClass
{
    public void Display(T value)
{
Console.WriteLine(value);
}
}

Esempio di Utilizzo dei Generics

1. Creare una Classe Generica:

public class GenericList<T>
{
       private List list = new List();

       public void Add(T item)
       {
          list.Add(item);
       }

      public T Get(int index)
      {
           return list[index];
      }
}

2. Utilizzare una Classe Generica:

class Program
{
      static void Main(string[] args)
      {
         GenericList<int> intList = new GenericList<int>();
         intList.Add(1);
         intList.Add(2);
         Console.WriteLine(intList.Get(0)); // Output: 1

         GenericList<string> stringList = new GenericList<string>();
         stringList.Add(“Hello“);
         stringList.Add(“World“);
         Console.WriteLine(stringList.Get(1)); // Output: World
     }
}

Vincoli sui Tipi Generici

C# permette di specificare vincoli sui parametri di tipo per limitare i tipi che possono essere utilizzati con una classe o un metodo generico.

Vincolo su Classe Specifica:

public class MyClass<T> where T : class
{
// Solo tipi di riferimento (reference types) sono permessi.
}

Vincolo su Interfaccia Specifica:

public class Repository<T> where T : IEntity
{
 // Solo tipi che implementano l’interfaccia IEntity sono permessi.
}

Vincolo su Costruttore senza Parametri:

public class Factory<T> where T : new()
{
     public T CreateInstance()
{
       return new T();
}
}

LINK AI PRECEDENTI POST

LINK AL CODICE SU GITHUB