Language: EN

csharp-genericos

What are generics and how to use them in C#

Generics in C# are a feature that allows us to define classes, interfaces, and methods dependent on a data type that 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 any type of data while maintaining compile-time type error detection.

Creating and using generic classes

Defining 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 with any specific type when creating an instance of the class.

public class MyClass<T>
{
    private T content;

    public MyClass(T content)
    {
        this.content = content;
    }

    public T GetContent()
    {
        return content;
    }

    public void DisplayContent()
    {
        Console.WriteLine($"The content of the box is: {content}");
    }
}

In fact, <T> is a convention inherited from Templating (a feature of C++). But any other name is possible. For example, like this:

public class MyClass<type1>
{
   // ... content of MyClass
}

It is common to use descriptive names for type parameters, improving code readability. Instead of T, consider using names like TElement, TResult, etc.

Instantiating a generic class

Now, to instantiate a generic class, we must specify the data type that will be used instead of the type parameter T.

// For integer
MyClass<int> integerBox = new MyClass<int>(123);
integerBox.DisplayContent(); // The content of the box is: 123

// For string
MyClass<string> stringBox = new MyClass<string>("Hello");
stringBox.DisplayContent(); // The content of the box is: Hello

Generic interfaces and delegates

There are other types of objects that can be generic. But the operation is the same for all of them. For example, interfaces and delegates can also be generic.

Here’s how a generic interface would look:

public interface IRepository<T>
{
    void Add(T item);
    T Get(int id);
}

While a generic delegate would look like this:

public delegate T Function<T>(T arg);

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.

You will find them frequently. 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 strings.

// list of integers
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// list of strings
List<string> strings = 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.

For example,

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:

ConditionDescription
T: structThe type must be a value type
T: classThe type must be a reference
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.

Let’s see it with an example,

public class Converter
{
    public TResult Convert<TInput, TResult>(TInput input)
    {
		TResult result = // whatever with input        
        return result;
    }
}

In this example, the method Convert accepts a type parameter TInput and returns a value of type TResult.