In Part 1–3, we learned:
-
Basics of generics (methods, classes, constraints)
-
Advanced patterns (delegates, factories, strategies)
-
Real-world use cases (repository, service, caching, API responses)
Now in Part 4, we’ll handle trickier topics:
-
Nested Generics
-
Covariance & Contravariance
-
Generic Methods in LINQ-style APIs
-
Performance Considerations with Generics
-
Common Pitfalls and Best Practices
1) Nested Generics
Sometimes we need a generic type inside another generic type.
This looks confusing at first, but it’s actually quite powerful.
Example: Dictionary of Lists
// A dictionary where the key is string, and the value is a list of integers
Dictionary<string, List<int>> studentMarks = new();
studentMarks["Alice"] = new List<int> { 90, 85, 88 };
studentMarks["Bob"] = new List<int> { 70, 75, 80 };
foreach (var kv in studentMarks)
{
Console.WriteLine($"{kv.Key}: {string.Join(", ", kv.Value)}");
}
👉 Why?
Because we want to store multiple values for one key.
-
string→ student name -
List<int>→ their marks
Example: Generic Wrapper Around Nested Generic
// A generic container that stores key-value pairs where value itself is generic
public class MultiValueDictionary<TKey, TValue>
{
private readonly Dictionary<TKey, List<TValue>> _dict = new();
public void Add(TKey key, TValue value)
{
if (!_dict.ContainsKey(key))
_dict[key] = new List<TValue>();
_dict[key].Add(value);
}
public IEnumerable<TValue> Get(TKey key) =>
_dict.ContainsKey(key) ? _dict[key] : Enumerable.Empty<TValue>();
}
// Usage
var marks = new MultiValueDictionary<string, int>();
marks.Add("Alice", 90);
marks.Add("Alice", 85);
marks.Add("Bob", 70);
foreach (var m in marks.Get("Alice"))
Console.WriteLine(m); // 90, 85
✅ Nested generics let us create complex data structures like dictionaries of lists, sets of tuples, etc.
2) Covariance & Contravariance
These scary words just mean “Can I substitute a derived type for a base type (and vice versa) when using generics?”.
🔹 Covariance (out)
Covariance means you can use a more derived type instead of a base type when returning values.
// Read-only interface (only returns T, never accepts T)
public interface IReadOnlyRepository<out T>
{
T GetById(int id);
}
public class CustomerRepo : IReadOnlyRepository<Customer>
{
public Customer GetById(int id) => new Customer { Id = id, Name = "Alice" };
}
// Because of "out T", we can assign CustomerRepo to a variable expecting IReadOnlyRepository<IEntity>
IReadOnlyRepository<IEntity> repo = new CustomerRepo();
IEntity entity = repo.GetById(1);
Console.WriteLine(entity.Id);
👉 Why?
Because it’s safe: if an interface only returns T, substituting Customer for IEntity doesn’t break anything.
🔹 Contravariance (in)
Contravariance means you can use a less derived type instead of a more derived type when passing parameters.
// Write-only interface (only accepts T, never returns T)
public interface IWriter<in T>
{
void Write(T item);
}
public class EntityWriter : IWriter<IEntity>
{
public void Write(IEntity entity) =>
Console.WriteLine($"Writing entity with Id {entity.Id}");
}
// Because of "in T", we can assign IWriter<IEntity> to a variable expecting IWriter<Customer>
IWriter<Customer> writer = new EntityWriter();
writer.Write(new Customer { Id = 1, Name = "Bob" });
👉 Why?
Because it’s safe: if a method only accepts T, passing a Customer where IEntity is expected is fine.
💡 Easy memory trick:
-
out= "output only" → covariance -
in= "input only" → contravariance
3) Generic Methods in LINQ-style APIs
Generics power LINQ methods like Where, Select, OrderBy.
Example: Custom Generic Filter Method
public static class Extensions
{
// A generic filter method similar to LINQ's Where
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
{
if (predicate(item)) yield return item;
}
}
}
// Usage
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Filter(n => n % 2 == 0);
foreach (var n in evenNumbers)
Console.WriteLine(n); // 2, 4
👉 Why generics here?
Because Filter<T> works for any collection (ints, strings, customers) with zero code duplication.
4) Performance Considerations
Generics in C# are not like Java generics — they are implemented with reified types (the actual type is preserved at runtime).
This means:
-
Value types (int, double, struct) → No boxing/unboxing overhead in generic collections (e.g.,
List<int>is efficient). -
Reference types (classes) → Only references are stored, so no extra overhead.
Example: Boxing Problem Without Generics
ArrayList list = new ArrayList(); // Non-generic collection
list.Add(10); // int boxed into object
int num = (int)list[0]; // unboxed back
👉 Boxing/unboxing slows down performance.
Example: No Boxing With Generics
List<int> list = new List<int>();
list.Add(10); // Stored directly (no boxing)
int num = list[0]; // Retrieved directly
✅ Generics are faster and safer.
5) Common Pitfalls & Best Practices
-
Don’t overuse generics
-
If a class/method is only used with one type, no need to make it generic.
-
Example:
CustomerPrinterdoesn’t need<T>if it always printsCustomer.
-
-
Use constraints wisely
-
where T : class(only reference types) -
where T : struct(only value types) -
where T : new()(must have default constructor) -
Prevents runtime bugs by catching errors at compile time.
-
-
Be careful with nested generics
-
Too much nesting (
Dictionary<string, List<Tuple<int, string>>>) can make code unreadable. -
Use
typedef(using) to simplify:
using StudentMarks = Dictionary<string, List<int>>; -
-
Prefer interfaces with variance (
in,out) when designing libraries.-
Makes APIs more flexible.
-
🎯 Key Takeaways from Part 4
-
Nested Generics → Build powerful structures like dictionary of lists.
-
Covariance/Contravariance →
out= return types,in= parameter types. -
Generic LINQ-style Methods → Enable reusable functional patterns (
Filter,Select). -
Performance → Generics avoid boxing/unboxing, making collections faster.
-
Best Practices → Use constraints, avoid over-complication, prefer variance in APIs.
✅ With this, our Generics in C# series (Parts 1–4) is complete!
You now know everything from basics to advanced enterprise usage.
Comments
Post a Comment