C# OOP Track
Learning Objectives
Use classes, structs and records; properties; constructors (including primary constructors); access modifiers.
Apply virtual/override/new, abstract/sealed, interfaces with default members, generics with variance.
Use delegates/events, pattern matching, attributes/reflection, IDisposable/using, exceptions and async.
Prerequisites: Stages 1–2.
Intuition and Definition
C# combines Java-like reference classes with value types (struct), first-class properties, delegates and events, LINQ, records and rich pattern matching. Methods are non-virtual by default; overriding requires virtual + override; new explicitly *hides* a base member.
Value vs reference struct (value), class (reference), record class/record struct (value equality)
get/set/init, auto-properties, required members public, private, protected, internal, protected internal, private protected, file
virtual/override/abstract/sealed, new for hiding event + delegates; EventHandler<T>
IDisposable/IAsyncDisposable, using/await using Reified, constraints (where T: class, new()), variance in/out on interfaces/delegates
Why It Matters
C# powers .NET back ends, Unity games, Windows desktop apps and many automotive/industrial tools; its features cover nearly every OOP concept in this course with concise syntax.
Syntax and Language Notes
public sealed class Ecu(string name, int address) // primary constructor (C# 12)
{
public string Name { get; } = name;
public int Address { get; } = address;
public required string Firmware { get; init; } // must be set by the creator
}Complete Example
var cluster = new Cluster();
cluster.Warning += (_, w) => Console.WriteLine($"event: {w}"); // event subscription
Sensor[] sensors = [new Coolant(112.5), new Fuel(8.0), new Coolant(90)];
foreach (var s in sensors) cluster.Check(s);
Base b = new Derived();
Console.WriteLine($"{b.Virtual()} | {b.Hidden()} | {((Derived)b).Hidden()}"); // override vs new
var p1 = new Point(1, 2);
var p2 = p1 with { Y = 5 }; // records: non-destructive copy
Console.WriteLine($"{p1} {p2} equal={p1 == new Point(1, 2)}");
using (var res = new Resource("can0")) Console.WriteLine($"using {res.Name}"); // Dispose at end
static string Describe(Sensor s) => s switch // pattern matching
{
Coolant { Celsius: > 110 } c => $"coolant overheating ({c.Celsius} C)",
Coolant c => $"coolant ok ({c.Celsius} C)",
Fuel { Percent: < 10 } => "fuel low",
_ => "other",
};
foreach (var s in sensors) Console.WriteLine(Describe(s));
public abstract record Sensor;
public sealed record Coolant(double Celsius) : Sensor;
public sealed record Fuel(double Percent) : Sensor;
public sealed class Cluster
{
public event EventHandler<string>? Wa
…event: Coolant { Celsius = 112.5 }
event: Fuel { Percent = 8 }
Derived.Virtual | Base.Hidden | Derived.Hidden
Point { X = 1, Y = 2 } Point { X = 1, Y = 5 } equal=True
using can0
disposed can0
coolant overheating (112.5 C)
fuel low
coolant ok (90 C)Step-by-Step Walkthrough
The cluster raises its Warning event for an overheating coolant and low fuel; the lambda subscriber prints them.
Through a Base reference, Virtual() dispatches to the override, but Hidden() calls Base.Hidden because new hides rather than overrides; casting to Derived reaches the hiding method.
record struct provides value equality and with copies; ToString is generated.
using calls Dispose deterministically at the end of the statement.
The switch expression matches on type and property patterns (Coolant { Celsius: > 110 }).
Common Mistakes
Using new accidentally (warning CS0108) when override was intended.
Mutable structs — copies are modified instead of the original (list[0].X = 1 doesn't compile for good reason).
Forgetting to unsubscribe from events — memory leaks (Module 41).
Not disposing IDisposables (streams, HttpResponseMessage); finalizers aren't timely.
async void outside event handlers — exceptions can't be awaited/caught.
Blocking on tasks (.Result, .Wait()).
Trade-offs and Alternatives
C# offers many ways to express the same thing (classes, records, structs, tuples); choose records for values, classes for entities/services, structs only for small immutable values where allocation matters.
Real-World Use
ASP.NET Core services, Unity game scripts, WPF/WinUI/MAUI apps, Azure Functions, diagnostic and calibration tools in the automotive industry.
Practice
Q1. (Easy) Are C# methods virtual by default?
Answer: No — they must be declared virtual (or abstract) to be overridable.
Q2. (Medium) override vs new?
Answer: override replaces a virtual method for dynamic dispatch; new declares an unrelated member that hides the base one — which version runs depends on the static type of the reference.
Q3. (Advanced) When would you choose record struct over record class?
Answer: For small, immutable values created in large numbers where avoiding heap allocation matters (points, money, ids); record class for larger values or when reference semantics/nullability is needed.
Interview Question
Q1. "Class vs struct in C#?" Answer: Classes are reference types (heap-allocated, reference copy, nullable, support inheritance); structs are value types (copied by value, usually stored inline/on the stack, no inheritance except interfaces, default value is all-zero). Use structs for small, immutable, value-like data (≤ ~16–24 bytes) to reduce allocations; avoid mutable structs because copies make mutations surprising; boxing (converting to object/interface) allocates.
Quick Quiz
Q1. What does init allow?
Answer: Setting a property only during object initialization.
Q2. Which interface enables using?
Answer: IDisposable (or IAsyncDisposable for await using).
Q3. C# keyword for a covariant type parameter?
Answer: out.
Summary
C#: classes/structs/records, properties, explicit virtual, events, pattern matching, IDisposable, reified generics.
Related: Modules 18, 22, 41, 57, 82.