Generic Class & Constraints in C#

Generics are one of the most powerful tools in C#, enabling reusable, type-safe, and flexible code. But with great power comes great responsibility, and that’s where constraints on generics come into play. In this article, we’ll dive deep into gene…


This content originally appeared on DEV Community and was authored by Mo

Generics are one of the most powerful tools in C#, enabling reusable, type-safe, and flexible code. But with great power comes great responsibility, and that’s where constraints on generics come into play. In this article, we’ll dive deep into generic constraints in C#, starting from the basics and progressing to advanced use cases.

Why Do We Need Generics?

Let’s first understand why generics exist. Imagine you’re writing a class to process integers and strings. Without generics, you’d need to write separate methods or overloads for each type:

public class Processor
{
    public void Process(int number)
    {
        Console.WriteLine($"Processing integer: {number}");
    }

    public void Process(string text)
    {
        Console.WriteLine($"Processing string: {text}");
    }
}

While this works, it quickly becomes unmanageable if you need to handle multiple types like float, bool, or custom objects. The code becomes verbose, error-prone, and harder to maintain.

Enter Generics

Generics solves this problem by allowing you to write a single class or method that works with any type. Here’s how the same Processor class looks with generics:

public class Processor<T>
{
    public void Process(T item)
    {
        Console.WriteLine($"Processing: {item}");
    }
}

// Usage:
var intProcessor = new Processor<int>();
intProcessor.Process(42);

var stringProcessor = new Processor<string>();
stringProcessor.Process("Hello, world!");

With generics, the Processor class works with any type, eliminating redundancy and ensuring type safety.

Class-Level vs Method-Level Generics

When working with generics, you can define them at the class level or method level.

  • Class-Level Generics: Use when the type parameter applies to the entire class.
  • Method-Level Generics: Use when you need type flexibility for only a specific method.

Here’s an example of both:

// Class-level generic
public class Storage<T>
{
    private List<T> _items = new List<T>();

    public void Add(T item)
    {
        _items.Add(item);
        Console.WriteLine($"{item} added to storage.");
    }
}

// Method-level generic
public class Utility
{
    public void Print<T>(T item)
    {
        Console.WriteLine($"Printing: {item}");
    }
}

// Usage:
var storage = new Storage<string>();
storage.Add("Hello, storage!");

var utility = new Utility();
utility.Print(42);

Understanding Generic Constraints

While generics are flexible, sometimes you need to restrict what types can be used. Constraints allow you to enforce specific requirements, such as the type being a value type, a reference type, or implementing an interface.

Let’s explore the various constraints and their usage.

1. Value Type Constraint (struct)

The struct constraint ensures that the type parameter is a value type, such as int, float, or a custom struct.

public class ValueProcessor<T> where T : struct
{
    public void ProcessValue(T value)
    {
        Console.WriteLine($"Processing value: {value}");
    }
}

// Usage:
var intProcessor = new ValueProcessor<int>();
intProcessor.ProcessValue(100);

var floatProcessor = new ValueProcessor<float>();
floatProcessor.ProcessValue(99.99f);

This is perfect for scenarios where you’re working with numeric types, enums, or other non-nullable structures.

2. Reference Type Constraint (class)

The class constraint ensures that the type parameter is a reference type, such as string or a custom object.

public class ReferenceProcessor<T> where T : class
{
    public void ProcessReference(T reference)
    {
        Console.WriteLine($"Processing reference: {reference}");
    }
}

// Usage:
var stringProcessor = new ReferenceProcessor<string>();
stringProcessor.ProcessReference("Hello, Reference!");

var objectProcessor = new ReferenceProcessor<object>();
objectProcessor.ProcessReference(new { Name = "C#", Version = 12 });

This is ideal for working with objects that support nullability.

3. Nullable Types

If you want to allow nullable value types, you can use a nullable type in your constraint.

public class NullableProcessor<T> where T : struct
{
    public void ProcessNullable(T? value)
    {
        Console.WriteLine($"Processing nullable value: {value}");
    }
}

// Usage:
var nullableIntProcessor = new NullableProcessor<int>();
nullableIntProcessor.ProcessNullable(null);
nullableIntProcessor.ProcessNullable(42);

This is great for handling scenarios like optional parameters or database fields that may contain null values.

4. Constructor Constraint (new())

The new() constraint ensures that the type parameter has a parameterless constructor, allowing you to create new instances.

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

// Usage:
var creator = new InstanceCreator<StringBuilder>();
var instance = creator.CreateInstance();
instance.Append("Hello from InstanceCreator!");
Console.WriteLine(instance.ToString());

This is especially useful when you need to instantiate objects dynamically.

5. Interface and Multiple Constraints

You can combine multiple constraints to enforce stricter rules.

public interface IEntity
{
    int Id { get; }
}

public class Repository<T> where T : class, IEntity, new()
{
    private readonly List<T> _items = new List<T>();

    public void Add(T item)
    {
        _items.Add(item);
        Console.WriteLine($"Added entity with ID: {item.Id}");
    }
}

// Usage:
public class Product: IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

var repository = new Repository<Product>();
repository.Add(new Product { Id = 1, Name = "Laptop" });

Here, T must:

  1. Be a reference type (class).
  2. Implement the IEntity interface.
  3. Have a parameterless constructor (new()).

This combination provides structure and consistency for your types.

Conclusion

Generics and their constraints are indispensable tools for writing reusable, type-safe, and maintainable code in C#. Whether you’re enforcing type safety or structuring your code better, constraints help you achieve flexibility without sacrificing reliability.

Take some time to experiment with these constraints in your projects, and see how they can simplify your codebase!

What’s your favourite use case for generics? Let me know in the comments!


This content originally appeared on DEV Community and was authored by Mo


Print Share Comment Cite Upload Translate Updates
APA

Mo | Sciencx (2025-01-05T13:36:58+00:00) Generic Class & Constraints in C#. Retrieved from https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/

MLA
" » Generic Class & Constraints in C#." Mo | Sciencx - Sunday January 5, 2025, https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/
HARVARD
Mo | Sciencx Sunday January 5, 2025 » Generic Class & Constraints in C#., viewed ,<https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/>
VANCOUVER
Mo | Sciencx - » Generic Class & Constraints in C#. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/
CHICAGO
" » Generic Class & Constraints in C#." Mo | Sciencx - Accessed . https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/
IEEE
" » Generic Class & Constraints in C#." Mo | Sciencx [Online]. Available: https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/. [Accessed: ]
rf:citation
» Generic Class & Constraints in C# | Mo | Sciencx | https://www.scien.cx/2025/01/05/generic-class-constraints-in-c/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.