Generics in C# are a feature that allows us to define classes, interfaces, and methods dependent on a data type (which, logically, can vary).
This data type is specified at compile time, providing flexibility and type safety without sacrificing performance.
Generics allow us to write methods and classes that work with different data types, while maintaining type error detection at compile time.
Creation and use of generic classes
Definition of a generic class
To define a generic class, the symbol <T> is used after the class name. Here, T is a type parameter that can be replaced by any specific type when an instance of the class is created.
public class MyClass<T>
{
private T content;
public MyClass(T content)
{
this.content = content;
}
public T GetContent()
{
return content;
}
public void ShowContent()
{
Console.WriteLine($"The content of the box is: {content}");
}
}
Actually <T> is a convention, inherited from Templating (a C++ feature). But any other name is possible. For example, like this:
public class MyClass<type1>
{
// ... content of MyClass
}
Often, it’s common to use descriptive names for type parameters to improve code readability. Instead of T, consider using names like TElement, TResult, etc.
Instantiation of a generic class
Now, to instantiate a generic class, we must specify the data type to be used in place of the type parameter T (or whatever you named the type).
// For integer
MyClass<int> intBox = new MyClass<int>(123);
intBox.ShowContent(); // The content of the box is: 123
// For string
MyClass<string> stringBox = new MyClass<string>("Hello");
stringBox.ShowContent(); // The content of the box is: Hello
Generics in collections
Generic collections in .NET (in the System.Collections.Generic namespace) are widely used and offer a safer and more efficient alternative to non-generic collections.
Some of the most common generic collections include:
List<T>Dictionary<TKey, TValue>Queue<T>Stack<T>
For example, let’s see how to create a List for integers and for text strings.
// integer list
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// string list
List<string> letters = new List<string> { "A", "B", "C", "D", "E" };
Generic type constraints
Constraints allow limiting the types that can be used as arguments for type parameters. This is achieved using the where keyword.
public class Storage<T> where T : class
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T Get(int index)
{
return items[index];
}
}
In this example, the constraint where T : class ensures that only reference types can be used as type arguments for T.
Some common constraints include:
| Condition | Description |
|---|---|
T: struct | The type must be a value type. |
T: class | The type must be a reference type. |
T: new() | The type must have a parameterless constructor. |
T: <BaseClass> | The type must be or derive from a specific base class. |
T: <Interface> | The type must implement a specific interface. |
Generic methods with multiple types
A generic method with multiple type parameters allows specifying more than one type in the method definition.
public class Converter
{
public TResult Convert<TInput, TResult>(TInput input)
{
TResult result = // whatever with input
return result;
}
}
In this example, the Convert method accepts a parameter of type TInput and returns a value of type TResult.
