Introduction
Generic types in C# allow developers to write reusable and type-safe code that can work with different data types. However, there are situations where a generic class or method should only accept specific types.
For example, a generic method may need to create an object, access members of a particular base class, or call methods defined by an interface. Without restrictions, the compiler cannot guarantee that the supplied type supports those operations.
C# provides generic constraints to solve this problem. Generic constraints allow developers to specify requirements that a type argument must satisfy before it can be used with a generic class, method, or interface.
In this article, we will explore the commonly used generic constraints in C# and build practical examples using struct, class, new(), base-class, and interface constraints.
What Are Generic Constraints in C#?
A generic constraint is a rule applied to a type parameter that restricts which types can be used with a generic class or method.
Consider a simple generic class:
public class Repository<T>
{
public void Add(T item)
{
Console.WriteLine($"Added: {item}");
}
}
Without a constraint, T can represent many different types.
We can restrict T by using the where keyword:
public class Repository<T> where T : class
{
public void Add(T item)
{
Console.WriteLine($"Added: {item}");
}
}
Now, T must be a reference type.
The compiler enforces this restriction when the generic type is used.
Why Use Generic Constraints?
Generic constraints provide several benefits:
Improve compile-time type safety.
Restrict generic code to supported types.
Allow specific members to be accessed safely.
Make generic APIs easier to understand.
Prevent invalid type arguments.
Reduce unnecessary runtime type checking.
For example, if a generic method requires a type implementing IEmployee, adding an interface constraint guarantees that the required interface members are available inside the method.
Common Generic Constraints
C# provides several generic constraints.
Constraint | Purpose |
|---|
where T : struct
| Restricts T to non-nullable value types |
where T : class
| Restricts T to reference types |
where T : new()
| Requires a public parameterless constructor |
where T : BaseClass
| Restricts T to a base class or derived type |
where T : IInterface
| Restricts T to types implementing an interface |
Let's look at each one with a practical example.
Using the struct Constraint
The struct constraint restricts the generic type parameter to a non-nullable value type.
Step 1: Create a Generic Method
public static void PrintValue<T>(T value) where T : struct
{
Console.WriteLine($"Value: {value}");
}
The where T : struct constraint means that T must be a non-nullable value type.
Step 2: Use the Method
PrintValue(100);
PrintValue(25.5);
Both int and double are value types, so the calls are valid.
Output
Value: 100
Value: 25.5
Trying to use a reference type will produce a compile-time error:
PrintValue("Hello");
The compiler rejects this because string does not satisfy the struct constraint.
Using the class Constraint
The class constraint restricts the type parameter to a reference type.
Step 1: Create a Generic Method
public static void PrintObject<T>(T value) where T : class
{
Console.WriteLine($"Object: {value}");
}
Step 2: Pass a Reference Type
string message = "Hello C#";
PrintObject(message);
Since string is a reference type, it satisfies the constraint.
Output
Object: Hello C#
A value type such as int cannot be used with this method:
PrintObject(100);
This results in a compile-time error because int is a value type.
Using the new() Constraint
The new() constraint requires the type argument to have a public parameterless constructor.
This is useful when generic code needs to create an instance of T.
Step 1: Create a Class
public class Employee
{
public string Name { get; set; } = string.Empty;
}
Step 2: Create a Generic Method
public static T CreateInstance<T>() where T : new()
{
return new T();
}
Because of the new() constraint, the compiler knows that T has a public parameterless constructor.
Step 3: Create an Employee
Employee employee = CreateInstance<Employee>();
employee.Name = "John";
Console.WriteLine(employee.Name);
Output
John
Without the new() constraint, the compiler would not allow the generic code to safely use new T().
Using a Base Class Constraint
A generic type can be restricted to a specific base class.
Step 1: Create a Base Class
public class Employee
{
public string Name { get; set; } = string.Empty;
public void DisplayName()
{
Console.WriteLine($"Employee: {Name}");
}
}
Step 2: Create a Derived Class
public class Developer : Employee
{
public string ProgrammingLanguage { get; set; } = string.Empty;
}
Step 3: Add the Generic Constraint
public static void DisplayEmployee<T>(T employee)
where T : Employee
{
employee.DisplayName();
}
The constraint guarantees that T is Employee or derives from Employee.
Therefore, the DisplayName() method is available inside the generic method.
Step 4: Call the Generic Method
var developer = new Developer
{
Name = "John",
ProgrammingLanguage = "C#"
};
DisplayEmployee(developer);
Output
Employee: John
The same method can accept Employee or any class derived from it.
Using an Interface Constraint
An interface constraint restricts the generic type to types that implement a particular interface.
Step 1: Create an Interface
public interface IPrintable
{
void Print();
}
Step 2: Implement the Interface
public class Employee : IPrintable
{
public string Name { get; set; } = string.Empty;
public void Print()
{
Console.WriteLine($"Employee: {Name}");
}
}
Step 3: Add the Interface Constraint
public static void PrintItem<T>(T item)
where T : IPrintable
{
item.Print();
}
Because T is constrained to IPrintable, the generic method can safely call Print().
Step 4: Use the Generic Method
var employee = new Employee
{
Name = "Sarah"
};
PrintItem(employee);
Output
Employee: Sarah
If a class does not implement IPrintable, it cannot be passed to this method.
Combining Multiple Constraints
C# also allows multiple constraints to be applied to a single type parameter.
For example:
public static T CreateEmployee<T>()
where T : Employee, IPrintable, new()
{
return new T();
}
This requires T to:
A type argument must satisfy all specified constraints.
Complete Example
The following example brings several concepts together:
using System;
public interface IPrintable
{
void Print();
}
public class Employee : IPrintable
{
public string Name { get; set; } = string.Empty;
public void Print()
{
Console.WriteLine($"Employee: {Name}");
}
}
public class Developer : Employee
{
public string ProgrammingLanguage { get; set; } = string.Empty;
}
public class GenericHelper
{
public static T CreateInstance<T>() where T : new()
{
return new T();
}
public static void DisplayEmployee<T>(T employee)
where T : Employee
{
employee.Print();
}
public static void PrintValue<T>(T value)
where T : struct
{
Console.WriteLine($"Value: {value}");
}
}
class Program
{
static void Main()
{
var developer = GenericHelper.CreateInstance<Developer>();
developer.Name = "John";
developer.ProgrammingLanguage = "C#";
GenericHelper.DisplayEmployee(developer);
GenericHelper.PrintValue(100);
GenericHelper.PrintValue(25.5);
}
}
Output
When the application is executed, the output is:
Employee: John
Value: 100
Value: 25.5
The example demonstrates how constraints allow the generic methods to work only with types that satisfy their requirements.
Generic Constraints and Compile-Time Safety
One of the biggest advantages of generic constraints is that invalid type arguments can be detected during compilation.
For example:
GenericHelper.PrintValue("Hello");
This is invalid because the PrintValue method requires:
where T : struct
Similarly, a method constrained to Employee cannot accept an unrelated class.
This means developers can discover many type-related errors before the application runs.
Generic Constraints vs Runtime Type Checking
Without generic constraints, developers may be tempted to use runtime checks:
if (value is Employee employee)
{
employee.Print();
}
A generic constraint provides a stronger contract:
public static void DisplayEmployee<T>(T employee)
where T : Employee
{
employee.Print();
}
The second approach communicates the requirement directly through the method signature and allows the compiler to enforce it.
Practical Guidelines
When working with generic constraints, consider the following practices:
Add constraints when generic code genuinely depends on specific capabilities.
Prefer compile-time constraints over unnecessary runtime type checks.
Use interface constraints when behavior is more important than a specific class hierarchy.
Use new() only when the generic code actually needs to create instances.
Keep generic APIs as simple as possible.
Combine constraints only when each constraint is required by the implementation.
What About Constraint-Based Prompting in AI?
The term "constraint-based prompting" is also used in AI prompt engineering. In that context, it refers to specifying boundaries for an AI system's response, such as its format, length, audience, scope, or available resources.
For example:
Explain this C# method in 100 words or fewer.
Include the method's purpose, parameters, return value,
and two possible edge cases.
These constraints control the expected format and scope of an AI-generated response.
This concept is different from generic constraints in C#. Generic constraints are compiler-enforced rules for type parameters, while prompt constraints are instructions provided to an AI system.
Conclusion
Generic constraints are an important feature of C# generics. They allow developers to restrict the types that can be used with generic classes and methods while improving compile-time type safety.
In this article, we explored the commonly used constraints:
where T : struct
where T : class
where T : new()
where T : BaseClass
where T : IInterface
We also saw how multiple constraints can be combined and how they allow generic code to safely access required members.
By choosing appropriate generic constraints, developers can create reusable APIs that remain flexible while clearly defining the capabilities required from their type parameters.
Join the conversation! Your thoughts help the community grow.