πŸ“„

Generics Constraints

Beginner 5 min read 900 words

C# Generics and Constraints

Introduction

Generics enable you to write type-safe, reusable code without sacrificing performance. They eliminate the need for boxing/unboxing and provide compile-time type checking, making your code more robust and maintainable.


Table of Contents


Generic Basics

Why Generics?

Before generics (C# 1.0), collections used object type:

// Pre-generics approach - ArrayList
ArrayList list = new ArrayList();
list.Add(42);           // Boxing: int β†’ object
list.Add("string");     // No type safety!
int value = (int)list[0]; // Unboxing + casting required

// Generic approach - List<T>
List<int> numbers = new List<int>();
numbers.Add(42);        // No boxing
// numbers.Add("string"); // Compile error! Type safety
int value = numbers[0]; // No casting needed

Benefits of Generics

Benefit Description
Type Safety Compile-time type checking prevents runtime errors
Performance No boxing/unboxing for value types
Code Reuse Write once, use with any type
IntelliSense Full IDE support with type information

Type Parameters

Naming Conventions

Parameter Usage Example
T Single type parameter List<T>, Task<T>
TKey, TValue Dictionary-like structures Dictionary<TKey, TValue>
TInput, TOutput Transformation operations Func<TInput, TOutput>
TSource, TResult LINQ operations Select<TSource, TResult>
TEntity Entity/domain objects Repository<TEntity>
TException Exception types Catch<TException>

Generic Classes

// Simple generic class
public class Box<T>
{
    private T _value;

    public T Value
    {
        get => _value;
        set => _value = value;
    }

    public bool HasValue => _value != null;
}

// Usage
var intBox = new Box<int> { Value = 42 };
var stringBox = new Box<string> { Value = "Hello" };

// Multiple type parameters
public class Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }

    public void Deconstruct(out TFirst first, out TSecond second)
    {
        first = First;
        second = Second;
    }
}

// Usage with deconstruction
var pair = new Pair<string, int> { First = "Age", Second = 30 };
var (name, value) = pair;

Generic Interfaces

// Generic interface
public interface IRepository<TEntity> where TEntity : class
{
    TEntity GetById(int id);
    IEnumerable<TEntity> GetAll();
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

// Implementation
public class Repository<TEntity> : IRepository<TEntity>
    where TEntity : class, new()
{
    private readonly DbContext _context;
    private readonly DbSet<TEntity> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<TEntity>();
    }

    public TEntity GetById(int id) => _dbSet.Find(id);
    public IEnumerable<TEntity> GetAll() => _dbSet.ToList();
    public void Add(TEntity entity) => _dbSet.Add(entity);
    public void Update(TEntity entity) => _dbSet.Update(entity);
    public void Delete(TEntity entity) => _dbSet.Remove(entity);
}

Generic Constraints

Constraints restrict which types can be used as type arguments, enabling access to specific members.

Constraint Types

// 1. Reference type constraint
public class Cache<T> where T : class
{
    // T is guaranteed to be a reference type
    // Can use null checks, reference equality
    public T GetOrDefault() => default; // Returns null
}

// 2. Value type constraint
public class ValueWrapper<T> where T : struct
{
    // T is guaranteed to be a non-nullable value type
    public T Value { get; set; }
    public T? NullableValue { get; set; } // Can make it nullable
}

// 3. Constructor constraint
public class Factory<T> where T : new()
{
    // T must have a parameterless constructor
    public T Create() => new T();
}

// 4. Base class constraint
public class AnimalShelter<T> where T : Animal
{
    public void Feed(T animal) => animal.Eat(); // Can access Animal members
}

// 5. Interface constraint
public class Sorter<T> where T : IComparable<T>
{
    public T FindMax(T a, T b) => a.CompareTo(b) > 0 ? a : b;
}

// 6. Multiple constraints
public class Repository<TEntity>
    where TEntity : class, IEntity, new()
{
    public TEntity CreateNew()
    {
        var entity = new TEntity();
        entity.Id = Guid.NewGuid();
        return entity;
    }
}

// 7. notnull constraint (C# 8.0+)
public class NonNullCache<T> where T : notnull
{
    private readonly Dictionary<string, T> _cache = new();

    public void Set(string key, T value) => _cache[key] = value;
}

// 8. unmanaged constraint (C# 7.3+)
public unsafe class NativeBuffer<T> where T : unmanaged
{
    // T can be used with pointers
    // Valid: int, double, struct with only unmanaged fields
    public T* Allocate(int count)
    {
        return (T*)Marshal.AllocHGlobal(sizeof(T) * count);
    }
}

Constraint Reference Table

Constraint Syntax Allows
Reference type where T : class Classes, interfaces, delegates, arrays
Non-nullable reference where T : class? Same as class but nullable aware
Value type where T : struct Structs, enums (not Nullable)
Not null where T : notnull Any non-nullable type
Unmanaged where T : unmanaged Unmanaged types (blittable)
Constructor where T : new() Types with parameterless constructor
Base class where T : BaseClass BaseClass or derived types
Interface where T : IInterface Types implementing IInterface
Type parameter where T : U T must be or derive from U

Constraint Order

When combining constraints, they must appear in specific order:

// Correct order: class/struct β†’ base β†’ interfaces β†’ new()
public class Example<T>
    where T : class,      // 1. Primary constraint (class or struct)
              BaseClass,  // 2. Base class constraint
              IDisposable,// 3. Interface constraints
              new()       // 4. Constructor constraint (must be last)
{
}

// Multiple type parameters with different constraints
public class MultiConstrained<TKey, TValue>
    where TKey : class, IComparable<TKey>
    where TValue : struct
{
}

Covariance and Contravariance

Variance describes how generic types relate to each other based on their type arguments’ inheritance hierarchy.

Visual Explanation

Inheritance:     Animal ← Dog

Covariance (out):     IEnumerable<Dog> β†’ IEnumerable<Animal>  βœ“
                      (Can use more derived as less derived)

Contravariance (in):  Action<Animal> β†’ Action<Dog>  βœ“
                      (Can use less derived as more derived)

Invariance:           List<Dog> ↛ List<Animal>  βœ—
                      (No conversion allowed)

Covariance (out keyword)

Covariance allows a generic type to be substituted with a more derived type argument.

// Covariant interface - T only appears in output positions
public interface IProducer<out T>
{
    T Produce();
    // void Consume(T item); // ERROR: T cannot be input
}

// Example: IEnumerable<T> is covariant
public class AnimalShelter
{
    public void ProcessAnimals(IEnumerable<Animal> animals)
    {
        foreach (var animal in animals)
            animal.Eat();
    }
}

// Usage
List<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
var shelter = new AnimalShelter();

// Works because IEnumerable<Dog> β†’ IEnumerable<Animal>
shelter.ProcessAnimals(dogs);

// Covariant delegate
public delegate TResult CovariantFunc<out TResult>();

CovariantFunc<Dog> getDog = () => new Dog();
CovariantFunc<Animal> getAnimal = getDog; // Valid assignment

Contravariance (in keyword)

Contravariance allows a generic type to be substituted with a less derived type argument.

// Contravariant interface - T only appears in input positions
public interface IConsumer<in T>
{
    void Consume(T item);
    // T Produce(); // ERROR: T cannot be output
}

// Example: Action<T> is contravariant
public class DogTrainer
{
    public void TrainDogs(Action<Dog> trainer, List<Dog> dogs)
    {
        foreach (var dog in dogs)
            trainer(dog);
    }
}

// Usage
Action<Animal> feedAnimal = animal => animal.Eat();
var trainer = new DogTrainer();
var dogs = new List<Dog> { new Dog(), new Dog() };

// Works because Action<Animal> β†’ Action<Dog>
trainer.TrainDogs(feedAnimal, dogs);

// Contravariant comparer
public interface IComparer<in T>
{
    int Compare(T x, T y);
}

IComparer<Animal> animalComparer = new AnimalComparer();
IComparer<Dog> dogComparer = animalComparer; // Valid assignment

Combining Variance

// Both covariance and contravariance
public interface IConverter<in TInput, out TOutput>
{
    TOutput Convert(TInput input);
}

// Implementation
public class AnimalToStringConverter : IConverter<Animal, string>
{
    public string Convert(Animal input) => input.Name;
}

// Usage with variance
IConverter<Animal, string> animalConverter = new AnimalToStringConverter();
IConverter<Dog, object> dogToObjectConverter = animalConverter;
// Works: Dog is more specific (contravariant input)
//        object is less specific (covariant output)

Built-in Variant Interfaces

Interface Variance Reason
IEnumerable<out T> Covariant Only produces T
IReadOnlyList<out T> Covariant Only produces T
IReadOnlyCollection<out T> Covariant Only produces T
IComparer<in T> Contravariant Only consumes T
IComparable<in T> Contravariant Only consumes T
IEqualityComparer<in T> Contravariant Only consumes T
Func<out TResult> Covariant Returns TResult
Func<in T, out TResult> Both Consumes T, produces TResult
Action<in T> Contravariant Only consumes T

Generic Methods

Basic Generic Methods

public class Utilities
{
    // Simple generic method
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }

    // Generic method with constraints
    public static T Max<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }

    // Multiple type parameters
    public static TOutput Transform<TInput, TOutput>(
        TInput input,
        Func<TInput, TOutput> transformer)
    {
        return transformer(input);
    }
}

// Usage
int x = 5, y = 10;
Utilities.Swap(ref x, ref y);           // Type inference: T = int
int max = Utilities.Max(10, 20);        // Type inference: T = int
string result = Utilities.Transform(42, n => n.ToString()); // Explicit types not needed

Generic Method Overloading

public class Parser
{
    // Non-generic version
    public object Parse(string input) => input;

    // Generic version
    public T Parse<T>(string input) where T : IParsable<T>
    {
        return T.Parse(input, null);
    }

    // Constrained generic version
    public T Parse<T>(string input, T defaultValue) where T : struct
    {
        if (string.IsNullOrEmpty(input))
            return defaultValue;

        return (T)Convert.ChangeType(input, typeof(T));
    }
}

// Method resolution
var parser = new Parser();
object obj = parser.Parse("hello");      // Non-generic
int num = parser.Parse<int>("42");       // Generic with IParsable
int withDefault = parser.Parse("", 0);   // Generic with default

Extension Methods with Generics

public static class EnumerableExtensions
{
    // Generic extension method
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
        where T : class
    {
        return source.Where(item => item != null)!;
    }

    // Extension with multiple constraints
    public static TSource MinBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector)
        where TKey : IComparable<TKey>
    {
        return source.OrderBy(keySelector).First();
    }

    // Batch/chunk extension
    public static IEnumerable<IEnumerable<T>> Batch<T>(
        this IEnumerable<T> source,
        int batchSize)
    {
        var batch = new List<T>(batchSize);
        foreach (var item in source)
        {
            batch.Add(item);
            if (batch.Count == batchSize)
            {
                yield return batch;
                batch = new List<T>(batchSize);
            }
        }
        if (batch.Count > 0)
            yield return batch;
    }
}

// Usage
var names = new[] { "Alice", null, "Bob", null, "Charlie" };
var nonNull = names.WhereNotNull(); // ["Alice", "Bob", "Charlie"]

var users = GetUsers();
var youngest = users.MinBy(u => u.Age);

var numbers = Enumerable.Range(1, 10);
var batches = numbers.Batch(3); // [[1,2,3], [4,5,6], [7,8,9], [10]]

Performance Characteristics

Value Types: No Boxing

// Non-generic: causes boxing
ArrayList list = new ArrayList();
for (int i = 0; i < 1_000_000; i++)
{
    list.Add(i);  // Boxing: int β†’ object (heap allocation)
}

// Generic: no boxing
List<int> genericList = new List<int>();
for (int i = 0; i < 1_000_000; i++)
{
    genericList.Add(i);  // No boxing, direct storage
}

// Performance comparison (typical results):
// ArrayList:   ~50ms, ~16MB allocations
// List<int>:   ~5ms,  ~4MB allocations

JIT Specialization

The JIT compiler creates specialized code for each value type:

// Single IL code, but JIT creates multiple native versions:
public class Stack<T>
{
    private T[] _items;
    public void Push(T item) { /* ... */ }
}

// JIT generates separate native code for:
var intStack = new Stack<int>();      // Specialized for int
var doubleStack = new Stack<double>(); // Specialized for double
var stringStack = new Stack<string>(); // Shared reference type code
var objectStack = new Stack<object>(); // Shares code with string

Memory Layout

Value Type Generic (List<int>):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ List<int> object (heap)              β”‚
β”‚ β”œβ”€β”€ _items: int[] reference          β”‚
β”‚ └── _size: int                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ int[] array (heap)                   β”‚
β”‚ [0][1][2][3][4][5]  ← Values inline  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Reference Type Generic (List<string>):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ List<string> object (heap)           β”‚
β”‚ β”œβ”€β”€ _items: string[] reference       β”‚
β”‚ └── _size: int                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ string[] array (heap)                β”‚
β”‚ [ref][ref][ref]  ← References only   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚    β”‚    β”‚
    β–Ό    β–Ό    β–Ό
  "A"  "B"  "C"   ← Strings on heap

Benchmark Example

// Using BenchmarkDotNet
[MemoryDiagnoser]
public class GenericsBenchmark
{
    private const int N = 100_000;

    [Benchmark(Baseline = true)]
    public int ArrayList_Sum()
    {
        var list = new ArrayList();
        for (int i = 0; i < N; i++)
            list.Add(i);

        int sum = 0;
        foreach (object item in list)
            sum += (int)item;
        return sum;
    }

    [Benchmark]
    public int GenericList_Sum()
    {
        var list = new List<int>(N);
        for (int i = 0; i < N; i++)
            list.Add(i);

        int sum = 0;
        foreach (int item in list)
            sum += item;
        return sum;
    }
}

// Typical results:
// |           Method |      Mean | Allocated |
// |----------------- |----------:|----------:|
// |    ArrayList_Sum | 1,200 ΞΌs  |  2.4 MB   |
// | GenericList_Sum  |   180 ΞΌs  |  0.4 MB   |

Advanced Patterns

Generic Factory Pattern

public interface IFactory<out T>
{
    T Create();
}

public class ServiceFactory<T> : IFactory<T> where T : class, new()
{
    public T Create() => new T();
}

// With dependency injection
public class DIFactory<T> : IFactory<T> where T : class
{
    private readonly IServiceProvider _services;

    public DIFactory(IServiceProvider services)
    {
        _services = services;
    }

    public T Create() => _services.GetRequiredService<T>();
}

Generic Repository Pattern

public interface IRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
    where TKey : IEquatable<TKey>
{
    Task<TEntity?> GetByIdAsync(TKey id);
    Task<IEnumerable<TEntity>> GetAllAsync();
    Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate);
    Task AddAsync(TEntity entity);
    Task UpdateAsync(TEntity entity);
    Task DeleteAsync(TKey id);
}

public class Repository<TEntity, TKey> : IRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
    where TKey : IEquatable<TKey>
{
    protected readonly DbContext _context;
    protected readonly DbSet<TEntity> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<TEntity>();
    }

    public virtual async Task<TEntity?> GetByIdAsync(TKey id)
    {
        return await _dbSet.FindAsync(id);
    }

    public virtual async Task<IEnumerable<TEntity>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }

    public virtual async Task<IEnumerable<TEntity>> FindAsync(
        Expression<Func<TEntity, bool>> predicate)
    {
        return await _dbSet.Where(predicate).ToListAsync();
    }

    public virtual async Task AddAsync(TEntity entity)
    {
        await _dbSet.AddAsync(entity);
        await _context.SaveChangesAsync();
    }

    public virtual async Task UpdateAsync(TEntity entity)
    {
        _dbSet.Update(entity);
        await _context.SaveChangesAsync();
    }

    public virtual async Task DeleteAsync(TKey id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null)
        {
            _dbSet.Remove(entity);
            await _context.SaveChangesAsync();
        }
    }
}

Generic Specification Pattern

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
    Expression<Func<T, bool>> ToExpression();
}

public abstract class Specification<T> : ISpecification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();

    public bool IsSatisfiedBy(T entity)
    {
        return ToExpression().Compile()(entity);
    }

    public Specification<T> And(Specification<T> other)
    {
        return new AndSpecification<T>(this, other);
    }

    public Specification<T> Or(Specification<T> other)
    {
        return new OrSpecification<T>(this, other);
    }

    public Specification<T> Not()
    {
        return new NotSpecification<T>(this);
    }
}

// Usage
public class ActiveUserSpecification : Specification<User>
{
    public override Expression<Func<User, bool>> ToExpression()
    {
        return user => user.IsActive;
    }
}

public class PremiumUserSpecification : Specification<User>
{
    public override Expression<Func<User, bool>> ToExpression()
    {
        return user => user.SubscriptionLevel == "Premium";
    }
}

// Combining specifications
var activePremiumUsers = new ActiveUserSpecification()
    .And(new PremiumUserSpecification());

var users = await _repository.FindAsync(activePremiumUsers.ToExpression());

Static Abstract Interface Members (C# 11+)

// Define interface with static abstract member
public interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
    static abstract TSelf Parse(string s, IFormatProvider? provider);
    static abstract bool TryParse(string? s, IFormatProvider? provider, out TSelf result);
}

// Generic method using static abstract
public static T ParseOrDefault<T>(string? input, T defaultValue)
    where T : IParsable<T>
{
    if (string.IsNullOrWhiteSpace(input))
        return defaultValue;

    return T.TryParse(input, null, out var result) ? result : defaultValue;
}

// Usage
int number = ParseOrDefault("42", 0);           // 42
double value = ParseOrDefault("invalid", 1.0);  // 1.0
DateTime date = ParseOrDefault("2024-01-01", DateTime.MinValue);

Common Pitfalls

❌ Comparing Generics Without Constraints

// Won't compile without constraint
public T FindMax<T>(T a, T b)
{
    return a > b ? a : b;  // ERROR: Operator '>' cannot be applied
}

// βœ… Fix: Add appropriate constraint
public T FindMax<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

❌ Assuming Default Value

public class Cache<T>
{
    public T GetOrDefault()
    {
        return default;  // null for reference, 0/false for value types
    }
}

// βœ… Consider using nullable or explicit default
public T? GetOrDefault() => default;
public T GetOrDefault(T defaultValue) => _cache ?? defaultValue;

❌ Type Checking at Runtime

// Avoid runtime type checking
public void Process<T>(T item)
{
    if (item is string s)  // Bad: defeats purpose of generics
    {
        Console.WriteLine(s.ToUpper());
    }
}

// βœ… Use constraints or specialized overloads
public void Process<T>(T item) where T : IFormattable
{
    Console.WriteLine(item.ToString("G", null));
}

❌ Creating Instances Without Constraint

// Won't compile
public T CreateInstance<T>()
{
    return new T();  // ERROR: Cannot create instance of type parameter
}

// βœ… Fix: Add new() constraint
public T CreateInstance<T>() where T : new()
{
    return new T();
}

Best Practices

βœ… Use Meaningful Type Parameter Names

// Good: Clear intent
public interface IRepository<TEntity> where TEntity : class { }
public class Converter<TInput, TOutput> { }
public delegate TResult Selector<TSource, TResult>(TSource source);

// Avoid: Single letters when multiple parameters exist
public class C<T, U, V> { }  // What are these?

βœ… Prefer Interface Constraints Over Base Class

// Good: More flexible
public class Sorter<T> where T : IComparable<T> { }

// Less flexible: Limits to specific hierarchy
public class Sorter<T> where T : ComparableBase { }

βœ… Make Generic Types as Constrained as Needed

// Too restrictive
public void Process<T>(T item) where T : class, IDisposable, ICloneable, new() { }

// Just right: Only what you need
public void Process<T>(T item) where T : IDisposable { }

βœ… Consider Variance for Interfaces and Delegates

// Good: Enables flexible assignment
public interface IReadOnlyRepository<out TEntity> { }
public interface IWriter<in TData> { }

// Design for variance when appropriate
public delegate TResult Factory<out TResult>();
public delegate void Handler<in TEvent>(TEvent evt);

Interview Questions

1. What is the difference between generics and object type?

Answer: Generics provide compile-time type safety and avoid boxing/unboxing for value types. When using object, you lose type information, require casting, and incur performance penalties from boxing. Generics are resolved at compile time, while object operations are resolved at runtime.

// object: boxing, runtime errors possible
object box = 42;        // Boxing
int val = (int)box;     // Requires cast
box = "string";         // No compile error
val = (int)box;         // Runtime InvalidCastException!

// Generic: no boxing, compile-time safety
List<int> list = new();
list.Add(42);           // No boxing
int val = list[0];      // No cast
// list.Add("string"); // Compile error!

2. Explain covariance and contravariance with examples.

Answer:

  • Covariance (out): Allows using a more derived type than specified. Used when the type is only returned (output). Example: IEnumerable<Dog> can be used where IEnumerable<Animal> is expected.
  • Contravariance (in): Allows using a less derived type than specified. Used when the type is only consumed (input). Example: Action<Animal> can be used where Action<Dog> is expected.
// Covariance: IEnumerable<Dog> β†’ IEnumerable<Animal>
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;  // Valid

// Contravariance: Action<Animal> β†’ Action<Dog>
Action<Animal> feedAnimal = a => a.Eat();
Action<Dog> feedDog = feedAnimal;  // Valid

3. What constraints can you apply to generic type parameters?

Answer: C# supports several constraint types:

  • where T : class - Reference type
  • where T : struct - Non-nullable value type
  • where T : new() - Has parameterless constructor
  • where T : BaseClass - Inherits from specific class
  • where T : IInterface - Implements interface
  • where T : U - T derives from type parameter U
  • where T : notnull - Non-nullable type (C# 8+)
  • where T : unmanaged - Unmanaged type (C# 7.3+)

Constraints must appear in order: class/struct β†’ base β†’ interfaces β†’ new()


4. How do generics improve performance over ArrayList?

Answer: Generics eliminate boxing/unboxing for value types. With ArrayList, every value type is boxed (copied to heap as object) and unboxed when retrieved. List<T> stores value types directly in the backing array without boxing. This results in:

  • Less memory allocation (no box objects)
  • Fewer GC collections
  • Better cache locality
  • Faster operations (no boxing overhead)

Benchmarks typically show 5-10x performance improvement for value type collections.


5. Can you create a generic method inside a non-generic class?

Answer: Yes, methods can have their own type parameters independent of the class:

public class NonGenericClass
{
    public T ConvertTo<T>(object value)
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }

    public void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

6. What is the default keyword in generics?

Answer: default returns the default value for a type parameter:

  • Reference types: null
  • Numeric types: 0
  • bool: false
  • Structs: All fields set to default
  • Nullable types: null
public T GetDefault<T>() => default;  // C# 7.1+ simplified syntax
public T GetDefault<T>() => default(T);  // Explicit syntax

Sources