Learnixo

.NET & C# Development · Lesson 9 of 229

Deep Dive: Advanced C# — Generics, Delegates & Events

Deep Dive: Advanced C# — Generics, Delegates & Events

These features separate junior from senior C# developers. Understanding them unlocks framework-level thinking and more expressive, reusable code.


Generics

C#
// Generic class — type-safe without boxing
public class Repository<T> where T : class
{
    private readonly List<T> _items = new();

    public void Add(T item) => _items.Add(item);
    public T? Find(Func<T, bool> predicate) => _items.FirstOrDefault(predicate);
    public IReadOnlyList<T> GetAll() => _items.AsReadOnly();
}

// Generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));       // 7
Console.WriteLine(Max("apple", "banana"));  // banana

Generic Constraints

C#
// where T : class          — T must be a reference type
// where T : struct          — T must be a value type (no null)
// where T : new()           — T must have a parameterless constructor
// where T : SomeBaseClass   — T must inherit from SomeBaseClass
// where T : ISomeInterface  — T must implement the interface
// where T : notnull         — T cannot be null

public static T CreateAndInitialise<T>() where T : new()
{
    return new T();
}

public static void Process<TEntity, TId>(TEntity entity)
    where TEntity : Entity<TId>
    where TId : IComparable<TId>
{
    Console.WriteLine(entity.Id);
}

Delegates, Func, and Action

C#
// Delegate — a type-safe function pointer
public delegate int MathOperation(int a, int b);

MathOperation add = (a, b) => a + b;
MathOperation mul = (a, b) => a * b;

Console.WriteLine(add(3, 4));   // 7
Console.WriteLine(mul(3, 4));   // 12

// Func<T, TResult> — built-in delegate (up to 16 inputs, one output)
Func<int, int, int> subtract = (a, b) => a - b;
Func<string, bool> isLong = s => s.Length > 10;

// Action<T> — built-in delegate, no return value
Action<string> print = s => Console.WriteLine(s);
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);

// Predicate<T> — Func<T, bool> shorthand
Predicate<int> isEven = n => n % 2 == 0;

// Multicast delegate — chain multiple methods
Action log = () => Console.Write("Log1 ");
log += () => Console.Write("Log2 ");
log += () => Console.Write("Log3 ");
log();   // Log1 Log2 Log3

Events

C#
public class OrderService
{
    // Event — a multicast delegate with restricted access
    // External code can only += or -=, not invoke directly
    public event EventHandler<OrderCreatedEventArgs>? OrderCreated;

    public void CreateOrder(int customerId, decimal amount)
    {
        // ... create order logic ...

        // Raise the event (thread-safe with ?.Invoke)
        OrderCreated?.Invoke(this, new OrderCreatedEventArgs
        {
            CustomerId = customerId,
            Amount     = amount,
            CreatedAt  = DateTime.UtcNow,
        });
    }
}

public class OrderCreatedEventArgs : EventArgs
{
    public int     CustomerId { get; init; }
    public decimal Amount     { get; init; }
    public DateTime CreatedAt { get; init; }
}

// Subscribing
var service = new OrderService();
service.OrderCreated += (sender, args) =>
    Console.WriteLine($"Order created for customer {args.CustomerId}: £{args.Amount}");

service.CreateOrder(42, 99.99m);

Pattern Matching (C# 8–11)

C#
object value = 42;

// Type pattern
if (value is int number)
    Console.WriteLine($"Integer: {number}");

// Switch expression with patterns
string Describe(object obj) => obj switch
{
    int n when n > 0 => "positive int",
    int n when n < 0 => "negative int",
    int                => "zero",
    string { Length: 0 } => "empty string",
    string s             => $"string: {s}",
    null                 => "null",
    _                    => "something else",
};

// Positional (deconstruct) pattern
var point = (3, -5);
string quadrant = point switch
{
    (> 0, > 0) => "Q1",
    (< 0, > 0) => "Q2",
    (< 0, < 0) => "Q3",
    (> 0, < 0) => "Q4",
    _           => "on axis",
};

// List pattern (C# 11)
int[] data = { 1, 2, 3, 4 };
bool startsWithOneTwo = data is [1, 2, ..];

Expression Trees

C#
using System.Linq.Expressions;

// Expression tree — code as data (not executed, inspected)
Expression<Func<int, bool>> expr = x => x > 5;

// Inspect the tree
var binary = (BinaryExpression)expr.Body;
Console.WriteLine(binary.NodeType);   // GreaterThan

// Compile and execute
Func<int, bool> compiled = expr.Compile();
Console.WriteLine(compiled(10));   // true

// LINQ providers (EF Core) use expression trees to translate LINQ to SQL
// Func<T, bool> → executes in memory (C# code)
// Expression<Func<T, bool>> → translated to SQL
IQueryable<Order> orders = dbContext.Orders;
orders.Where(o => o.Amount > 100);   // EF Core translates this to SQL

Covariance and Contravariance

C#
// Covariance (out) — generic type can be used as its base type
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings;   // works because IEnumerable<out T>

// Contravariance (in) — generic type can accept derived types
IComparer<object> objComparer = Comparer<object>.Default;
IComparer<string> strComparer = objComparer;   // works because IComparer<in T>

// Custom covariant interface
public interface IReader<out T>
{
    T Read();
}

// Custom contravariant interface
public interface IWriter<in T>
{
    void Write(T value);
}

Interview Answer

"Generics provide compile-time type safety without boxing — use constraints (where T : class, where T : new()) to express requirements on type parameters. Delegates are typed function pointers; Func<T, TResult> and Action<T> are the built-in generic versions. Events are multicast delegates where external code can only subscribe/unsubscribe, not invoke — use ?.Invoke for thread-safe raising. Pattern matching (switch expressions, type patterns, positional patterns) replaces verbose if/else chains with exhaustive, readable logic. Expression trees represent code as data — LINQ providers like EF Core use them to translate C# lambdas to SQL. Covariance (out) allows a List<string> to be treated as IEnumerable<object>; contravariance (in) allows the reverse for input positions."