
Learn all C# 13 new features with runnable code examples — params collections, the Lock type, partial properties and more. Master C# 13 today.
C# 13 shipped alongside .NET 9, and by 2026 it has become the version most production teams are writing every day. If you're searching for C# 13 new features, this guide walks through every major addition — params collections, the new System.Threading.Lock type, partial properties, implicit index access, and more — with runnable code examples and, just as importantly, an explanation of why each feature exists and when you should reach for it.
Whether you're a beginner wondering how to use the new syntax, an intermediate developer looking for best practices, or a senior engineer evaluating advanced ref struct capabilities, there's something here for you. Let's dig in.
What's New in C# 13? A Quick Overview
Here's the full list of C# 13 new features at a glance:
- params collections —
paramsnow works withSpan<T>,IEnumerable<T>, lists, and any collection type, not just arrays - New
System.Threading.Locktype — a dedicated, faster lock object that replaces locking on plainobject - Partial properties and indexers — huge for source generators
- Implicit index access (
^) in object initializers - Overload resolution priority — library authors can steer the compiler toward better overloads
refandunsafein iterators and async methodsref structtypes can implement interfaces and be used as generic type arguments viaallows ref struct- The
\eescape sequence for the ESC character - Better natural type for method groups
You need the .NET 9 SDK (or newer) and <LangVersion>13</LangVersion> — or simply <TargetFramework>net9.0</TargetFramework>, which enables C# 13 by default. Now let's look at each feature in depth.
C# 13 New Features Explained with Code Examples
1. params Collections — No More Array Allocations
For two decades, params only worked with arrays. Every call to a params method silently allocated an array on the heap — even for two or three arguments. C# 13 fixes this by allowing params on any collection type, most importantly ReadOnlySpan<T>:
// C# 12 and earlier — heap allocation on every call
public static int SumOld(params int[] numbers)
{
int total = 0;
foreach (var n in numbers) total += n;
return total;
}
// C# 13 — the compiler can place arguments on the stack. Zero heap allocation.
public static int Sum(params ReadOnlySpan<int> numbers)
{
int total = 0;
foreach (var n in numbers) total += n;
return total;
}
// Call site looks identical
int result = Sum(1, 2, 3, 4, 5); // no array allocated
It also works with interfaces and concrete collections:
public static void PrintAll(params IEnumerable<string> items)
{
foreach (var item in items)
Console.WriteLine(item);
}
public static List<T> MakeList<T>(params List<T> items) => items;
PrintAll("apple", "banana", "cherry");
Why it matters: in hot paths — logging, string formatting, math helpers — the hidden array allocations from old-style params add measurable GC pressure. With params ReadOnlySpan<T>, the compiler stack-allocates the arguments. When both a span overload and an array overload exist, the compiler prefers the span, so libraries like String.Concat got faster in .NET 9 without anyone changing their calling code.
Best practice: prefer params ReadOnlySpan<T> for new APIs unless you need to store the collection, in which case take params IEnumerable<T> or an array.
2. The New System.Threading.Lock Type
This is one of the most searched C# 13 new features, and for good reason. Since C# 1.0, developers have locked on plain object instances. C# 13 introduces a dedicated Lock type that is both faster and safer:
public class OrderProcessor
{
private readonly Lock _lock = new();
private int _processedCount;
public void Process()
{
lock (_lock) // compiler emits Lock.EnterScope(), not Monitor.Enter()
{
_processedCount++;
// critical section
}
}
public void ProcessManually()
{
// Equivalent explicit form — useful when you need finer control
using (_lock.EnterScope())
{
_processedCount++;
}
}
}
Why it matters: when the compiler sees lock on a Lock instance, it emits calls to Lock.EnterScope() instead of Monitor.Enter/Monitor.Exit. The new type avoids the object-header/sync-block machinery of Monitor, which makes it measurably faster under contention. It also communicates intent: a field of type Lock is unambiguously a lock, not a random object someone might reuse.
Common pitfall: don't box a Lock by assigning it to an object variable and locking on that — you'd silently fall back to Monitor and end up with two different synchronization mechanisms guarding the same state. The compiler warns about this (CS9216); treat that warning as an error.
3. Partial Properties and Indexers
C# 3 gave us partial methods; C# 13 extends the idea to properties and indexers. The killer use case is source generators:
// Your file
public partial class ViewModel
{
[ObservableProperty]
public partial string UserName { get; set; }
}
// Generated file (e.g. by the MVVM Toolkit)
public partial class ViewModel
{
private string _userName = string.Empty;
public partial string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
OnPropertyChanged(nameof(UserName));
}
}
}
}
Why it matters: before C# 13, source generators had to invent naming conventions — you'd write a field named _userName and the generator produced a property named UserName. That broke navigation, refactoring, and discoverability. With partial properties, you declare the actual property and the generator fills in the implementation. Regex source generators, MVVM frameworks, and logging generators all adopted this pattern quickly.
4. Implicit Index Access in Object Initializers
The "from the end" index operator ^ now works inside object initializers:
public class Countdown
{
public int[] Values { get; set; } = new int[10];
}
var countdown = new Countdown
{
Values =
{
[^1] = 0, // last element
[^2] = 1,
[^3] = 2
}
};
// countdown.Values = [0, 0, 0, 0, 0, 0, 0, 2, 1, 0]
A small feature, but it removes an inconsistency: ^ worked everywhere except initializers. Now the language is uniform.
5. Overload Resolution Priority (Advanced)
Library authors can now tell the compiler which overload to prefer when several are applicable, using [OverloadResolutionPriority]:
using System.Runtime.CompilerServices;
public static class Logger
{
[OverloadResolutionPriority(1)]
public static void Log(ReadOnlySpan<char> message)
=> Console.Out.Write(message); // fast path, preferred
public static void Log(string message)
=> Console.Out.Write(message); // kept for binary compatibility
}
Why it matters: this lets libraries add faster overloads without breaking existing compiled code or causing ambiguity errors. The .NET base class library uses it extensively. Best practice for application developers: you almost never need this in app code — it's a library-author tool. Overusing it makes overload resolution harder to reason about.
6. ref and unsafe in Iterators and Async Methods
Before C# 13, you couldn't declare a ref local or use a ref struct like Span<T> anywhere inside an async method or iterator. C# 13 relaxes this: you can use them as long as they don't live across an await or yield return boundary:
public async Task<int> CountDigitsAsync(string input)
{
// Legal in C# 13: Span usage before the await, not across it
ReadOnlySpan<char> span = input.AsSpan();
int digits = 0;
foreach (var c in span)
if (char.IsDigit(c)) digits++;
await Task.Delay(10); // span is not used after this point
return digits;
}
Why it matters: previously you had to extract span-based logic into a separate synchronous helper method purely to satisfy the compiler. Now high-performance parsing code and async I/O can coexist in one method, with the compiler still guaranteeing that stack-only types never survive a suspension point.
7. ref struct Interfaces and allows ref struct (Advanced C#)
C# 13 lets ref struct types implement interfaces, and lets generic parameters opt in to accepting them:
public interface IWriter
{
void Write(ReadOnlySpan<char> text);
}
// A ref struct can now implement an interface
public ref struct BufferWriter : IWriter
{
private Span<char> _buffer;
private int _position;
public BufferWriter(Span<char> buffer) => _buffer = buffer;
public void Write(ReadOnlySpan<char> text)
{
text.CopyTo(_buffer[_position..]);
_position += text.Length;
}
}
// Generic code opts in with 'allows ref struct'
public static void WriteGreeting<T>(T writer) where T : IWriter, allows ref struct
{
writer.Write("Hello, C# 13!");
}
The critical pitfall: you still cannot box a ref struct. Casting BufferWriter to IWriter directly is a compile error — the interface can only be used as a generic constraint. This preserves the stack-only safety guarantee while finally letting Span-based types participate in generic abstractions. This feature is why .NET 9+ APIs like TensorPrimitives and the LINQ-like span extensions could be built at all.
8. Smaller Quality-of-Life Improvements
\eescape sequence: write"\e[1;31mRed text\e[0m"for ANSI terminal colors instead of the awkward"\u001b"or"\x1b"(the latter of which has a nasty parsing gotcha when followed by hex digits).- Method group natural type: the compiler now prunes inapplicable candidate methods scope-by-scope when inferring a delegate type for a method group, making more code with
var handler = SomeMethod;just work — and compile faster.
C# 13 Best Practices and Common Pitfalls
- Migrate hot-path
paramsAPIs toReadOnlySpan<T>. It's binary-compatible to add the overload alongside the array version, and callers get the faster path on recompile. - Replace
private readonly object _lock = new();withprivate readonly Lock _lock = new();in new code. Don't mix the two mechanisms on the same state. - Don't sprinkle
[OverloadResolutionPriority]in application code. It exists for library evolution scenarios. - Watch
ref structboxing errors. If you see CS0029 converting aref structto an interface, you need a generic method withallows ref struct, not a cast. - Remember the async boundary rule.
Span<T>in async methods is fine between awaits, never across them — the compiler enforces this, but designing with it in mind saves refactoring. - Note on the
fieldkeyword: it was preview-only in C# 13 and became a full feature in C# 14 (.NET 10). If you're on .NET 9 with LangVersion 13, don't rely on it in production code.
Conclusion: Why C# 13 New Features Matter in 2026
The theme running through all the C# 13 new features is performance without ceremony. params collections remove hidden allocations, the Lock type speeds up the most common synchronization pattern in the language, and the ref struct improvements let high-performance span-based code finally participate in generics, iterators, and async methods. Meanwhile, partial properties quietly transformed the entire source-generator ecosystem.
Key takeaways:
- Use
params ReadOnlySpan<T>for allocation-free variadic APIs. - Adopt
System.Threading.Lockfor new synchronization code — it's faster and clearer. - Partial properties make source generators first-class; expect your MVVM and Regex code to use them.
allows ref structandref structinterfaces unlock generic abstractions overSpan<T>.refandunsafenow work in async methods and iterators, as long as they don't cross suspension points.
If you're targeting .NET 9 or later, every one of these features is available today. Pick one — the Lock type is the easiest win — and start using it in your next pull request. And when you're ready to go further, check out our other C# tutorials on csharp-coder.com covering C# 14, .NET 10, and modern high-performance patterns.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment