Chapter 02 Csharp 2 Generics

3 min read
Rapid overview

Chapter 02 — C# 2 (Generics, nullables, delegates, iterators)

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).


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 Customer only”).

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 Type for the generic parameter.
  • Useful for diagnostics, caching per-type, reflection-based behavior (with caution).

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> vs Cache<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 for Nullable<T> (when T is 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.Value
  • x ?? fallback
  • x?.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 return produces values one-at-a-time.
  • finally blocks 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 InternalsVisibleTo for testing).