Reading — step 1 of 7
Learn
C# generics are reified — type information is preserved at runtime, unlike Java's erased generics. Combined with constraints and variance, they let you write reusable, type-safe APIs.
Type constraints with where
public static T Max<T>(IEnumerable<T> items) where T : IComparable<T>
{
T max = items.First();
foreach (var item in items.Skip(1))
if (item.CompareTo(max) > 0) max = item;
return max;
}
where T : IComparable<T> constrains T to types that implement IComparable<T> — gives you .CompareTo() inside the function.
Available constraints:
where T : class // T is a reference type
where T : struct // T is a value type (non-nullable)
where T : new() // T has a parameterless constructor
where T : SomeBaseClass // T is or derives from SomeBaseClass
where T : ISomeInterface // T implements ISomeInterface
where T : U // T is or derives from another type parameter U
where T : notnull // T is non-nullable (C# 8+)
where T : unmanaged // T is a non-pointer, non-reference type
where T : default // (for distinguishing notnull/class in disambiguation)
Multiple constraints
public class Cache<T> where T : class, ICacheable, new()
{
public T Create() => new T();
public bool IsValid(T item) => item.Hash != 0;
}
List them comma-separated. new() must be last. Class/struct must be first.
Variance: out and in
C# generics are invariant by default. You can declare variance on INTERFACES and DELEGATES:
public interface IProducer<out T>
{
T Produce(); // produces T — covariant
}
public interface IConsumer<in T>
{
void Consume(T item); // consumes T — contravariant
}
out T = T appears only in OUTPUT positions (return types). Subtyping flows the same way:
IProducer<string> stringProducer = ...;
IProducer<object> objectProducer = stringProducer; // OK — covariant
in T = T appears only in INPUT positions (parameters). Subtyping reverses:
IConsumer<object> objectConsumer = ...;
IConsumer<string> stringConsumer = objectConsumer; // OK — contravariant
IEnumerable<out T> is covariant — that's why you can pass IEnumerable<string> where IEnumerable<object> is expected. Same with Func<in T, out R> (contravariant in T, covariant in R).
Generics on classes vs methods
// Class generic — applies to whole class
public class Stack<T>
{
private List<T> items = new();
public void Push(T item) => items.Add(item);
public T Pop() { ... }
}
// Method generic — applies to one method
public class Helper
{
public static T First<T>(IEnumerable<T> items) where T : class
=> items.FirstOrDefault();
}
Classes parameterize all members. Method generics are scoped to that method.
Reified generics — runtime type info
Unlike Java, C# preserves T at runtime:
public static string Describe<T>(T value)
{
return $"{typeof(T).Name}: {value}";
}
Describe(42); // "Int32: 42"
Describe("hello"); // "String: hello"
typeof(T) works inside generic methods. Each instantiation gets its own type tokens.
Common mistakes
- Mixing variance with concrete types — variance only on interfaces/delegates.
class List<out T>is a compile error. - Using
out/inwhere T appears in both — compile error. Variance is structural; T must only appear in the matching position. - Forgetting
new()constraint — without it,new T()won't compile. - Confusing covariance with subclass relationships —
List<Cat>is NOT aList<Animal>. Use IEnumerable (covariant) for that pattern. - Excessive constraints — sometimes only one method needs the constraint. Move it to that method.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…