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 as ToString(), Equals(), and GetHashCode().
  • Equality Checks: Direct comparison operators (== or !=) generally cannot be used unless casting to object or using EqualityComparer<T>.Default, because value types might not implement reference equality.
  • Default Values: Assigning null directly is not always valid if T is a value type. Instead, the default literal 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 : IDisposable or where 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 the new 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>.