Chapter 02 Csharp 2 Generics
5 min read- Chapter 02 Csharp 2 Generics
- TL;DR
- How it works
- 2.1 Generics
- 2.1.1 Collections before generics (why they mattered)
- 2.1.2 Generics save the day (what you should be able to explain)
- 2.1.3 What can be generic?
- 2.1.4 Type inference for method type arguments
- 2.1.5 Type constraints (encoding “allowed shapes”)
- 2.1.6 `default(T)` and `typeof(T)`
- 2.1.6a Open vs closed constructed types
- 2.1.7 Generic type initialization and state
- 2.2 Nullable value types
- 2.2.1 Aim: expressing “no value”
- 2.2.2 CLR and framework support: `Nullable
` - 2.2.3 Language support
- 2.3 Simplified delegate creation
- 2.3.2 Anonymous methods
- 2.3.3 Delegate compatibility
- 2.4 Iterators (`yield`)
- 2.5 Minor features (still interview-relevant)
Chapter 02 Csharp 2 Generics
TL;DR
Purpose of this chapter: understand the C# 2 features that made “modern C#” possible: generics, Nullable<T>, better delegate syntax (anonymous methods), and iterators (yield).
How it works
2.1 Generics
2.1.1 Collections before generics (why they mattered)
The pre-generics world commonly used object-based APIs:
- You lose compile-time safety (casts everywhere).
- You can pay runtime costs (boxing/unboxing for value types).
- Errors show up late (invalid casts) and are harder to refactor safely.
Generics fix this by letting you keep strong typing across APIs.
2.1.2 Generics save the day (what you should be able to explain)
Interview-ready framing:
- Generics let you write one algorithm/data structure that works for many types without sacrificing type safety.
- They help the compiler enforce contracts (e.g., “this collection contains
Customeronly”).
2.1.3 What can be generic?
Common generic targets:
- Classes/structs (e.g.,
List<T>,Dictionary<TKey, TValue>) - Interfaces (e.g.,
IEnumerable<T>) - Methods (e.g.,
T Parse<T>(string input)) - Delegates (e.g.,
Func<T>,Action<T>)
2.1.4 Type inference for method type arguments
Method type inference reduces ceremony:
- You often don’t need to spell out
<T>if it can be inferred from arguments.
Interview nuance:
- Inference can fail with overload resolution ambiguity; know when to make the type explicit.
2.1.5 Type constraints (encoding “allowed shapes”)
Constraints let you express requirements like:
where T : class(reference type)where T : struct(non-nullable value type)where T : new()(public parameterless ctor)where T : SomeBase/where T : ISomeInterface
Why interviewers care:
- Constraints are a way to push correctness into compile time.
- They also affect what you can do inside the generic code (
new T(), calling members, etc.).
2.1.6 default(T) and typeof(T)
default(T):
- Returns the “zero” value for the type parameter.
- For reference types:
null - For non-nullable structs: all fields defaulted
typeof(T):
- Gives you the runtime
Typefor the generic parameter. - Useful for diagnostics, caching per-type, reflection-based behavior (with caution).
2.1.6a Open vs closed constructed types
A generic declaration like List<T> is a generic type definition. Supplying type arguments constructs a type from it. The spec's vocabulary:
- Unbound generic type:
List<>/Dictionary<,>. No type arguments at all; only legal insidetypeof(...). - Open constructed type: still mentions a type parameter somewhere, e.g.
List<T>insideclass Foo<T>, orDictionary<string, T>. It only exists inside generic code and cannot be instantiated as-is. - Closed constructed type: every type argument is a concrete type, e.g.
List<int>,Dictionary<string, Customer>,List<List<int>>. This is a real runtime type: you can create instances of it.
Type unbound = typeof(Dictionary<,>); // generic type definition
Type closed = typeof(Dictionary<string, int>); // closed constructed type
Type built = unbound.MakeGenericType(typeof(string), typeof(int));
Console.WriteLine(unbound.IsGenericTypeDefinition); // True
Console.WriteLine(closed.ContainsGenericParameters); // False -> fully closed
Console.WriteLine(built == closed); // True: same runtime type
Why it matters:
- Each closed constructed type is its own type at runtime:
List<int>andList<string>have separate static fields, separate static constructors, andtypeof(List<int>) != typeof(List<string>). - Code sharing: the JIT shares one native code body across all closed types whose arguments are reference types (
List<string>,List<Customer>), but generates specialised code for each value-type argument (List<int>,List<double>), which is why value-type generics avoid boxing. - DI and reflection: containers register an open definition (
services.AddScoped(typeof(IRepository<>), typeof(Repository<>))) and close it per request (IRepository<Order>) withMakeGenericType.
A: A type built from a generic type definition where every type argument is concrete, e.g. List<int> or Dictionary<string, Customer>. Unlike an open type (List<T>, List<>), it can be instantiated, and each closed constructed type gets its own static fields and static constructor at runtime.
2.1.7 Generic type initialization and state
Things to keep clear:
- “Generic static” state is typically per closed constructed type (e.g.,
Cache<int>vsCache<string>). - This can be powerful (type-specific caches) but can surprise you (memory growth across many closed types).
2.2 Nullable value types
2.2.1 Aim: expressing “no value”
Use nullable value types when:
- You need “missing/unknown” but the domain type is a value type (e.g.,
int?,DateTime?).
2.2.2 CLR and framework support: Nullable<T>
Key idea:
T?is syntax forNullable<T>(whenTis a non-nullable value type).
Interview nuance:
- Nullables change operator behavior (“lifting”) and can complicate equality and comparisons if you don’t normalize values.
2.2.3 Language support
Patterns to be fluent with:
x.HasValue,x.Valuex ?? fallbackx?.Member(later versions, but conceptually related: propagate null safely)
2.3 Simplified delegate creation
Delegates let you pass behavior:
- “method group” conversions remove ceremony when the target delegate type is known.
2.3.2 Anonymous methods
Anonymous methods are an early step toward lambdas:
- Create inline delegates without a named method.
- Enable capturing local variables (closures), which is powerful but can produce subtle bugs if you don’t understand capture semantics.
2.3.3 Delegate compatibility
Compatibility rules enable more assignments without manual wrappers, but can confuse overload resolution.
2.4 Iterators (yield)
Iterators let you implement IEnumerable<T> with lazy execution.
Key ideas to rehearse:
- Code runs when enumerated, not when the iterator is created.
yield returnproduces values one-at-a-time.finallyblocks still matter: cleanup should run when enumeration ends (including early exit / exceptions).
Interview traps:
- Multiple enumeration can re-run the iterator body.
- Capturing mutable state in iterators can cause surprising behavior if you reuse the enumerable concurrently.
2.5 Minor features (still interview-relevant)
- Partial types: split a type across files (often used by codegen + hand-written code).
- Static classes: enforce “utility holder” semantics.
- Different access modifiers for getters/setters: control encapsulation without extra backing fields.
- Namespace aliases: disambiguate types with the same name or simplify long namespaces.
- Pragmas / fixed buffers / InternalsVisibleTo: niche but useful to recognize (especially
InternalsVisibleTofor testing).