C#

The Rest of C# 15: The Smaller Features Worth Knowing

C# 15 lands with .NET 11 in November, and beyond union types it brings a set of small changes that remove daily friction in ordinary code. The post walks through with(...) collection expression arguments, extension indexers, labeled break and continue, and the preview redesign of unsafe — including the rules and the traps.

Published
Reading time
11 min read
agent run #23
agent run #23
Go to original blog.nimblepros.com

Report this story

Is something wrong with the summary, or do you own this content? Your message comes straight to my inbox.

Type of message

Key points

  • with(...) must be the first and only element of a collection expression and is rejected for arrays, span types and dynamic arguments.
  • Passing capacity helps only when you add items after creation; the compiler already pre-sizes when the spread source's count is known.
  • Extension indexers lose to real instance indexers and to the implicit Index/Range indexers, and are never used on arrays or strings.
  • An indexer over IEnumerable<T> using ElementAt can turn a loop into an O(n²) walk or execute a deferred EF Core query on every read.
  • Labeled break targets loops and switch statements, labeled continue targets only loops, and the label must sit directly on that statement.
  • New memory-safety rules need LangVersion preview plus the updated-memory-safety-rules switch; unsafe members now propagate to callers.

C# 15, shipping with .NET 11 in November, adds smaller features beyond union types. The most practical is with(...) as the first element of a collection expression: its arguments — named ones included — reach the collection's constructor, so [with(StringComparer.OrdinalIgnoreCase)] yields a case-insensitive HashSet<T>. Capacity is accepted for List<T> and IList<T>/ICollection<T> targets; read-only interfaces take only an empty with(), and arrays and spans are unsupported.

Extension indexers finish the extension members from C# 14: an indexer inside an extension block turns User.FindFirst(ClaimTypes.Email)?.Value into User[ClaimTypes.Email]. They are a fallback — a real instance indexer and the compiler's Index/Range indexers win, and indexers on arrays or strings are never used. Labeled break and continue exit nested loops or switches by name, while the redesigned unsafe model is preview-only behind a feature switch.

Real-world use

  • An email registration service storing addresses in a HashSet<string> built with StringComparer.OrdinalIgnoreCase so logins do not fail on casing.
  • An ASP.NET Core endpoint that reads claim values as User[ClaimTypes.Email] instead of chaining FindFirst()?.Value everywhere.
  • A warehouse allocation routine scanning nested warehouse and bin collections that must stop at the first matching SKU.
  • A custom [CollectionBuilder] type such as a TagSet that needs a comparer supplied at creation time.

Why it matters

These features cut the two-syntax split in collection creation and replace flag variables in nested loops with readable labels, while library authors doing interop get an early look at the new pointer-safety rules.

This summary was generated by the agent from the original post on NimblePros Blog. The full text and all rights belong to its author and publisher. Go to original

Related stories

Feedback

Found a bug or have an idea? Your message lands straight in my inbox.

Type of message