C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
236: List Patterns (C# 11)
I was looking through some old code recently—a small utility for parsing coordinate-based movement in a grid system—and I realized how much I hated the way we used to handle arrays. We'd spend half the method just checking Length and then accessing indices, praying we didn't hit an IndexOutOfRangeException.
Fighting with array indices
Let's look at this snippet I found. It's meant to determine a movement direction based on an array of integers. It works, but it's a nightmare to read:
int[] move = { 1, 0, 5 };
string direction = "";
if (move.Length == 2 && move[0] == 1 && move[1] == 0)
{
direction = "North";
}
else if (move.Length == 2 && move[0] == 0 && move[1] == 1)
{
direction = "East";
}
else if (move.Length > 2 && move[0] == 1 && move[move.Length - 1] == 5)
{
direction = "Special Warp";
}
The logic is buried under the syntax. I'm spending more time thinking about move.Length - 1 than I am about the actual business logic. I remember when C# 11 introduced List Patterns, and I thought, "Finally, we can treat the array as a shape rather than a collection of slots."
Let's try a pattern match
What if I just describe what the array should look like? Instead of checking the length first, I can use the is operator with square brackets. Let's see if I can rewrite those first two checks.
if (move is [1, 0])
{
direction = "North";
}
else if (move is [0, 1])
{
direction = "East";
}
That's immediately cleaner. The [1, 0] pattern does two things at once: it ensures the array has exactly two elements and that those elements are exactly 1 and 0. No more Length == 2 boilerplate. It's almost like writing a regular expression, but for data structures.
Dealing with the noise
But real-world data is rarely that tidy. What if I only care that the first element is 1, but I don't care what the second element is? In the old way, I'd just omit the check for move[1]. With list patterns, I can use the discard symbol _.
// I only care that it starts with 1, and has exactly two elements
if (move is [1, _])
{
Console.WriteLine("Moving North-ish...");
}
Wait, I just noticed something. If the array has three elements, [1, _] will return false because the pattern expects exactly two elements. If I want to say "starts with 1, and I don't care how many elements follow," I need something more flexible. That's where the slice pattern .. comes in.
Handling the gaps
The slice pattern .. is the real power move here. It represents zero or more elements. Let's go back to that "Special Warp" logic—where the array starts with 1 and ends with 5, regardless of what's in the middle.
// Starts with 1, ends with 5, anything (or nothing) in between
if (move is [1, .., 5])
{
direction = "Special Warp";
}
I love this because it's declarative. I'm not calculating indices; I'm describing a sequence. I can even combine these. For instance, if I want to match any array that starts with 1, 2, and then has whatever else, I'd use [1, 2, ..]. If I want to match an empty array? Just [].
If we wrap this all into a switch expression, the transformation is pretty dramatic:
string direction = move switch
{
[1, 0] => "North",
[0, 1] => "East",
[1, ..] => "Northward Trend",
[.., 5] => "Ending in Warp",
[1, .., 5] => "Special Warp", // Be careful: order matters!
[] => "Stationary",
_ => "Unknown"
};
One thing to watch out for: the order of patterns. Since the switch evaluates top-to-bottom, a more general pattern like [1, ..] would swallow a more specific one like [1, 0] if it came first. Always put your most specific "shapes" at the top.
📋 Practical Task
Implement a Packet Header Validator
You are building a network tool that inspects byte arrays (packets). You need to create a method called ValidatePacket(byte[] packet) that returns a string describing the packet type based on the following List Pattern rules:
- An empty array
[]should return"Empty Packet". - An array that is exactly
[255, 0]should return"Keep-Alive". - An array that starts with
255, followed by any number of elements, and ends with10should return"Encrypted Data". - An array that starts with
1, followed by any number of elements should return"Standard Request". - Any other pattern should return
"Malformed Packet".
Implement this logic using a switch expression and list patterns.
There are no comments for now.