Language design philosophy
Anders Hejlsberg's core belief: Languages should be powerful yet approachable. Complexity should be opt-in. The simple case should be simple; advanced features available when needed.
"Make the easy things easy, and the hard things possible."
Every language feature should earn its place. If it makes common tasks harder or confuses developers, it shouldn't exist.
Types exist to catch errors and enable tooling—not to satisfy the compiler.
Progressive disclosure of complexity:
// Simple case: inference handles it
var name = "Alice";
var numbers = new[] { 1, 2, 3 };
// More control when needed
List<string> names = new();
// Full generic power when required
Dictionary<string, Func<Task<IEnumerable<int>>>> lookup = new();
The evolution:
Each step made the common case simpler while keeping full power available.
Hejlsberg admits null was a billion-dollar mistake—but fixing it in an existing language requires care.
The retrofit approach:
// Opt-in at project or file level
#nullable enable
// Existing code still works
string oldCode = null; // Warning, not error
// New code is safe
string newCode = "safe";
string? nullable = null;
// Gradual migration possible
Why not breaking change:
Prefer explicit, composable features over hidden behavior.
LINQ: Composition done right
// Each method is independent, composable
var result = items
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.Select(x => x.Id)
.Take(10);
// You understand each step
// You can rearrange, remove, add
// No hidden coupling
Compare to "magic" ORMs:
// Hidden behavior, hard to understand
var items = context.Items
.Include(x => x.Related) // What SQL does this generate?
.ThenInclude(x => x.Other) // N+1 query issue hidden?
.ToList(); // When does query execute?
Modern C# increasingly favors immutability.
Records (C# 9):
// Immutable by default
public record Person(string Name, int Age);
// Mutation through copying
var older = person with { Age = person.Age + 1 };
Init-only properties (C# 9):
public class Config
{
public string Endpoint { get; init; } // Set once, then readonly
public int Timeout { get; init; }
}
Required members (C# 11):
public class Order
{
public required string CustomerId { get; init; }
public required decimal Total { get; init; }
}
// Compiler enforces all required members are set
Express what you want, not how to get it.
Evolution:
// C# 1: Imperative, error-prone
if (obj is string)
{
string s = (string)obj;
if (s.Length > 0) { ... }
}
// C# 7: Basic patterns
if (obj is string s && s.Length > 0) { ... }
// C# 8: Switch expressions
var description = obj switch
{
string s when s.Length == 0 => "empty string",
string s => $"string: {s}",
int n when n < 0 => "negative",
int n => $"number: {n}",
null => "null",
_ => "unknown"
};
// C# 11: List patterns
var result = list switch
{
[] => "empty",
[var single] => $"one: {single}",
[var first, .., var last] => $"first: {first}, last: {last}"
};
Async/await transformed how we write concurrent code.
Before (callback hell):
client.GetAsync(url, response =>
{
response.ReadAsync(data =>
{
Process(data, result =>
{
callback(result);
});
});
});
After (async/await):
var response = await client.GetAsync(url);
var data = await response.ReadAsync();
var result = await Process(data);
return result;
Design principle: Async code should read like sync code. The complexity of continuations is handled by the compiler.
When a method is just a return, show it.
Evolution:
// C# 1: Verbose
public int Double(int x)
{
return x * 2;
}
// C# 6: Expression-bodied methods
public int Double(int x) => x * 2;
// Extended to properties, constructors, etc.
public string Name { get; }
public Person(string name) => Name = name;
public override string ToString() => $"Person: {Name}";
Every new C# version can compile old code. Breaking changes are extremely rare.
Why this matters:
Advanced features don't complicate simple code.
// You can write C# ignoring:
// - Spans and Memory<T>
// - ref structs
// - Unsafe code
// - Source generators
// - Advanced generic constraints
// Until you need them
The compiler should help you write correct code.
// Definite assignment analysis
int x;
if (condition)
x = 1;
Console.WriteLine(x); // Error: x might not be assigned
// Exhaustiveness checking
int result = shape switch
{
Circle c => c.Area,
Rectangle r => r.Area,
// Warning: pattern not exhaustive
};
// Nullability analysis
string? name = GetName();
Console.WriteLine(name.Length); // Warning: possible null
When designing APIs or writing code, ask:
// Fields can't evolve
public string Name; // Can't add validation later without breaking
// Properties can
public string Name { get; set; } // Add logic later, no break
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException();
}
// Can't modify string class
// But can extend it
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
}
// Enables LINQ without modifying IEnumerable
// Enables method syntax on any type
var point = new { X = 1, Y = 2 }; // Anonymous type
// Can't be returned from methods
// Can't be passed as parameters (without generics)
// WHY: Forces named types for public contracts
| Scenario | Apply Hejlsberg |
|---|---|
| API design decisions | Yes - simplicity and evolution |
| Choosing between language features | Yes - what's idiomatic? |
| Understanding "why C# works this way" | Yes |
| Performance optimization | Partially - correctness first |
| Interop with other languages | Partially - C# idioms may not apply |
"Make the easy things easy, and the hard things possible." — Anders Hejlsberg