INTRODUCTION TO GENERICS
In C # there are two categories in which we can apply the Generics mechanism. Generic types and methods.
- Generic types (class, interface, struct, delegate)
- Generic Methods
With generics in C Sharp we can define type parameters in Generic Types or Generic Methods. Conventionally they are indicated with the letter T, which is simply a placeholder that allows us to postpone the creation of real objects or the invocation of a method with a type that will be introduced when we use generic types or generic methods.
OVERLOAD METHODS
using System; namespace Generics { class Program { static void Main(string[] args) { MyClass mc = new MyClass(); int a = 4, b = 60; Console.WriteLine($"First of the call, a: {a + " b: "+ b}"); mc.Swap(ref a,ref b); Console.WriteLine($"After of the call, 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; } } }
As you can see up to now we have proceeded using the overload of the methods, that is methods with the same name but different parameters. This is a big benefit because instead of resorting to methods named SwapInt or SwapDouble or similar we reused the same method name. The above code defines a MyClass class with Swap methods that do nothing but swap the values of the variables passed in input. To do this, you will notice that I used the ref keyword both in the method and in the invocation. As we will see later, the ref keyword defined in the method causes primitive types to be treated as reference types, which is what we need in this case to be able to exchange references. Let’s see how the Generics come to meet us to further improve the Swap method.
GENERIC METHODS
using System; namespace Generics { class Program { static void Main(string[] args) { MyClass mc = new MyClass(); int a = 4, b = 60; Console.WriteLine($"First of the call, a: {a + " b: "+ b}"); mc.Swap<int>(ref a,ref b); Console.WriteLine($"After of the call, 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; } } }
ANALYSIS OF THE GENERIC METHOD
The placeholder T introduced between the angle brackets tells the compiler that we are defining a generic method, which we then did in the method parameters indicating that two parameters of type T will be passed by reference. T is not a real data type but a placeholder indicating that when we invoke the method we are committed to providing a parameter of some type, int, double, string, bool, byte etc.
THE GENERICS IN C SHARP THE INVOCATION OF THE GENERIC METHOD
As you can see from the code, to invoke a generic method the type as declared in the definition of the generic method is enclosed in angle brackets, in this case and the parameters that must respect the indication of the Swap<T> method, i.e. they must be two integers passed for reference. In fact, we have eliminated the overload of the methods, defining only one, moreover the compiler is able to infer the type from the types of parameters passed, so we can simply write
mc.Swap (ref a, ref b);
I report the code of a printer class that has defined a generic public Stampa<T> method within it.
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); } } }
We can specify multiple placeholders for different data types see an example.
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>("Course C#",'A'); } } public class Stampante { public void Stampa<T,S>(T a,S b) { Console.WriteLine($"{a + " " + b}"); } } }
We can also use a single type parameter and within the parameters specify multiple arguments.
using System; namespace Generics { class Program { static void Main(string[] args) { Stampante s = new Stampante(); s.Stampa<int>(10,30); s.Stampa<string>("Course C#","Mario"); } } public class Stampante { public void Stampa<T>(T a,T b) { Console.WriteLine($"{a + " " + b}"); } } }
We can also specify a return value.
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>("Course C#","Mario"); } } public class Stampante { public T Stampa<T>(T a,T b) { Console.WriteLine($"{a + " " + b}"); return a; } } }
THE GENERICS IN C SHARP THE GENERIC CLASSES
Generic classes are often used to manage data collections such as Arrays, Lists, Dictionary etc. In this code example I will introduce you to a generic List<T>, a strongly typed and widely used class that we will see in a later post. As you can see, to declare a generic class after the name in angle brackets, the type T must be specified which indicates to the compiler that we are defining a generic class.
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(); } }
THE KEYWORD DEFAULT
Sometimes it is necessary to set a default value for a generic attribute. In the MyGenericList<T> class to show you how to initialize a generic attribute I have declared a private member private T item; not knowing a priori what the type T will be to initialize it correctly in the constructor of the class we use the keyword default. With this keyword the reference types are set to null, the numeric values to zero and for structs the generic members are set to zero if they are value types or to null if they are reference types.
I GENERICS IN C SHARP I CONSTRAINT
Often it is necessary to set constraints on the generic types that we declare classes, structs, interfaces and as we will see the delegates. In practice, setting a constraint means telling the compiler that the generic type we intend to use will have to meet certain requirements. To do this, use the where keyword immediately after the type declaration. For example:
class MyGenericList<T> where T: struct
With this instruction we constrain the compiler and therefore the user of the class to provide only value types for the generic parameter T. But let’s see what we can indicate with the where clause.
- where T:struct with this constraint we restrict the possible values for the placeholder T to only the value types.
- where T:class The placeholder T should be a reference type.
- where T:new() The generic class must have a public, parameterless constructor. If we introduce this particular constraint this must be specified last in case there are others. In fact, it is possible to have several constraints separated by a comma.
- where T:nome della classe base The formal parameter T must correspond to a base class or a subclass of it.
THE GENERICS IN C SHARP THE GENERIC INTERFACES
Let’s see how a generic interface is declared with a generic method inside.
public interface IMyInterface<T>
{
T MyMethod(T item);
}
Let’s see a code example regarding generic interfaces and structs.
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
As we know Primitive types that are value types do not allow null values as do reference types. Sometimes, however, it might be convenient to assign a null value for example to a boolean as it can be in a state that is not yet defined, i.e. its value is neither true nor false. To meet these needs, the C# language provides us with Nullable types. Let’s see an example in the code.
using System; namespace Generics { class Program { static void Main(string[] args) { try { /*--NULLABLE TYPES*/ int myInt1 = 0; //IF WE TRY TO ASSIGN NULL TO myInt1 WE GET AN ERROR. //DEFINITION OF A NULLABLE TYPES int? myInt2 = 0;/*--TO ENSURE THAT THE TYPE INT DECLARED CAN ASSUME NULL VALUES MUST PUT A ? AFTER THE KEYWORD int*/ myInt2=null; /*--THIS STATEMENT IS ACTUALLY A SHORTCUT, THERE IS A STRUCTURE NULLABLE<T>; WHICH IS EQUIVALENT TO THE ABOVE DECLARATION.*/ Nullable<int> myInt = 0; //EQUIVAL TO int? myInt = 0. myInt=null; if (myInt.HasValue)//THIS TEST FAILS AS myInt = null. Console.WriteLine(myInt.Value); myInt=100; if (myInt.HasValue) Console.WriteLine(myInt.Value); myInt=null; /*--NULL COALESCING OPERATOR ?? ALLOWS US TO ASSIGN THE ACTUAL VALUE OF A NULLABLE TYPES IF THIS IS PRESENT OR ASSIGN US A VALUE THE DEFAULT IF PRESENT NULL. */ myInt1 = myInt ?? 0; /* - IN THIS CASE THE VALUE OF myInt is null SO myInt1 WILL ASSUME THE VALUE 0. IF myInt HAD VALUE 100 THEN myInt1 WOULD HAVE ASSUMED THE SAME VALUE. */ } catch(Exception ex) { Console.WriteLine(ex.Message); } } } }
THE GENERICS IN C SHARP CODE OF THE EXAMPLES USED
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(); } } }
DEEPENING
Generics in C# (Generics in C#).
Generics are a powerful and flexible feature of C# that allows classes, methods, interfaces, and delegates to be defined with a type parameter, allowing them to operate on any type specified at the time of use. This allows the creation of more reusable and safe code, reducing the need to perform type conversions (casts) and improving type safety at compile time.
Advantages of Generics.
1. Code Reusability: Generics allow the creation of classes and methods that work with any type specified by the user.
2. Type Safety: Generics provide compile-time type checking, reducing the possibility of runtime errors due to invalid type conversions.
3. Performance: The use of generics avoids boxing and unboxing, improving performance when working with value types.
Basic Syntax of Generics
– Generic Classes:
public class MyGenericClass<T>
{
private T data;
public MyGenericClass(T value)
{
data = value;
}
public T GetData()
{
return data;
}
}
– General Methods:
public class MyClass
{
public void Display(T value)
{
Console.WriteLine(value);
}
}
Example of the Use of Generics
1. Create a Generic Class:
public class GenericList
{
private List list = new List();
public void Add(T item)
{
list.Add(item);
}
public T Get(int index)
{
return list[index];
}
}
2. Use a Generic Class:
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
}
}
Constraints on Generic Types.
C# allows you to specify constraints on type parameters to limit the types that can be used with a generic class or method.
– Constraint on Specific Class:
public class MyClass<T> where T : class
{
// Solo tipi di riferimento (reference types) sono permessi.
}
– Constraint on Specific Interface:
public class Repository<T> where T : IEntity
{
// Solo tipi che implementano l’interfaccia IEntity sono permessi.
}
– Constraint on Constructor without Parameters:
public class Factory<T> where T : new()
{
public T CreateInstance()
{
return new T();
}
}
Leave A Comment