Unbounded vs Bounded Generics in C
In C#, generic programming provides type-safe code reusability through type parameters, which can be defined as either unbounded or bounded. Unbounded generics place no restrictions on the underlying type argument, allowing any type to be substituted, whereas bounded generics use constraints to limit arguments to specific types, interfaces, or capabilities. This article explains the definitions, syntax, operational limits, and best-use scenarios for both unbounded and bounded generic types in C#.
What Are Unbounded Generic Types?
An unbounded generic type is a generic class, interface, method, or struct declared without any constraints on its type parameters. Because the compiler has no advance knowledge of what type will be supplied, it treats the type parameter as potentially being any valid C# type—whether it is a reference type, a non-nullable value type, or a nullable value type.
public class Storage<T>
{
private T _data;
public void SetData(T item)
{
_data = item;
}
public T GetData()
{
return _data;
}
}Characteristics of Unbounded Generics
- Broadest Compatibility: Any type (e.g.,
int,string, custom objects) can be passed as the type argument. - Limited Operations: Inside the generic definition,
you can only invoke methods inherited from
System.Object, such asToString(),Equals(), andGetHashCode(). - Equality Checks: Direct comparison operators
(
==or!=) generally cannot be used unless casting toobjector usingEqualityComparer<T>.Default, because value types might not implement reference equality. - Default Values: Assigning
nulldirectly is not always valid ifTis a value type. Instead, thedefaultliteral must be used (e.g.,T item = default;).
What Are Bounded Generic Types?
A bounded generic type limits the types of arguments that can be
assigned to a generic type parameter by using the where
keyword. These constraints instruct the compiler about the capabilities
the supplied type is guaranteed to have, thereby unlocking additional
language features and member access inside the generic
implementation.
public class EntityRepository<T> where T : class, IComparable<T>, new()
{
public T CreateAndCompare(T other)
{
T instance = new T();
if (instance.CompareTo(other) > 0)
{
return instance;
}
return other;
}
}Common Constraint Types
- Type Constraints: Restricts to a specific class or
interface (
where T : IDisposableorwhere T : Stream). - Reference/Value Constraints: Restricts to reference
types (
where T : class), value types (where T : struct), or unmanaged types (where T : unmanaged). - Constructor Constraints: Requires the type to
expose a public, parameterless constructor
(
where T : new()). - Nullability Constraints: Explicitly marks the
parameter as non-nullable (
where T : notnull).
Characteristics of Bounded Generics
- Increased Functionality: Inside the class or method, you can directly access properties, methods, and operators defined by the constraints.
- Compile-Time Validation: Attempting to instantiate the generic type with an invalid argument results in a compile-time error.
- Instantiability: The
new()constraint allows using thenew T()expression directly within the method body.
Key Differences
| Feature | Unbounded Generics | Bounded Generics |
|---|---|---|
| Declaration Syntax | class Example<T> |
class Example<T> where T : Constraint |
| Accepted Types | Any valid C# type | Only types that satisfy the declared constraints |
| Available Members | Only methods from
System.Object |
Members from System.Object
plus all constrained interfaces and base classes |
Instantiation
(new T()) |
Not permitted | Permitted when using the
where T : new() constraint |
| Comparison Operators | == and != are
generally prohibited |
Permitted if constrained to types
supporting operators or via interfaces like
IComparable<T> |
| Null Handling | T may be a reference type or
value type; requires default |
Determined by class,
struct, or notnull constraints |
When to Use Each
Use unbounded generics when building generic collections, wrappers, or data structures that function as containers without needing to interact with the underlying data's internal API. Examples include data storage classes, queues, stacks, or agnostic event dispatchers.
Use bounded generics when your logic relies on
specific functionality of the type parameter. This includes mathematical
algorithms that require numeric interfaces, repositories requiring
database entity base classes, serialization engines needing
parameterless constructors, or search algorithms relying on
IComparable<T>.