Generics
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 (<>
).
Constraints
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.
No Constraints
If you don't specify any constraints, the type parameter can be any type.
public class GenericClass<T>
{
public T Value { get; set; }
}
Value Type
public class ValueTypeOnly<T> where T : struct
{
public T Value { get; set; }
}
Reference Type
public class ReferenceTypeOnly<T> where T : class
{
public T Value { get; set; }
}
Parameterless Constructor
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
}
}
Base Class or Implemented interface
public class BaseClass { }
public class DerivedClass : BaseClass { }
public class DerivedTypeOnly<T> where T : BaseClass
{
public T Value { get; set; }
}
Multiple Constraints
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
}
}
Enum
public class EnumConstraint<T> where T : struct, Enum
{
public void PrintEnumValues()
{
foreach (var value in Enum.GetValues(typeof(T)))
{
Console.WriteLine(value);
}
}
}
Delegate
public class DelegateConstraint<T> where T : Delegate
{
public void InvokeDelegate(T del)
{
del.DynamicInvoke(); // Invoke delegate dynamically
}
}
notnull
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
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
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
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>();