Collections — List, Dictionary, HashSet & Friends

Which container, what each operation costs, and the read-only, immutable and frozen variants.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (the base class library)

Sources: Collections and data structures — .NET · Collection expressions — C# reference

Pick the container

Enumeration order of `Dictionary` and `HashSet` is not guaranteed — sort when order matters.

NeedUseWhy
ordered listList<T>, T[]contiguous; [..] collection expressions build either
key → valueDictionary<TKey, TValue>O(1) average; TryGetValue for a miss
membership, dedupeHashSet<T>O(1) Contains; set operations mutate in place
sorted keysSortedDictionary, SortedSetO(log n)
FIFO / LIFOQueue<T>, Stack<T>O(1) at the end that matters
priority queuePriorityQueue<TElement, TPriority>a min-heap since .NET 6
read-mostly lookupFrozenDictionary, FrozenSetbuilt once, optimized for reads (.NET 8)
snapshots you can shareImmutableList, ImmutableDictionaryevery change returns a new collection

What operations cost

Amortised costs (*) include the occasional resize.

TypeLookupInsertRemove
List<T>O(1) by index, O(n) ContainsO(1)* Add, O(n) InsertO(1) last, O(n) other
Dictionary / HashSetO(1) averageO(1)* averageO(1) average
SortedDictionary / SortedSetO(log n)O(log n)O(log n)
Queue<T> / Stack<T>O(1) peekO(1)*O(1)
PriorityQueueO(1) peekO(log n)O(log n)
ImmutableList<T>O(log n)O(log n)O(log n)

Dictionary and HashSet

`GetValueOrDefault` reads a missing key as the default; `UnionWith` changes the set in place.

var counts = new Dictionary<string, int>();
foreach (var word in "the cat and the hat".Split(' '))
{
    counts[word] = counts.GetValueOrDefault(word) + 1;
}
Console.WriteLine(string.Join(", ", counts.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => $"{kv.Key}={kv.Value}")));
Console.WriteLine($"{counts["the"]} {counts.ContainsKey("dog")} {counts.TryGetValue("cat", out var cat)} {cat}");

var set = new HashSet<int> { 1, 2, 3 };
set.UnionWith([2, 3, 4]);
Console.WriteLine(string.Join(",", set.Order()));
Result
and=1, cat=1, hat=1, the=2
2 False True 1
1,2,3,4
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Dictionary<TKey,TValue> — .NET API

Keys need value equality

A `record` generates `Equals` and `GetHashCode`; a class compares by reference.

var points = new HashSet<Point> { new(1, 1), new(1, 1) };
var cells = new HashSet<Cell> { new(1, 1), new(1, 1) };
Console.WriteLine($"{points.Count} {cells.Count}");

record Point(int X, int Y);
class Cell(int x, int y)
{
    public int X => x;
    public int Y => y;
}
Result
1 2
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Records — C# reference

Queue, Stack, PriorityQueue

`PriorityQueue` dequeues the lowest priority first and is not stable for equal priorities.

var queue = new Queue<string>(["a", "b"]);
queue.Enqueue("c");
var stack = new Stack<int>([1, 2, 3]);
var tasks = new PriorityQueue<string, int>();
tasks.Enqueue("low", 5);
tasks.Enqueue("urgent", 1);
tasks.Enqueue("mid", 3);
Console.WriteLine($"{queue.Dequeue()} {stack.Pop()} {tasks.Dequeue()} {tasks.Dequeue()}");
Result
a 3 urgent mid
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: PriorityQueue<TElement,TPriority> — .NET API

Read-only, immutable, frozen

A read-only interface is a view; immutable collections return new ones; frozen ones trade build time for fast reads.

using System.Collections.Frozen;
using System.Collections.Immutable;

var colors = new Dictionary<string, int> { ["red"] = 1, ["green"] = 2 }.ToFrozenDictionary();
ImmutableList<int> v1 = [1, 2];
var v2 = v1.Add(3);                            // a new list; v1 is unchanged
var backing = new List<int> { 1 };
IReadOnlyList<int> view = backing;
backing.Add(2);                                // the view sees it
Console.WriteLine($"{colors["green"]} {v1.Count} {v2.Count} {view.Count}");
Result
2 2 3 2
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: System.Collections.Frozen — .NET API

Pitfalls

Where C# differs from Swift's value-type collections.

  • Modifying a List or Dictionary inside its own foreach throws InvalidOperationException.
  • list.RemoveAt(0) in a loop is O(n²) — use Queue<T>.
  • Relying on Dictionary or HashSet enumeration order: it is an implementation detail.
  • Expose IReadOnlyList<T> / IReadOnlyDictionary from APIs and keep the mutable type private.
  • Collections are reference types even when they hold structs: var b = a aliases the list.

Collections — LINQ Cookbook

Filter, project, fold, group, sort, chunk and zip — and the deferred execution that surprises everyone once.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (System.Linq)

Sources: Language Integrated Query (LINQ) — C#

I want to…

Method syntax; every operator returns a new sequence.

GoalOperator
transform eachSelect, SelectMany
keep someWhere, OfType<T>()
fold to one valueSum, Aggregate, Count
first matchFirst, FirstOrDefault, Single
testAny, All, Contains
sortOrder, OrderBy(…).ThenBy(…) (stable)
group / index / countGroupBy, ToDictionary, ToLookup, CountBy (.NET 9)
sliceSkip, Take, Chunk, TakeWhile
pair upZip
dedupeDistinct, DistinctBy
extremesMax, MaxBy, MinBy

Filter, project, fold

`Sum` says the intent; `Aggregate` is the general fold.

Order[] orders = [new(1, "ana", 30), new(2, "bo", 12), new(3, "ana", 8)];
Console.WriteLine(string.Join(",", orders.Where(o => o.Total >= 10).Select(o => o.Id)));
Console.WriteLine($"{orders.Sum(o => o.Total)} {orders.Aggregate(0, (acc, o) => acc + o.Total)}");

record Order(int Id, string Customer, int Total);
Result
1,2
50 50
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Group, index, count

`GroupBy` keeps the order in which keys first appear; `CountBy` is new in .NET 9.

Person[] people = [new("Ana", "web"), new("Bo", "data"), new("Cy", "web")];
foreach (var team in people.GroupBy(p => p.Team))
    Console.WriteLine($"{team.Key}: {string.Join(",", team.Select(p => p.Name))}");

Console.WriteLine(people.ToDictionary(p => p.Name)["Bo"].Team);
Console.WriteLine(string.Join(" ", people.CountBy(p => p.Team).Select(kv => $"{kv.Key}={kv.Value}")));
Console.WriteLine(people.ToLookup(p => p.Team)["web"].Count());

record Person(string Name, string Team);
Result
web: Ana,Cy
data: Bo
data
web=2 data=1
2
Setup
  1. .NET 9 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

  • csharp >=13
  • dotnet >=9

Sources: Enumerable.CountBy — .NET API

Deferred execution

A query is a recipe, not a result — it runs each time it is enumerated, against the source as it is then.

var source = new List<int> { 1, 2, 3 };
var evens = source.Where(n => n % 2 == 0);     // nothing has run yet
source.Add(4);
Console.WriteLine(string.Join(",", evens));    // runs now, and sees 4

var snapshot = evens.ToList();                  // materialize once
source.Add(6);
Console.WriteLine($"{string.Join(",", evens)} vs {string.Join(",", snapshot)}");
Result
2,4
2,4,6 vs 2,4
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Deferred execution and lazy evaluation — .NET

Sort by several keys

`OrderBy` is stable; pass an explicit comparer for text so the culture cannot change the order.

Player[] players = [new("cy", 7), new("ana", 9), new("bo", 7)];
var ranked = players.OrderByDescending(p => p.Score).ThenBy(p => p.Name, StringComparer.Ordinal);
Console.WriteLine(string.Join(" ", ranked.Select(p => $"{p.Name}:{p.Score}")));
Console.WriteLine($"{players.MaxBy(p => p.Score)!.Name} {players.MinBy(p => p.Score)!.Name}");

record Player(string Name, int Score);
Result
ana:9 bo:7 cy:7
ana cy
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Chunk, distinct, zip, skip/take

All built in since .NET 6.

int[] xs = [1, 2, 3, 4, 5];
Console.WriteLine(string.Join(" ", xs.Chunk(2).Select(c => $"[{string.Join(",", c)}]")));
Console.WriteLine(string.Join(",", new[] { "b", "a", "b", "c", "a" }.Distinct()));
Console.WriteLine(string.Join(" ", new[] { "a", "b", "c" }.Zip(new[] { 1, 2 }, (s, n) => $"{s}{n}")));
Console.WriteLine(string.Join(",", xs.Skip(1).Take(3)));
Result
[1,2] [3,4] [5]
b,a,c
a1 b2
2,3,4
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Rules

Where LINQ costs more than it looks.

  • Enumerating the same query twice runs it twice — including any database or network call behind an IQueryable.
  • Count() > 0 walks the sequence; Any() stops at the first element.
  • Materialize (ToList, ToArray) at the boundary where you want a snapshot.
  • Lambdas in hot loops allocate; for tight numeric code a for loop over a Span<T> is faster.

Async/Await, Tasks & Cancellation

Sequential vs concurrent awaits, WhenAll and its exceptions, CancellationToken, IAsyncEnumerable and bridging callbacks.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (System.Threading.Tasks)

Sources: Asynchronous programming with async and await — C# · Cancellation in managed threads — .NET

Sequential vs concurrent

A task starts when it is created; `await` only waits. Start both, then await both.

using System.Diagnostics;

static async Task<string> Load(string name)
{
    await Task.Delay(100);                         // frees the thread while waiting
    return name;
}

var clock = Stopwatch.StartNew();
var user = await Load("user");
var posts = await Load("posts");
var sequential = clock.ElapsedMilliseconds;

clock.Restart();
var both = await Task.WhenAll(Load("user"), Load("posts"));
var concurrent = clock.ElapsedMilliseconds;
Console.WriteLine($"{user} {posts} | {string.Join(" ", both)}");
// About 200 ms against about 100 ms; compared, not printed, so a busy machine cannot change the output
Console.WriteLine($"concurrent took well under the sequential time: {concurrent < sequential * 3 / 4}");
Result
user posts | user posts
concurrent took well under the sequential time: True
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Cancellation with CancellationToken

Cancellation is a token you pass down; awaiting a cancelled operation throws `OperationCanceledException`.

static async Task Poll(CancellationToken token, Action<int> onTick)
{
    for (var i = 0; ; i++)
    {
        Console.WriteLine($"tick {i}");
        onTick(i);
        await Task.Delay(50, token);           // a cancellation point
    }
}

// new CancellationTokenSource(TimeSpan.FromSeconds(5)) cancels on a deadline the same way
using var cts = new CancellationTokenSource();
try
{
    await Poll(cts.Token, i => { if (i == 2) cts.Cancel(); });   // e.g. the user left the screen
}
catch (OperationCanceledException)
{
    Console.WriteLine("cancelled");
}
Result
tick 0
tick 1
tick 2
cancelled
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

WhenAll and its exceptions

`await` rethrows the first failure only; the task's `Exception` holds all of them.

static async Task<int> Work(int n)
{
    await Task.Delay(10 * n);
    return n == 2 ? throw new InvalidOperationException($"job {n} failed") : n;
}

var all = Task.WhenAll(Work(1), Work(2), Work(3));
try
{
    await all;
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
    Console.WriteLine($"{all.Exception!.InnerExceptions.Count} failure(s) recorded");
}
Result
job 2 failed
1 failure(s) recorded
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Async streams

`IAsyncEnumerable` is the pull-based stream; `break` disposes the enumerator, so no further page is fetched.

using System.Runtime.CompilerServices;

static async IAsyncEnumerable<int[]> Pages(int total, int size, [EnumeratorCancellation] CancellationToken token = default)
{
    for (var offset = 0; offset < total; offset += size)
    {
        await Task.Delay(10, token);               // stand-in for a fetch
        yield return Enumerable.Range(offset, Math.Min(size, total - offset)).ToArray();
    }
}

var seen = 0;
await foreach (var page in Pages(7, 3))
{
    seen += page.Length;
    Console.WriteLine(string.Join(",", page));
    if (seen >= 6) break;
}
Result
0,1,2
3,4,5
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Generate and consume async streams — C#

Bridging callbacks

`TaskCompletionSource` is `withCheckedContinuation`; `WaitAsync` adds a timeout to any task.

static void FetchTheme(Action<string> onDone) =>        // a callback API you do not own
    ThreadPool.QueueUserWorkItem(_ => onDone("dark"));

static Task<string> FetchThemeAsync()
{
    var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
    FetchTheme(value => tcs.SetResult(value));
    return tcs.Task;
}

Console.WriteLine(await FetchThemeAsync());
Console.WriteLine(await FetchThemeAsync().WaitAsync(TimeSpan.FromSeconds(5)));
Result
dark
dark
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Swift concurrency → C#

The same shapes; C# tasks are hot and cancellation is explicit.

SwiftC#Difference
async letstart a Task, await it latertasks start eagerly
withThrowingTaskGroupTask.WhenAll(items.Select(Work))no automatic cancellation of siblings — share a token
Task.isCancelledtoken.IsCancellationRequested, ThrowIfCancellationRequested()a parameter, not ambient
AsyncStreamIAsyncEnumerable<T>, Channel<T>—
withCheckedContinuationTaskCompletionSource<T>SetResult twice throws; TrySetResult does not
Task.detachedTask.Runqueues to the thread pool
@MainActorSynchronizationContext (UI thread)a runtime mechanism, not compiler-checked

Rules

What reviewers flag first.

  • async void outside event handlers: exceptions crash the process and nobody can await it.
  • .Result / .Wait() on a UI or legacy ASP.NET context deadlocks — await all the way up.
  • Accept a CancellationToken in every async API and pass it to everything you call.
  • ConfigureAwait(false) in library code that does not need the caller's context.
  • ValueTask only for hot paths that usually complete synchronously; await it exactly once.

Algorithmic Patterns & Complexity

The BCL's costs, then the patterns that solve most problems — PriorityQueue and BinarySearch included.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (the base class library)

Sources: Collections and data structures — .NET

Built-in costs

Two sorts with different guarantees — know which one you called.

NeedBuilt-inCost
stack / queueStack<T>, Queue<T>O(1)
priority queuePriorityQueue<TElement, TPriority>O(log n) enqueue / dequeue
sorted map / setSortedDictionary, SortedSetO(log n); SortedSet.GetViewBetween for ranges
sort in placeArray.Sort, List.SortO(n log n) introsort — not stable
sort a copyOrderByO(n log n) — stable
binary searchArray.BinarySearch, List.BinarySearchO(log n); ~result is the insertion point on a miss
build a stringStringBuilder, string.Join+= in a loop is O(n²)

Two pointers

On sorted input, move the pointer that brings the sum closer. O(n), O(1) space.

static (int, int)? PairWithSum(int[] sorted, int target)
{
    int lo = 0, hi = sorted.Length - 1;
    while (lo < hi)
    {
        var sum = sorted[lo] + sorted[hi];
        if (sum == target) return (lo, hi);
        if (sum < target) lo++; else hi--;
    }
    return null;
}

Console.WriteLine($"{PairWithSum([1, 3, 4, 6, 9], 10)} {PairWithSum([1, 2], 7)?.ToString() ?? "none"}");
Result
(0, 4) none
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sliding window

Longest substring without repeats — grow the right edge, jump the left past the last repeat. O(n).

static int LongestUnique(string s)
{
    var lastSeen = new Dictionary<char, int>();
    int start = 0, best = 0;
    for (var i = 0; i < s.Length; i++)
    {
        if (lastSeen.TryGetValue(s[i], out var prev) && prev >= start) start = prev + 1;
        lastSeen[s[i]] = i;
        best = Math.Max(best, i - start + 1);
    }
    return best;
}

Console.WriteLine($"{LongestUnique("abcabcbb")} {LongestUnique("pwwkew")}");
Result
3 3
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Heap — top k

A min-heap of size k keeps the k largest; its root is the smallest of them. O(n log k).

static int[] TopK(IEnumerable<int> xs, int k)
{
    var heap = new PriorityQueue<int, int>();
    foreach (var x in xs)
    {
        heap.Enqueue(x, x);
        if (heap.Count > k) heap.Dequeue();
    }
    return [.. heap.UnorderedItems.Select(e => e.Element).OrderDescending()];
}

Console.WriteLine(string.Join(",", TopK([5, 1, 9, 3, 7, 9, 2], 3)));
Result
9,9,7
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Breadth-first search on a grid

BFS finds shortest paths in unweighted graphs.

static int ShortestPath(string[] grid, (int R, int C) from, (int R, int C) to)
{
    var dist = new Dictionary<(int, int), int> { [from] = 0 };
    var queue = new Queue<(int R, int C)>([from]);
    while (queue.TryDequeue(out var cell))
    {
        var here = dist[cell];
        if (cell == to) return here;
        foreach (var (dr, dc) in new[] { (1, 0), (-1, 0), (0, 1), (0, -1) })
        {
            var next = (R: cell.R + dr, C: cell.C + dc);
            if (next.R < 0 || next.C < 0 || next.R >= grid.Length || next.C >= grid[0].Length
                || grid[next.R][next.C] == '#' || dist.ContainsKey(next)) continue;
            dist[next] = here + 1;
            queue.Enqueue(next);
        }
    }
    return -1;
}

Console.WriteLine(ShortestPath(["..#.", ".##.", "...."], (0, 0), (0, 3)));
Result
7
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Working a problem out loud

The habits that matter more than the language.

  • State the brute force and its cost first, then the bottleneck the better pattern removes.
  • Name the edge cases before coding: empty input, one element, duplicates, int overflow (checked makes it throw).
  • Relying on Array.Sort keeping equal elements in order — use OrderBy when stability matters.
  • Span<T> slices without copying — the cheap way to pass a window of an array.

Testing Approaches in .NET

xUnit, NUnit or MSTest; TimeProvider and hand-written fakes for deterministic tests.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET with a test project

Sources: Testing in .NET

Tooling matrix

The common .NET choices.

NeedTool
runner and assertionsxUnit, NUnit or MSTest (dotnet new xunit, nunit, mstest)
timeTimeProvider (.NET 8) + FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing)
test doubleshand-written fakes; NSubstitute or Moq when a fake is impractical
HTTPa fake HttpMessageHandler; WebApplicationFactory for ASP.NET Core
coveragedotnet test --collect:"XPlat Code Coverage" (coverlet)

Design for testability — inject TimeProvider

Code that asks a `TimeProvider` for the time can be tested without sleeping.

var clock = new ManualClock(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
var session = new Session(clock, TimeSpan.FromMinutes(30));
Console.WriteLine(session.Expired);
clock.Advance(TimeSpan.FromMinutes(30));       // the test owns time
Console.WriteLine(session.Expired);

class Session(TimeProvider time, TimeSpan ttl)
{
    private readonly DateTimeOffset started = time.GetUtcNow();
    public bool Expired => time.GetUtcNow() - started >= ttl;
}

// FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) does this, plus timers
class ManualClock(DateTimeOffset start) : TimeProvider
{
    private DateTimeOffset now = start;
    public override DateTimeOffset GetUtcNow() => now;
    public void Advance(TimeSpan by) => now += by;
}
Result
False
True
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: What is TimeProvider? — .NET

An xUnit test class

`[Theory]` runs once per `[InlineData]` row; `Assert.Throws` checks the exception type exactly.

using System.Text.RegularExpressions;
using Xunit;

public static class Slug
{
    public static string From(string title)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(title);
        return Regex.Replace(title.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-").Trim('-');
    }
}

public class SlugTests
{
    [Theory]
    [InlineData("  Hello, World!  ", "hello-world")]
    [InlineData("--x--", "x")]
    public void JoinsWordsWithSingleHyphens(string title, string expected) =>
        Assert.Equal(expected, Slug.From(title));

    [Fact]
    public void RejectsBlankTitles() => Assert.Throws<ArgumentException>(() => Slug.From("   "));
}
Result

dotnet test reports 3 tests passed: two InlineData rows and one Fact.

Setup
  1. dotnet new xunit -o Demo.Tests (it references xunit, the test SDK and the runner)
  2. Add the class to the project and run dotnet test Demo.Tests

Excerpt, not compiled or run here: it needs an xUnit test project; its NuGet packages were not available offline on this machine.

  • xunit
  • csharp >=12
  • dotnet >=8
  • xunit >=2.5

Sources: Unit testing C# with xUnit — .NET

Principles worth stating

The same in every language.

  • Fakes you own (an in-memory repository, a TimeProvider) over mocks of code you do not.
  • Pass CancellationToken and TimeProvider in — both make async tests deterministic.
  • Thread.Sleep / Task.Delay in tests, and tests that share static state.
  • ArgumentException.ThrowIfNullOrWhiteSpace (.NET 8) is the one-line guard clause.

C# Language Essentials

Nullable references, records and pattern matching, generics and generic math, exceptions, value vs reference.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET

Sources: A tour of the C# language

Swift idea → C#

Close in shape; C# nullability for references is a compiler analysis, not a type.

SwiftC#Watch out
Optional<T>, ?., ??T?, ?., ??, is { } xfor references T? is an annotation: warnings, not a different type
enum with associated valuesa record hierarchy + switch expressionthe compiler cannot prove a hierarchy closed: keep a _ arm
structstruct, readonly record structlarge structs copy on every assignment
final classsealed classclasses are open by default
protocol + extensioninterface (default members), extension methods—
throwsunchecked exceptionsnothing in a signature says it throws
defertry { } finally { }, usingusing var disposes at the end of the scope

Nullable references

With `<Nullable>enable</Nullable>` (the template's default), dereferencing a maybe-null reference is a warning.

var user = new User("Ana", Address: null);
Console.WriteLine(user.Address?.City ?? "unknown city");
string name = user.Name;                       // non-nullable: no check needed
if (user.Address is { City: { } city }) Console.WriteLine(city);
else Console.WriteLine($"no city for {name}");

record Address(string? City);
record User(string Name, Address? Address);
Result
unknown city
no city for Ana
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Nullable reference types — C#

Records and pattern matching

Type, positional, property and list patterns in a `switch` expression.

using System.Globalization;

static double Area(Shape s) => s switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rect(var w, var h) => w * h,
    _ => throw new ArgumentOutOfRangeException(nameof(s)),
};

Console.WriteLine(Area(new Rect(2, 3)));
Console.WriteLine(Area(new Circle(1)).ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine(new[] { 1, 2, 3 } is [1, .., 3] ? "list pattern matched" : "no match");

abstract record Shape;
record Circle(double Radius) : Shape;
record Rect(double Width, double Height) : Shape;
Result
6
3.14
list pattern matched
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Pattern matching overview — C#

Generics and generic math

Constraints with `where`; static abstract interface members make `T.Zero` and `+` generic (.NET 7).

using System.Numerics;

static T Clamp<T>(T value, T low, T high) where T : IComparable<T> =>
    value.CompareTo(low) < 0 ? low : value.CompareTo(high) > 0 ? high : value;

static T Sum<T>(IEnumerable<T> xs) where T : INumber<T> =>
    xs.Aggregate(T.Zero, (acc, x) => acc + x);

Console.WriteLine($"{Clamp(15, 0, 10)} {Clamp("m", "a", "f")} {Sum(new[] { 1, 2, 3 })} {Sum(new[] { 10L, 20L })}");
Result
10 f 6 30
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Generic math — .NET

Exceptions, Try-methods and filters

`TryParse` for expected failures, `throw` expressions for guards, `when` filters to catch only what you handle.

using System.Globalization;

static int ParsePort(string text) =>
    int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out var n) && n is > 0 and < 65536
        ? n
        : throw new FormatException($"not a port: {text}");

foreach (var input in new[] { "8080", "http", "70000" })
{
    try
    {
        Console.WriteLine($"port {ParsePort(input)}");
    }
    catch (FormatException e) when (e.Message.StartsWith("not a port", StringComparison.Ordinal))
    {
        Console.WriteLine(e.Message);
    }
}
Result
port 8080
not a port: http
not a port: 70000
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Best practices for exceptions — .NET

Value vs reference, and `with`

A struct copies on assignment, a class aliases; `with` makes a shallow non-destructive copy of a record.

var p1 = new PointStruct { X = 1 };
var p2 = p1;                                   // copied
p2.X = 99;
var b1 = new Box { X = 1 };
var b2 = b1;                                   // the same object
b2.X = 99;
var r1 = new Settings("dark", ["x"]);
var r2 = r1 with { Theme = "light" };
Console.WriteLine($"{p1.X} {b1.X} {r1 == (r1 with { })} {ReferenceEquals(r1.Tags, r2.Tags)} {r2.Theme}");

struct PointStruct { public int X; }
class Box { public int X; }
record Settings(string Theme, List<string> Tags);
Result
1 99 True True light
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Shared State — lock, Interlocked, SemaphoreSlim, Channels

Keeping threads and tasks from racing on shared data — none of it checked by the compiler.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (System.Threading)

Sources: Overview of synchronization primitives — .NET

Swift isolation → C#

Swift proves data-race freedom at compile time; C# leaves it to these tools and to discipline.

SwiftC#Difference
actorstate owned by one consumer of a Channel<T>no reentrancy unless you write it
actor-isolated mutationlock (sync code), SemaphoreSlim.WaitAsync (async code)lock cannot contain an await
Sendable—no compiler check: immutable records and ImmutableArray by convention
@MainActorthe UI thread's SynchronizationContext / Dispatchera runtime mechanism
atomic countersInterlocked—

lock and Interlocked

100,000 increments from the thread pool. A plain `count++` would lose updates; both of these do not.

var gate = new Lock();
var guarded = 0;
var atomic = 0;
Parallel.For(0, 100_000, _ =>
{
    lock (gate) { guarded++; }
    Interlocked.Increment(ref atomic);
});
Console.WriteLine($"guarded={guarded} atomic={atomic}");
Result
guarded=100000 atomic=100000
Setup
  1. .NET 9 SDK or later (the Lock type; on .NET 8 lock a new object() instead)
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

  • csharp >=13
  • dotnet >=9

Sources: System.Threading.Lock — .NET API

An async mutex with SemaphoreSlim

A read-modify-write that spans an `await` needs an async-aware lock.

var gate = new SemaphoreSlim(1, 1);
var balance = 0;

async Task Deposit(int amount)
{
    await gate.WaitAsync();
    try
    {
        var read = balance;
        await Task.Yield();                    // an await inside the critical section
        balance = read + amount;
    }
    finally
    {
        gate.Release();
    }
}

await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => Deposit(10)));
Console.WriteLine(balance);
Result
1000
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: SemaphoreSlim — .NET API

One owner behind a Channel

Producers write messages; a single consumer owns the state — the actor shape, without a compiler check.

using System.Threading.Channels;

var inbox = Channel.CreateUnbounded<int>();
var total = 0;
var owner = Task.Run(async () =>
{
    await foreach (var amount in inbox.Reader.ReadAllAsync())
        total += amount;                       // only the owner touches total
});

await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => inbox.Writer.WriteAsync(10).AsTask()));
inbox.Writer.Complete();
await owner;
Console.WriteLine(total);
Result
1000
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: Channels — .NET

Rules

Without a compiler check, the design has to make races impossible.

  • Prefer immutable messages: record types with init properties, ImmutableArray<T>.
  • ConcurrentDictionary for a shared cache; GetOrAdd may run the factory more than once.
  • lock (this) or lock (typeof(X)): anyone can take the same lock. Lock a private object (or a Lock).
  • volatile makes writes visible, not compound updates atomic: x++ still races.

.NET — Files, Text & Atomic I/O

System.IO — small files, atomic replace with a flush to disk, streaming lines, journals, enumerating trees.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (System.IO)

Sources: File and stream I/O — .NET

Read and write a small text file

Whole-file helpers are right for small files; they write UTF-8 without a byte-order mark.

var dir = Directory.CreateTempSubdirectory("demo-");
var file = Path.Combine(dir.FullName, "notes.txt");
await File.WriteAllTextAsync(file, "first line\nsecond line\n");
var lines = await File.ReadAllLinesAsync(file);
Console.WriteLine($"{lines.Length} lines, {new FileInfo(file).Length} bytes");
dir.Delete(recursive: true);
Result
2 lines, 23 bytes
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo — it works in a fresh temporary directory and removes it

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Writing atomically

Write a temporary sibling, `Flush(flushToDisk: true)`, then move it over the target in one rename.

static void WriteAtomically(string path, string text)
{
    var temp = Path.Combine(Path.GetDirectoryName(path)!, $".{Path.GetFileName(path)}.{Guid.NewGuid():N}.tmp");
    using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write))
    using (var writer = new StreamWriter(stream))
    {
        writer.Write(text);
        writer.Flush();
        stream.Flush(flushToDisk: true);       // on the device before the move publishes it
    }
    File.Move(temp, path, overwrite: true);    // a rename within one volume
}

var dir = Directory.CreateTempSubdirectory("atomic-");
var settings = Path.Combine(dir.FullName, "settings.json");
WriteAtomically(settings, """{"theme":"dark"}""");
WriteAtomically(settings, """{"theme":"light"}""");
Console.WriteLine(File.ReadAllText(settings));
dir.Delete(recursive: true);
Result
{"theme":"light"}
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo — it works in a fresh temporary directory and removes it

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: File.Move — .NET API · FileStream.Flush(Boolean) — .NET API

Streaming a large file line by line

`File.ReadLinesAsync` (.NET 7) yields one line at a time; `ReadAllLines` would load them all.

var log = Path.GetTempFileName();
await using (var writer = new StreamWriter(log))
{
    for (var i = 0; i < 10_000; i++)
        await writer.WriteLineAsync($"{(i % 10 == 0 ? "ERROR" : "INFO")} event {i}");
}

var errors = 0;
await foreach (var line in File.ReadLinesAsync(log))
    if (line.StartsWith("ERROR", StringComparison.Ordinal)) errors++;
Console.WriteLine($"{errors} errors");
File.Delete(log);
Result
1000 errors
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo — it works in a fresh temporary directory and removes it

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

An append-only journal, and enumerating a tree

`AppendAllText` never rewrites existing bytes; `EnumerateFiles` is lazy, `GetFiles` is not.

using System.Text.Json;

var dir = Directory.CreateTempSubdirectory("tree-");
Directory.CreateDirectory(Path.Combine(dir.FullName, "src", "app"));
File.WriteAllText(Path.Combine(dir.FullName, "src", "app", "Program.cs"), "");
File.WriteAllText(Path.Combine(dir.FullName, "README.md"), "# demo");

var journal = Path.Combine(dir.FullName, "events.jsonl");
foreach (var e in new[] { "start", "save", "stop" })
    File.AppendAllText(journal, JsonSerializer.Serialize(new { @event = e }) + "\n");

var sources = Directory.EnumerateFiles(dir.FullName, "*.cs", SearchOption.AllDirectories)
    .Select(f => Path.GetRelativePath(dir.FullName, f).Replace('\\', '/'));
Console.WriteLine($"{string.Join(",", sources)} | {File.ReadAllLines(journal).Length} events");
dir.Delete(recursive: true);
Result
src/app/Program.cs | 3 events
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo — it works in a fresh temporary directory and removes it

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Rules that prevent data loss

.NET adds a few platform differences.

  • Dispose every stream (using / await using) — Dispose flushes; a missed one loses the tail.
  • Replace a file with a temporary sibling + move; never truncate the original first.
  • File.Replace(temp, path, backup) also keeps a backup of the old file and the target's metadata; it needs both files on the same volume.
  • Path.GetTempFileName() creates a file in the shared temp folder — do not use it as a sibling for an atomic move to another volume.
  • Paths: Path.Combine and Path.GetRelativePath, never string concatenation with \.

MVVM State — INotifyPropertyChanged & Observable Collections

The change notifications XAML bindings listen to, written by hand and with the MVVM Toolkit's source generators.

  • dotnet
  • csharp >=12
  • dotnet >=8
  • .NET (System.ComponentModel); bound from WinUI, WPF or .NET MAUI

Sources: How to implement property change notification — WPF · Introduction to the MVVM Toolkit — .NET Community Toolkit

SwiftUI → .NET MVVM

The view model is the observable object; the XAML binding is the view's subscription.

SwiftUI.NET MVVM
@Observable / ObservableObjectINotifyPropertyChanged (or the Toolkit's ObservableObject)
@Published propertya property that raises PropertyChanged
a button actionICommand (the Toolkit's [RelayCommand])
ForEach over an arrayObservableCollection<T> raising CollectionChanged
@Binding{Binding Path, Mode=TwoWay} in XAML

INotifyPropertyChanged by hand

Raise the event for the property and for anything derived from it; skip equal values.

using System.ComponentModel;
using System.Runtime.CompilerServices;

var vm = new CounterViewModel();
vm.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed");   // what a binding listens to
vm.Increment();
vm.Count = 1;                                  // the same value: no notification
Console.WriteLine(vm.Label);

class CounterViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private int count;

    public int Count
    {
        get => count;
        set
        {
            if (count == value) return;
            count = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(Label));
        }
    }

    public string Label => $"Clicked {Count} times";
    public void Increment() => Count++;

    private void OnPropertyChanged([CallerMemberName] string? name = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Result
Count changed
Label changed
Clicked 1 times
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo (the binding engine itself needs a XAML UI)

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

ObservableCollection

A list control re-renders from `CollectionChanged`; replacing the whole list would rebuild every row.

using System.Collections.ObjectModel;

var items = new ObservableCollection<string>();
items.CollectionChanged += (_, e) =>
    Console.WriteLine($"{e.Action} {string.Join(",", (e.NewItems ?? e.OldItems)!.Cast<string>())}");
items.Add("milk");
items.Add("eggs");
items.RemoveAt(0);
Result
Add milk
Add eggs
Remove milk
Setup
  1. .NET 8 SDK or later
  2. dotnet new console -o Demo, replace Demo/Program.cs with the example, then dotnet run --project Demo (the binding engine itself needs a XAML UI)

Ran with .NET SDK 9.0.304 (net9.0, C# 13) on 2026-11-11: the output matched the result shown.

Sources: ObservableCollection<T> — .NET API

The same view model with the MVVM Toolkit

Source generators write the property, the notifications and the command from attributes.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class CounterViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Label))]
    private int count;                         // generates Count, with change notification

    public string Label => $"Clicked {Count} times";

    [RelayCommand]
    private void Increment() => Count++;       // generates IncrementCommand
}
Result

The label reads "Clicked 0 times" and counts up each time the bound button is pressed.

Setup
  1. A WinUI, WPF or .NET MAUI app project with the CommunityToolkit.Mvvm NuGet package (8.x)
  2. Bind it from XAML: <TextBlock Text="{Binding Label}"/> and <Button Command="{Binding IncrementCommand}"/>

Excerpt, not compiled or run here: it needs a XAML app project referencing CommunityToolkit.Mvvm (its source generators run at build time).

  • CommunityToolkit.Mvvm
  • csharp >=12
  • dotnet >=8
  • communitytoolkit-mvvm >=8.0

Rules

Most binding bugs are one of these.

  • Raising PropertyChanged off the UI thread in WPF or WinUI — marshal to the dispatcher first.
  • Replacing an ObservableCollection instance without raising PropertyChanged for it: the bound list keeps the old one.
  • Derived properties raise their own notification when a source changes (NotifyPropertyChangedFor).
  • View models stay UI-free, so they run and are tested in a plain console or test project — as above.

Coverage against the Swift Catalog

Every Swift topic, and what this catalog has for it — an equivalent, a partial counterpart, or the reason there is none.

How to read the matrix

Nothing here is a translation made to fill a gap.

  • equivalent — a sheet here covers the same concept and shares its concept id, so the language selector lands on it from the Swift sheet.
  • partial — a sheet here covers part of it under its own concept id, so the selector does not land on it: the Here column names it and the reason says what is missing.
  • no counterpart — nothing here; the reason says whether the topic does not exist in this language or is simply not written yet.

Swift topic by topic

In the Swift catalog's reading order.

Swift topicDispositionHereWhy
SwiftUI — State & Data FlowpartialMVVM State — INotifyPropertyChanged & Observable Collectionsthe observable view model and collection a XAML binding watches; no view-owned state wrappers like @State
SwiftUI — Views, Layout & Animationno counterpart—XAML layout (WinUI, WPF, MAUI) is not written up yet
SwiftUI vs UIKitno counterpart—a comparison of two Apple UI frameworks
TCA — Core Loopno counterpart—The Composable Architecture is a Swift library; not covered
TCA — Composition, Dependencies, Testingno counterpart—a Swift library's API
TCA + SwiftUI — Navigation & UIno counterpart—a Swift library's API on an Apple UI framework
Modern Swift Concurrency — CoreequivalentAsync/Await, Tasks & Cancellationtasks, WhenAll, cancellation tokens, async streams and callback bridging
Actors, Sendable & IsolationpartialShared State — lock, Interlocked, SemaphoreSlim, Channelslocks, an async mutex and a channel-owned state; C# has no compiler-checked isolation or Sendable
Swift 6 Concurrency — Do's & Don'tsno counterpart—about migrating to Swift 6's strict concurrency checking
Multithreading — GCD vs Combine vs async/awaitpartialAsync/Await, Tasks & CancellationTask-based async only; the Thread / ThreadPool / BackgroundWorker comparison is not drawn
Combine — CorepartialAsync/Await, Tasks & CancellationIAsyncEnumerable covers pull-based streams; Rx.NET (System.Reactive), the push-based counterpart, is not in the BCL and not covered
Timers & Async Patterns — Combine vs async/awaitpartialAsync/Await, Tasks & Cancellationdelays, timeouts, polling and fan-out are covered; there is no Combine side to compare
SwiftUI + Combine — Wiring Them TogetherpartialMVVM State — INotifyPropertyChanged & Observable Collectionschange notifications are the view-model → view connection point; no reactive library is involved
Timing Operators — Combine vs Swift ConcurrencypartialTesting Approaches in .NETinjectable time with TimeProvider; no timing operators are catalogued
Clean Code in Swiftno counterpart—not written yet; the principles are language-neutral and the Swift sheet's advice carries over
SOLID in Swiftno counterpart—not written yet; the principles are language-neutral
Functional Programming in SwiftpartialCollections — LINQ CookbookLINQ's higher-order operators and deferred execution; no composition, currying or modelling card
Swift Language EssentialsequivalentC# Language Essentialsthe same questions — optionals, enums, generics, errors, value vs reference, pattern matching — answered for C#
Collections — Types & ProtocolsequivalentCollections — List, Dictionary, HashSet & Friendswhich container and what it costs
Collections — Operation CookbookequivalentCollections — LINQ Cookbookfilter, project, fold, group, sort, chunk, zip
Swift Engineering Best Practicesno counterpart—not written yet
Architectural Approachesno counterpart—not written yet
Modularization Approachesno counterpart—not written yet; projects, assemblies and internal are a different mechanism from Swift packages
Testing Approaches on iOSequivalentTesting Approaches in .NETtooling, injectable time and an xUnit class
AI Workflows for Swift (Cursor + Claude)no counterpart—Swift-specific by design: it is about why Swift is harder for models and wiring Xcode to an agent
Algorithmic Patterns & ComplexityequivalentAlgorithmic Patterns & Complexitybuilt-in costs and five core patterns; fewer patterns than the Swift sheet
Streaming & Time-Series Patternsno counterpart—not written yet; the algorithms (ring buffers, sketches, reservoir sampling) are language-neutral
Files, Text & Atomic I/Oequivalent.NET — Files, Text & Atomic I/Othe same file-safety questions on System.IO
Core Bluetooth (BLE)no counterpart—an Apple framework; Windows has Windows.Devices.Bluetooth, not written up yet