Understanding Generic Constraints in C#

Generics in C# allow you to define classes, methods, and interfaces with a placeholder for the type of data they store or use. This flexibility enables you to write more general and reusable code. However, sometimes you need to restrict the types that can be used with generics to ensure certain operations are safe or possible. This is where generic constraints come into play.

In this article, we’ll explore the various constraints that can be applied to generic type parameters in C#, providing complete examples for each.

1. where T: struct

The struct constraint ensures that the type parameter is a value type. This is useful when you want to ensure the generic type is a non-nullable value type like int, float, or a custom struct.

public class GenericStruct<T> where T : struct
{
    private T _value;

    public GenericStruct(T value)
    {
        _value = value;
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(T value)
    {
        _value = value;
    }
}

var genericStruct = new GenericStruct<int>(10);
Console.WriteLine($"Value: {genericStruct.GetValue()}"); // Output: Value: 10

2. where T: class

The class constraint ensures that the type parameter is a reference type. This is useful for ensuring the type is a class, interface, delegate, or array.

public class GenericClass<T> where T : class
{
    private T _instance;

    public GenericClass(T instance)
    {
        _instance = instance;
    }

    public T GetInstance()
    {
        return _instance;
    }

    public void SetInstance(T instance)
    {
        _instance = instance;
    }
}

var genericClass = new GenericClass<string>("Hello");
Console.WriteLine($"Instance: {genericClass.GetInstance()}"); // Output: Instance: Hello

3. where T: new()

The new() constraint ensures that the type parameter has a public parameterless constructor. This is useful when you need to create instances of the generic type within the class.

public class GenericWithConstructor<T> where T : new()
{
    private T _instance;

    public GenericWithConstructor()
    {
        _instance = new T();
    }

    public T GetInstance()
    {
        return _instance;
    }
}

var genericWithConstructor = new GenericWithConstructor<MyClass>();
Console.WriteLine($"Instance: {genericWithConstructor.GetInstance().Name}"); // Assuming MyClass has a Name property

4. where T: <base class>

This constraint ensures that the type parameter is a specific class or derives from a specific base class.

public class Animal { }

public class Dog : Animal { }

public class GenericWithBaseClass<T> where T : Animal
{
    private T _animal;

    public GenericWithBaseClass(T animal)
    {
        _animal = animal;
    }

    public T GetAnimal()
    {
        return _animal;
    }
}

var genericWithBaseClass = new GenericWithBaseClass<Dog>(new Dog());
Console.WriteLine($"Animal: {genericWithBaseClass.GetAnimal()}"); // Output: Animal: Dog

5. where T: <interface>

The interface constraint ensures that the type parameter implements a specific interface.

public interface IShape
{
    void Draw();
}

public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing Circle");
    }
}

public class GenericWithInterface<T> where T : IShape
{
    private T _shape;

    public GenericWithInterface(T shape)
    {
        _shape = shape;
    }

    public void DrawShape()
    {
        _shape.Draw();
    }
}

var genericWithInterface = new GenericWithInterface<Circle>(new Circle());
genericWithInterface.DrawShape(); // Output: Drawing Circle

6. where T: unmanaged

The unmanaged constraint ensures that the type parameter is an unmanaged type, which includes simple types like int, float, char, pointers, and enums.

public class GenericUnmanaged<T> where T : unmanaged
{
    private T _value;

    public GenericUnmanaged(T value)
    {
        _value = value;
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(T value)
    {
        _value = value;
    }
}

var genericUnmanaged = new GenericUnmanaged<int>(42);
Console.WriteLine($"Value: {genericUnmanaged.GetValue()}"); // Output: Value: 42

7. where T: <type parameter name>

This constraint ensures that the type parameter is the same as or inherits from another type parameter.

public class GenericWithTypeParam<T, U> where T : U
{
    private T _value;

    public GenericWithTypeParam(T value)
    {
        _value = value;
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(T value)
    {
        _value = value;
    }
}

// Usage
public class MyBaseClass { }
public class MyClass : MyBaseClass { }

var genericWithTypeParam = new GenericWithTypeParam<MyClass, MyBaseClass>(new MyClass());
Console.WriteLine($"Value: {genericWithTypeParam.GetValue()}"); // Output: Value: MyClass instance

8. where T: class, new()

This constraint ensures that the type parameter is a reference type and has a public parameterless constructor.

public class MyClass
{
    public string Name { get; set; }

    public MyClass()
    {
        Name = "Default";
    }
}

public class GenericWithMultipleTypes<T> where T : class, new()
{
    private T _value;

    public GenericWithMultipleTypes()
    {
        _value = new T();
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(T value)
    {
        _value = value;
    }
}

var genericWithMultipleTypes = new GenericWithMultipleTypes<MyClass>();
Console.WriteLine($"Initial Value: {genericWithMultipleTypes.GetValue().Name}"); // Output: Initial Value: Default

var newMyClass = new MyClass { Name = "Updated" };
genericWithMultipleTypes.SetValue(newMyClass);
Console.WriteLine($"New Value: {genericWithMultipleTypes.GetValue().Name}"); // Output: New Value: Updated

Github Project Link

Conclusion

Generic constraints in C# are powerful tools that allow you to enforce certain conditions on the types used with your generic classes, methods, and interfaces. By using constraints like struct, class, new(), and others, you can ensure that your generic code is both flexible and type-safe, allowing for more robust and maintainable applications.


Similar Articles