Generics in C# allow you to define classes, methods, interfaces, and delegates with type parameters, enabling type safety and code reuse without compromising performance.
Type parameters can be defined for types and methods using angle brackets (<>
).
Generic constraints in C# provide a way to restrict the types that can be used as arguments for a generic type parameter. Without constraints, generic type parameters can be any type (class, struct, enum, etc.). However, sometimes you want to limit the generic type to certain kinds of types (e.g., reference types, value types, types that implement a specific interface, etc.). Constraints are declared using the where
clause in the generic type or method declaration.
If you don't specify any constraints, the type parameter can be any type.
public class GenericClass<T>
{
public T Value { get; set; }
}
public class ValueTypeOnly<T> where T : struct
{
public T Value { get; set; }
}
public class ReferenceTypeOnly<T> where T : class
{
public T Value { get; set; }
}
The new()
constraint ensures that the type T
has a parameterless constructor. This is useful when you need to create instances of the type within the generic class or method.
public class InstantiateType<T> where T : new()
{
public T CreateInstance()
{
return new T(); // Requires T to have a parameterless constructor
}
}
public class BaseClass { }
public class DerivedClass : BaseClass { }
public class DerivedTypeOnly<T> where T : BaseClass
{
public T Value { get; set; }
}
public class MultiConstraint<T> where T : class, new()
{
public T CreateInstance()
{
return new T(); // T must be a reference type and have a parameterless constructor
}
}
public class EnumConstraint<T> where T : struct, Enum
{
public void PrintEnumValues()
{
foreach (var value in Enum.GetValues(typeof(T)))
{
Console.WriteLine(value);
}
}
}
public class DelegateConstraint<T> where T : Delegate
{
public void InvokeDelegate(T del)
{
del.DynamicInvoke(); // Invoke delegate dynamically
}
}
The notnull
constraint restricts the generic type parameter T
to types that are not nullable, meaning:
- It can be any value type
- It can be any non-nullable reference type
- It cannot be nullable types like int? (nullable value type) or string? (nullable reference type in C# 8.0 with nullable reference types enabled).
public class NotNullConstraint<T> where T : notnull
{
public T Value { get; set; }
public NotNullConstraint(T value)
{
Value = value ?? throw new ArgumentNullException(nameof(value), "Value cannot be null");
}
}
// Allowed: Non-nullable reference type (string)
NotNullConstraint<string> stringConstraint = new NotNullConstraint<string>("Hello");
// Allowed: Non-nullable reference type (string)
NotNullConstraint<string> stringConstraint = new NotNullConstraint<string>("Hello");
// Not allowed: Nullable reference type (string?) in C# 8.0ű
// compile error
NotNullConstraint<string?> nullableStringConstraint = new NotNullConstraint<string?>(null);
// Not allowed: Nullable value type (int?)
// compile error
NotNullConstraint<int?> nullableValueConstraint = new NotNullConstraint<int?>(null);
Covariance allows you to use a more derived type than originally specified. In C#, covariance applies to generic type parameters in interfaces and delegates when used for output (return types). You declare a type parameter as covariant using the out
keyword.
public interface ICovariant<out T>
{
T GetItem();
}
Here, T
is covariant because it is marked with the out
keyword, and it can only be used as a return type. The out
keyword means that if Dog
inherits from Animal
, then ICovariant<Dog>
can be treated as ICovariant<Animal>
:
ICovariant<Animal> animals = new CovariantImplementation<Dog>();
Animal animal = animals.GetItem();
Contravariance allows you to use a more general (or base) type than originally specified. In C#, contravariance applies to generic type parameters in interfaces and delegates when used for input (method parameters). You declare a type parameter as contravariant using the in
keyword.
public interface IContravariant<in T>
{
void SetItem(T item);
}
Here, T
is contravariant because it is marked with the in keyword, and it can only be used as a method parameter. The in
keyword means that if Dog
inherits from Animal
, then IContravariant<Animal>
can be treated as IContravariant<Dog>
.
IContravariant<Dog> dogs = new ContravariantImplementation<Animal>();
dogs.SetItem(new Dog());
Invariance means that there is no relationship between GenericType<Derived>
and GenericType<Base>
, even if Derived
inherits from Base
. Most generic types in C# are invariant by default. This means that List<Dog>
is not considered a subtype of List<Animal>
, even though Dog
is a subtype of Animal
.
//the following causes a compile error
List<Animal> animals = new List<Dog>();