| name | csharp-depth |
| description | Deep C# expertise from C# in Depth |
Skeet: C# Mastery
Jon Skeet's core belief: Understand the language deeply, then let that understanding guide simple code. The goal isn't to use every feature—it's to know which feature fits each situation.
The Foundational Principle
"The more you understand about how C# works, the simpler your code can be."
Deep knowledge enables simplicity. You don't need clever tricks when you understand the fundamentals.
Core Principles
1. Understand Value vs Reference Semantics
The most fundamental distinction in C#. Get this wrong, and everything else breaks.
Value types (structs):
- Copied on assignment
- Stored on stack (usually)
- No null by default (before nullable value types)
Reference types (classes):
- Reference copied, object shared
- Stored on heap
- Can be null
int a = 5;
int b = a;
b = 10;
Console.WriteLine(a);
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count);
Struct guidelines:
- Keep small (< 16 bytes)
- Make immutable
- Don't inherit (can't anyway)
- Use for naturally value-like concepts (Point, DateTime, Guid)
2. Nullable Reference Types Are Essential
Enable nullable reference types. They catch null bugs at compile time.
#nullable enable
string name = "Alice";
string? nickname = null;
void Greet(string name)
{
Console.WriteLine(name.Length);
}
void GreetOptional(string? name)
{
if (name is not null)
{
Console.WriteLine(name.Length);
}
}
Nullable patterns:
string display = nickname ?? "No nickname";
nickname ??= "Default";
int? length = nickname?.Length;
string definitelyNotNull = GetValueThatMightBeNull()!;
3. Pattern Matching Is Your Friend
Modern C# pattern matching replaces verbose type checks and casts.
Not this:
if (obj is string)
{
string s = (string)obj;
Console.WriteLine(s.Length);
}
This:
if (obj is string s)
{
Console.WriteLine(s.Length);
}
Switch expressions (C# 8+):
string GetDescription(Shape shape) => shape switch
{
Circle { Radius: 0 } => "Point",
Circle { Radius: var r } => $"Circle with radius {r}",
Rectangle { Width: var w, Height: var h } when w == h => $"Square {w}x{w}",
Rectangle { Width: var w, Height: var h } => $"Rectangle {w}x{h}",
_ => "Unknown shape"
};
Property patterns:
if (person is { Address: { City: "London" } })
{
}
if (person is { Address.City: "London" })
{
}
4. Records for Data
Records are the right choice for immutable data types.
public record Person(string Name, int Age);
var alice = new Person("Alice", 30);
var older = alice with { Age = 31 };
var alice2 = new Person("Alice", 30);
Console.WriteLine(alice == alice2);
var (name, age) = alice;
Record structs (C# 10+):
public readonly record struct Point(double X, double Y);
When to use records vs classes:
- Record: Data transfer, immutable state, value equality needed
- Class: Identity matters, mutable state, complex behavior
5. Generics: Understand Variance
Covariance and contravariance are confusing until they click.
Covariance (out): Can use more derived type
IEnumerable<Animal> animals = new List<Dog>();
foreach (Animal animal in animals) { }
Contravariance (in): Can use less derived type
Action<Dog> dogAction = (Animal a) => Console.WriteLine(a.Name);
dogAction(new Dog());
Memory aid:
out = output = covariant = can substitute derived
in = input = contravariant = can substitute base
6. LINQ: Deferred vs Immediate Execution
Understanding when LINQ executes is critical.
Deferred execution (most LINQ):
var query = numbers.Where(n => n > 5);
foreach (var n in query) { }
Immediate execution:
var list = numbers.Where(n => n > 5).ToList();
var count = numbers.Count(n => n > 5);
var first = numbers.First(n => n > 5);
The multiple enumeration trap:
IEnumerable<int> filtered = GetNumbers().Where(n => n > 5);
Console.WriteLine(filtered.Count());
Console.WriteLine(filtered.Sum());
var filtered = GetNumbers().Where(n => n > 5).ToList();
Console.WriteLine(filtered.Count);
Console.WriteLine(filtered.Sum());
7. Collection Expressions (C# 12)
Modern syntax for creating collections.
int[] numbers = new int[] { 1, 2, 3 };
List<int> list = new List<int> { 1, 2, 3 };
int[] numbers = [1, 2, 3];
List<int> list = [1, 2, 3];
int[] combined = [..first, ..second, 99];
ImmutableArray<int> immutable = [1, 2, 3];
Span<int> span = [1, 2, 3];
Common Pitfalls
Closure Capture
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
for (int i = 0; i < 5; i++)
{
int captured = i;
actions.Add(() => Console.WriteLine(captured));
}
foreach (var item in items)
{
actions.Add(() => Console.WriteLine(item));
}
Struct Mutation
public struct MutablePoint
{
public int X;
public int Y;
public void Move(int dx, int dy) { X += dx; Y += dy; }
}
var point = new MutablePoint { X = 1, Y = 2 };
point.Move(1, 1);
var points = new List<MutablePoint> { point };
points[0].Move(1, 1);
public readonly struct Point
{
public int X { get; init; }
public int Y { get; init; }
public Point Move(int dx, int dy) => new(X + dx, Y + dy);
}
Boxing Overhead
int x = 42;
object boxed = x;
void Log(object value) => Console.WriteLine(value);
Log(42);
void Log<T>(T value) => Console.WriteLine(value);
Log(42);
The Skeet Test
Before committing C# code, ask:
- Value or reference? Is the semantics correct for the type?
- Nullability clear? Are nullable types marked
?, non-nullable guaranteed?
- Pattern matching used? Could if-else chains be switch expressions?
- LINQ materialized? Are queries enumerated multiple times?
- Closures safe? Are loop variables captured correctly?
- Structs immutable? Are any mutable structs causing issues?
When to Apply
| Scenario | Apply Skeet |
|---|
| Language edge cases, "why does this work?" | Yes |
| Generics, variance, type inference | Yes |
| LINQ behavior and optimization | Yes |
| Nullable reference types | Yes |
| Async/await behavior | Partially - see async |
| Performance optimization | Partially - see performance-specific resources |
Source Material
- "C# in Depth" (4th Edition, Manning, 2019)
- Stack Overflow contributions (top reputation, all time)
- NodaTime library (designed by Skeet)
- Blog posts and conference talks
"The more you understand about how C# works, the simpler your code can be." — Jon Skeet