Pattern Matching to jedna z najważniejszych ewolucji C# ostatnich lat! W 2015 roku miałeś if/else i switch. W 2026 roku masz potężny system rozpoznawania wzorców który eliminuje 90% boilerplate kodu!
W tym wpisie poznasz type patterns, property patterns, positional patterns, list patterns (C# 11), logical patterns (and/or/not), relational patterns (<, >, <=, >=), i switch expressions - najbardziej zwięzły kod jaki kiedykolwiek napisałeś!
📅 Timeline - ewolucja pattern matching w C#
C# 1.0-6.0 (do 2015) - Tylko if/else i switch statement
C# 7.0 (2017) - 🔥 Type patterns, constant patterns, var patterns
// Przed C# 7 - verbose type checking
object obj = GetObject();
if (obj is string)
{
string str = (string)obj; // Casting!
Console.WriteLine($"String: {str.Length}");
}
else if (obj is int)
{
int number = (int)obj; // Casting!
Console.WriteLine($"Int: {number * 2}");
}
// Type check + casting = repetition! 😱
Type Patterns - C# 7+
🎉 C# 7 - Type Patterns
is Type variable - sprawdza typ i przypisuje do zmiennej w jednym kroku!
// C# 7+ - type pattern z declaration
object obj = GetObject();
if (obj is string str)
{
Console.WriteLine($"String: {str.Length}"); // str jest dostępny!
}
else if (obj is int number)
{
Console.WriteLine($"Int: {number * 2}");
}
else if (obj is List list)
{
Console.WriteLine($"List with {list.Count} items");
}
// Type check + assignment w jednej linii! ✨
Type patterns w switch
// Type patterns w switch statement
object value = GetValue();
switch (value)
{
case string str:
Console.WriteLine($"String of length {str.Length}");
break;
case int number when number > 0:
Console.WriteLine($"Positive int: {number}");
break;
case int number:
Console.WriteLine($"Non-positive int: {number}");
break;
case List list:
Console.WriteLine($"List with {list.Count} items");
break;
case null:
Console.WriteLine("Null value");
break;
default:
Console.WriteLine("Unknown type");
break;
}
❌ Przed C# 7 - verbose
object shape = GetShape();
if (shape is Circle)
{
Circle c = (Circle)shape;
Console.WriteLine(c.Radius);
}
else if (shape is Rectangle)
{
Rectangle r = (Rectangle)shape;
Console.WriteLine(r.Width);
}
// Type check + cast - dwa kroki!
✅ C# 7+ - zwięźle
object shape = GetShape();
if (shape is Circle c)
{
Console.WriteLine(c.Radius);
}
else if (shape is Rectangle r)
{
Console.WriteLine(r.Width);
}
// Jeden krok! ✨
{ Property: value } - sprawdzaj wartości properties w pattern!
// Property patterns w if
public record Person(string Name, int Age, string City);
Person person = GetPerson();
// Property pattern - sprawdź wartość property
if (person is { Age: 30 })
{
Console.WriteLine("Person is 30 years old");
}
// Multiple properties
if (person is { Age: 30, City: "Warsaw" })
{
Console.WriteLine("30 year old from Warsaw");
}
// Property + variable
if (person is { Age: var age, City: "Warsaw" })
{
Console.WriteLine($"Age {age} from Warsaw");
}
// Łączenie positional i property patterns
public record Person(string Name, int Age)
{
public string City { get; init; }
}
string Describe(Person person) => person switch
{
// Positional pattern + property pattern
("Jan", var age) { City: "Warsaw" } => $"Jan, {age}, from Warsaw",
// Positional with conditions
(var name, > 18) => $"{name} is an adult",
(var name, <= 18) => $"{name} is a minor",
_ => "Unknown"
};
var p1 = new Person("Jan", 30) { City = "Warsaw" };
Console.WriteLine(Describe(p1)); // "Jan, 30, from Warsaw"
🔥 List Patterns - pattern matching na kolekcjach (C# 11)
Podstawowe list patterns
🎉 C# 11 - List Patterns
[pattern1, pattern2, ..] - pattern matching na elementach listy/array!
// List patterns - matching na array/list
int[] numbers = { 1, 2, 3, 4, 5 };
// Match specific elements
if (numbers is [1, 2, 3, 4, 5])
{
Console.WriteLine("Exactly these numbers");
}
// Match with discard
if (numbers is [1, _, 3, _, 5])
{
Console.WriteLine("1, something, 3, something, 5");
}
// Match with var (capture element)
if (numbers is [var first, var second, ..])
{
Console.WriteLine($"First: {first}, Second: {second}"); // 1, 2
}
// Match any length
if (numbers is [..])
{
Console.WriteLine("Any array (not null)");
}
// Capture slice with var
int[] numbers = { 1, 2, 3, 4, 5 };
if (numbers is [var first, .. var rest])
{
Console.WriteLine($"First: {first}"); // 1
Console.WriteLine($"Rest: {string.Join(",", rest)}"); // 2,3,4,5
}
// Multiple slices
if (numbers is [var first, .. var middle, var last])
{
Console.WriteLine($"First: {first}, Last: {last}"); // 1, 5
Console.WriteLine($"Middle: {string.Join(",", middle)}"); // 2,3,4
}
// Match from end
if (numbers is [.., var secondLast, var last])
{
Console.WriteLine($"Last two: {secondLast}, {last}"); // 4, 5
}
// Complex patterns
string AnalyzeSequence(int[] seq) => seq switch
{
[] => "Empty",
[_] => "Single element",
[var x, var y] when x == y => "Two equal elements",
[var x, var y] when x < y => "Ascending pair",
[var x, var y] when x > y => "Descending pair",
[1, ..] => "Starts with 1",
[.., 10] => "Ends with 10",
[1, .., 10] => "Starts with 1, ends with 10",
[var first, .. var middle, var last] when first == last => "First equals last",
_ => "Other sequence"
};
Console.WriteLine(AnalyzeSequence(new[] { 1, 2, 3, 10 })); // "Starts with 1, ends with 10"
List patterns z type patterns
// List patterns + type patterns
object[] mixed = { 1, "hello", 3.14, true };
if (mixed is [int x, string s, double d, bool b])
{
Console.WriteLine($"Int: {x}, String: {s}, Double: {d}, Bool: {b}");
}
// Switch expression z list patterns
string DescribeMixed(object[] arr) => arr switch
{
[int, int, int] => "Three integers",
[string, ..] => "Starts with string",
[.., string] => "Ends with string",
[int x, string s] => $"Int {x} and string {s}",
_ => "Mixed types"
};
💡 Praktyczne przykłady - list patterns
// Przykład 1: Command parsing
string ProcessCommand(string[] args) => args switch
{
["help"] => ShowHelp(),
["version"] => ShowVersion(),
["run", var file] => RunFile(file),
["run", var file, "--debug"] => RunFile(file, debug: true),
["install", .. var packages] => Install(packages),
_ => "Unknown command"
};
// Przykład 2: Path parsing
string GetFileInfo(string[] pathParts) => pathParts switch
{
[] => "No path",
[var file] => $"File: {file}",
[.., var file] => $"File in subdirectory: {file}",
["home", "user", .. var rest] => $"User path: {string.Join("/", rest)}"
};
// Przykład 3: HTTP request routing
string Route(string[] segments) => segments switch
{
["api", "users"] => GetAllUsers(),
["api", "users", var id] => GetUser(id),
["api", "products", var id, "reviews"] => GetProductReviews(id),
_ => "Not found"
};
Logical Patterns - and, or, not (C# 9+)
Logical patterns - łączenie warunków
🎉 C# 9 - Logical Patterns
and, or, not - łącz patterns jak warunki logiczne!
// and pattern - oba muszą być spełnione
int number = 15;
if (number is > 10 and < 20)
{
Console.WriteLine("Between 10 and 20");
}
// or pattern - przynajmniej jeden
if (number is < 10 or > 100)
{
Console.WriteLine("Less than 10 OR greater than 100");
}
// not pattern - negacja
if (number is not 0)
{
Console.WriteLine("Not zero");
}
// and + or
int x = 15;
if (x is (> 10 and < 20) or (> 30 and < 40))
{
Console.WriteLine("In range 11-19 OR 31-39");
}
// not + and
if (x is not (< 0 or > 100))
{
Console.WriteLine("Valid percentage (0-100)");
}
// Complex logical patterns
string ValidateInput(int value) => value switch
{
< 0 => "Negative not allowed",
>= 0 and <= 100 => "Valid",
> 100 and <= 1000 => "High value",
> 1000 => "Too high",
_ => "Invalid"
};
Logical patterns z property patterns
// Logical patterns + property patterns
public record Product(string Name, decimal Price, string Category, bool InStock);
string GetProductStatus(Product product) => product switch
{
// and z property patterns
{ Category: "Electronics", Price: > 1000 and < 5000 }
=> "Mid-range electronics",
// or z property patterns
{ Category: "Books" or "Magazines", InStock: true }
=> "Available reading material",
// not z property patterns
{ Category: not "Food", Price: > 10000 }
=> "Expensive non-food item",
// Complex combination
{ InStock: true, Price: > 100 and < 500 } and { Category: "Electronics" or "Appliances" }
=> "Available mid-price electronics/appliances",
_ => "Other product"
};
Relational Patterns - porównania (C# 9+)
Relational patterns - <, >, <=, >=
🎉 C# 9 - Relational Patterns
<, >, <=, >= - porównania bezpośrednio w patterns!
// Relational + property patterns
public record Order(decimal Total, int ItemCount, bool IsPremium);
decimal CalculateShipping(Order order) => order switch
{
{ Total: >= 100 } => 0, // Free shipping
{ Total: >= 50, IsPremium: true } => 0, // Premium free at 50
{ Total: >= 50 } => 5, // Reduced shipping
{ ItemCount: > 10 } => 10, // Bulk shipping
{ Total: < 20 } => 8, // Small order
_ => 12 // Standard shipping
};
var order1 = new Order(120, 5, false);
Console.WriteLine(CalculateShipping(order1)); // 0 (free shipping)
var order2 = new Order(60, 3, true);
Console.WriteLine(CalculateShipping(order2)); // 0 (premium free at 50)
Relational patterns - praktyczne przykłady
// BMI classification
string ClassifyBMI(double bmi) => bmi switch
{
< 18.5 => "Underweight",
>= 18.5 and < 25 => "Normal weight",
>= 25 and < 30 => "Overweight",
>= 30 and < 35 => "Obese Class I",
>= 35 and < 40 => "Obese Class II",
>= 40 => "Obese Class III"
};
// HTTP status code classification
string ClassifyStatusCode(int code) => code switch
{
>= 200 and < 300 => "Success",
>= 300 and < 400 => "Redirection",
>= 400 and < 500 => "Client Error",
>= 500 => "Server Error",
_ => "Invalid status code"
};
// Temperature comfort
string ComfortLevel(double celsius) => celsius switch
{
< 10 => "Too cold",
>= 10 and < 18 => "Cold",
>= 18 and < 22 => "Comfortable",
>= 22 and < 28 => "Warm",
>= 28 => "Too hot"
};
Console.WriteLine(ClassifyBMI(22.5)); // "Normal weight"
Console.WriteLine(ClassifyStatusCode(404)); // "Client Error"
Console.WriteLine(ComfortLevel(20)); // "Comfortable"
Switch Expressions - pełna moc (C# 8+)
Switch statement vs switch expression
❌ Switch statement - verbose
string GetDayType(DayOfWeek day)
{
string result;
switch (day)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
result = "Weekend";
break;
case DayOfWeek.Monday:
result = "Start of week";
break;
case DayOfWeek.Friday:
result = "End of week";
break;
default:
result = "Weekday";
break;
}
return result;
}
// 19 linii!
✅ Switch expression - zwięźle
string GetDayType(DayOfWeek day) => day switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday
=> "Weekend",
DayOfWeek.Monday => "Start of week",
DayOfWeek.Friday => "End of week",
_ => "Weekday"
};
// 6 linii! ✨
Switch expressions - składnia
// Switch expression składnia
// value switch { pattern => result, ... }
// Podstawowe użycie
string GetSeason(int month) => month switch
{
12 or 1 or 2 => "Winter",
3 or 4 or 5 => "Spring",
6 or 7 or 8 => "Summer",
9 or 10 or 11 => "Fall",
_ => "Invalid month"
};
// Z type patterns
string DescribeType(object obj) => obj switch
{
int i => $"Integer: {i}",
string s => $"String of length {s.Length}",
double d => $"Double: {d:F2}",
null => "Null value",
_ => "Unknown type"
};
// Z property patterns
record Person(string Name, int Age);
string Describe(Person p) => p switch
{
{ Age: < 13 } => "Child",
{ Age: < 20 } => "Teenager",
{ Age: >= 65 } => "Senior",
_ => "Adult"
};
Switch expressions - all patterns combined
// Wszystkie patterns razem!
public record Order(int Id, string Status, decimal Total, List Items, Customer Customer);
public record Customer(string Name, bool IsPremium, int OrderCount);
string AnalyzeOrder(Order order) => order switch
{
// Null check
null => "No order",
// Property + relational + logical
{ Status: "Cancelled" } => "Order cancelled",
// List pattern + property
{ Items: [] } => "Empty order",
{ Items: [var single] } => $"Single item order: {single}",
{ Items: [var first, .., var last] } => $"Multiple items: {first}...{last}",
// Nested property + relational
{ Customer.IsPremium: true, Total: > 1000 }
=> "High-value premium order",
{ Customer: { OrderCount: > 10, IsPremium: false }, Total: > 500 }
=> "Consider upgrading to premium",
// Property + relational + logical (complex)
{ Status: "Shipped", Total: >= 100 and < 500 }
=> "Standard shipped order",
{ Status: "Shipped", Total: >= 500 }
=> "Large shipped order",
// Type pattern na nested object
{ Customer: { Name: var name } } when name.StartsWith("VIP")
=> $"VIP customer order: {name}",
_ => "Regular order"
};
var order = new Order(
1,
"Shipped",
250m,
new List { "Laptop", "Mouse", "Keyboard" },
new Customer("John", false, 5)
);
Console.WriteLine(AnalyzeOrder(order)); // "Standard shipped order"
Switch expressions - praktyczne przykłady
// Przykład 1: Calculator
decimal Calculate(decimal a, decimal b, string op) => op switch
{
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" when b != 0 => a / b,
"/" => throw new DivideByZeroException(),
_ => throw new ArgumentException($"Unknown operator: {op}")
};
// Przykład 2: HTTP status message
string GetStatusMessage(int statusCode) => statusCode switch
{
200 => "OK",
201 => "Created",
204 => "No Content",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
>= 200 and < 300 => "Success",
>= 300 and < 400 => "Redirection",
>= 400 and < 500 => "Client Error",
>= 500 => "Server Error",
_ => "Unknown Status"
};
// Przykład 3: Discount calculation
decimal GetDiscount(Customer customer, decimal total) => (customer, total) switch
{
(null, _) => 0,
({ IsPremium: true }, > 1000) => 0.25m,
({ IsPremium: true }, _) => 0.15m,
({ OrderCount: > 20 }, > 500) => 0.20m,
({ OrderCount: > 10 }, _) => 0.10m,
(_, > 500) => 0.05m,
_ => 0
};
// Przykład 4: Rock Paper Scissors
string RockPaperScissors(string p1, string p2) => (p1, p2) switch
{
("rock", "rock") or ("paper", "paper") or ("scissors", "scissors") => "Tie",
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "Player 1 wins",
("scissors", "rock") or ("paper", "scissors") or ("rock", "paper") => "Player 2 wins",
_ => "Invalid input"
};
Console.WriteLine(Calculate(10, 5, "+")); // 15
Console.WriteLine(GetStatusMessage(404)); // "Not Found"
Console.WriteLine(RockPaperScissors("rock", "scissors")); // "Player 1 wins"
Podsumowanie
Pattern Matching - od verbose if/else do zwięzłego, eleganci kodu!
✅ Type patterns (C# 7) - is Type variable, type checking + assignment
W kolejnym wpisie skupimy się na Span<T> i Memory<T> - wydajność bez alokacji!
Zadanie dla Ciebie 🎯
Stwórz system routingu HTTP używając WSZYSTKICH patterns:
public record HttpRequest(string Method, string[] Path, Dictionary Query);
public record HttpResponse(int StatusCode, string Body);
// Zaimplementuj router używając switch expression + all patterns
HttpResponse Route(HttpRequest request)
{
// GET /api/users -> list all users
// GET /api/users/{id} -> get user by id
// POST /api/users -> create user
// GET /api/products?category=electronics -> filter products
// DELETE /api/orders/{id} -> delete order
// etc.
}
Wymagania - użyj:
Switch expression jako główny router
Property patterns na Method
List patterns na Path (first, last, middle)
Logical patterns (and, or, not)
Relational patterns jeśli potrzeba
🎯 BONUS: Expression Evaluator z Pattern Matching
Stwórz evaluator dla wyrażeń matematycznych używając records + pattern matching!
Do zaimplementowania:
Expression tree z records:
abstract record Expr;
record Constant(double Value) : Expr;
record Variable(string Name) : Expr;
record BinaryOp(Expr Left, string Op, Expr Right) : Expr;
record UnaryOp(string Op, Expr Operand) : Expr;
Evaluate używając switch expression:
Type patterns na typy Expr
Property patterns na Op
Positional patterns na Left/Right
Simplify (optimizer) używając patterns:
x + 0 → x
x * 1 → x
x * 0 → 0
(Constant a) + (Constant b) → Constant(a + b)
ToString używając patterns:
Pretty print wyrażenia
Nawiasy gdy potrzeba
Przykład użycia:
// Build: (x + 5) * 2
var expr = new BinaryOp(
new BinaryOp(
new Variable("x"),
"+",
new Constant(5)
),
"*",
new Constant(2)
);
// Evaluate with x = 10
var vars = new Dictionary { ["x"] = 10 };
var result = Evaluate(expr, vars); // (10 + 5) * 2 = 30
// Simplify
var simplified = Simplify(expr); // Pattern match dla optimizations
// ToString
Console.WriteLine(expr.ToString()); // "(x + 5) * 2"
// More examples:
var zero = new BinaryOp(new Variable("x"), "*", new Constant(0));
var simplified = Simplify(zero); // Constant(0) - eliminated x!
var identity = new BinaryOp(new Variable("x"), "+", new Constant(0));
var simplified2 = Simplify(identity); // Variable("x") - eliminated +0!
Ten projekt demonstruje pełną moc pattern matching - type patterns, property patterns, positional patterns, switch expressions, wszystko razem! 🚀✨🧮