17 questions foundCovariance ('out T') allows a generic interface with a type parameter used only in OUTPUT positions (return values) to be treated as its LESS specific base type automatically — e.g., IEnumerable<Dog> can be used where IEnumerable<Animal> is expected, since you can only READ Dogs out of it as Animals safely; contravariance ('in T') is the reverse, for type parameters used only in INPUT positions.
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings; // OK! covariant: IEnumerable<out T> allows this
// Contravariance example:
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // OK! contravariant: Action<in T> allows this
Real-world example Understanding why you can assign an IEnumerable<Dog> directly to an IEnumerable<Animal>-typed variable without any explicit conversion.
Common follow-ups: Why can't a MUTABLE generic interface like IList<T> be covariant the same way IEnumerable<T> is?
Interfaces & Abstract Classes
You can declare several type parameters on the same method/class, each with independent (or interdependent) constraints — a common pattern is a generic converter/mapper where the source and destination types both need to satisfy their own requirements, letting the compiler enforce correctness across the whole mapping operation.
public TDest Map<TSource, TDest>(TSource source, Func<TSource, TDest> mapper)
where TSource : class
where TDest : class, new() {
return mapper(source);
}
var userDto = Map<User, UserDto>(user, u => new UserDto { Name = u.Name });
Real-world example Building a generic, type-safe mapping utility between domain entities and DTOs with independently-constrained source and destination types.
Common follow-ups: How would you further constrain TDest to guarantee it has a specific interface method needed during mapping?
Interfaces & Abstract Classes
Unlike instance members, STATIC fields on a generic class are NOT shared across different closed generic instantiations — Box<int> and Box<string> each get their OWN, completely independent copy of any static field declared in Box<T>, since the CLR generates a distinct type for each unique combination of generic arguments (for reference types sharing code, but with separate static storage).
public class Counter<T> {
public static int InstanceCount = 0;
public Counter() { InstanceCount++; }
}
new Counter<int>(); new Counter<int>();
new Counter<string>();
Console.WriteLine(Counter<int>.InstanceCount); // 2
Console.WriteLine(Counter<string>.InstanceCount); // 1 -- separate, independent counter!
Real-world example Debugging a surprising bug where a 'shared' static counter on a generic class turned out to be tracking each closed generic type SEPARATELY, not globally.
Common follow-ups: Why does the CLR handle this differently for VALUE TYPE versus REFERENCE TYPE generic instantiations, in terms of code generation?
Memory & Garbage Collection
Constrain the pool's type parameter to an interface (like IPoolable) requiring a Reset() method, letting the generic pool safely call it when returning an object to the pool — this avoids unsafe casting or reflection while keeping the pool fully generic and reusable across any poolable type.
public interface IPoolable { void Reset(); }
public class ObjectPool<T> where T : class, IPoolable, new() {
private readonly Stack<T> _pool = new();
public T Rent() => _pool.Count > 0 ? _pool.Pop() : new T();
public void Return(T item) { item.Reset(); _pool.Push(item); }
}
Real-world example Building a reusable pool for expensive-to-create objects (like StringBuilder or a game entity) to reduce garbage collection pressure in a hot loop.
Common follow-ups: How does this hand-rolled pool compare to the built-in Microsoft.Extensions.ObjectPool library's ObjectPool<T>?
Memory & Garbage Collection
Prior to C# 11, an attribute class couldn't itself be generic (`public class MyAttribute<T> : Attribute` was disallowed), forcing awkward workarounds like passing a Type object as a constructor argument instead; C# 11 allows genuinely generic attribute classes, letting you specify the type argument directly and get compile-time type checking on the attribute's type parameter.
// C# 11+: genuinely generic attribute
public class ValidatorAttribute<T> : Attribute where T : IValidator { }
[Validator<EmailValidator>] // type-checked at compile time
public string Email { get; set; } = "";
// Older workaround before C# 11:
// [Validator(typeof(EmailValidator))] -- no compile-time type constraint enforcement
Real-world example Writing a strongly-typed validation attribute that references a specific validator TYPE with full compile-time type checking.
Common follow-ups: Why couldn't older C# express this same idea with full type safety before generic attributes were introduced?
Attributes & Reflection
Constrain the generic type parameter to INumber<T> (introduced in .NET 7 alongside static abstract interface members), which guarantees standard arithmetic operators (+, -, etc.) and static members like Zero are available — letting you write ONE generic numeric algorithm that works correctly across every built-in (and custom) numeric type without duplicating overloads for each.
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T> {
T total = T.Zero;
foreach (var value in values) total += value;
return total;
}
int intSum = Sum(new[] { 1, 2, 3 }); // 6
double doubleSum = Sum(new[] { 1.5, 2.5 }); // 4.0
Real-world example Writing a single, reusable Sum(), Average(), or Min()/Max() utility working generically across every numeric type without separate overloads.
Common follow-ups: What is a 'static abstract interface member,' and how does it make this specific generic math pattern possible in C# 11+?
Interfaces & Abstract Classes
Generics let you define a class, method, or interface with a placeholder type parameter (like <T>) that's specified when the type is actually used, avoiding the need to write near-identical code for every different data type -- beyond reducing duplication, generics provide genuine compile-time type safety (the compiler rejects an incompatible type at the call site) and better performance than the older, pre-generics approach of using object and relying on boxing/unboxing or unsafe casts.
// Without generics: separate overloads needed per type
public void Print(int value) => Console.WriteLine(value);
public void Print(string value) => Console.WriteLine(value);
// With generics: one method, any type, still fully type-checked
public void Print<T>(T value) => Console.WriteLine(value);
List<int> numbers = new(); // type-safe: only accepts int, unlike the old non-generic ArrayList
Real-world example A generic Repository<T> base class implements common CRUD operations (Add, GetById, Delete) once, reused across UserRepository, ProductRepository, and every other entity-specific repository in the application without duplicating the same logic per entity type.
Common follow-ups: How do generic constraints (where T : IComparable<T>) restrict what types can be used?;Why do generic collections like List<T> outperform their non-generic predecessors like ArrayList?
Generics;Collections Framework