
What’s New in C# 13 – Explore the Latest Features
C# continues to evolve with every release, bringing developers new language improvements to write cleaner, safer, and more expressive code. With C# 13 (part of .NET 9), we get a mix of features that improve developer productivity, add flexibility, and enhance existing capabilities.
In this article, we’ll explore all the new features in C# 13, explain them in detail, and walk through code examples to understand how they work.
Overview of C# 13 Features
Here’s a quick list of what’s new in C# 13:
params
Collections- New Lock Object
- Partial Properties and Indexers
- New Escape Sequence
- Implicit Index Access
ref
andunsafe
in Iterators andasync
Methodsallows ref struct
in Genericsref struct
Interfaces
Now let’s dive into each feature one by one.
params
Collections
In previous versions of C#, params
allowed passing a variable number of arguments, but only as arrays. With C# 13, params
now supports collections like List<T>
and Span<T>
.
This means we can write APIs that are more efficient and flexible:
DisplayItems(1, 2, 3, 4, 5);
DisplayItems("Apple", "Banana", "Orange");
void DisplayItems<T>(params List<T> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}
No need to manually create a list, the compiler handles it for us. This makes method signatures more natural and expressive.
New Lock Object
The .NET 9 runtime introduces a new synchronization type called System.Threading.Lock
. This type provides a more modern and efficient way to handle thread synchronization compared to the traditional Monitor
-based approach.
When you call Lock.EnterScope()
, it creates an exclusive scope, and the returned ref struct
follows the Dispose
pattern, meaning the lock is automatically released at the end of the scope.
In C# 13, the lock
statement is smart enough to detect when you’re working with a Lock
object. In that case, it uses the new System.Threading.Lock
APIs behind the scenes instead of falling back to Monitor
. If you convert the Lock
to another type, then the compiler switches back to the old Monitor
implementation.
Before C# 13:
public class MyClass
{
private object _lockObj = new();
public void DoWork()
{
lock (_lockObj)
{
// Critical section
}
}
}
With C# 13:
using System.Threading;
public class MyClass
{
// Just change the type from object to Lock
private Lock _lockObj = new();
public void DoWork()
{
lock (_lockObj)
{
// Critical section
}
}
}
The only change is the type of the lock object. The rest of your code remains the same, but now it benefits from the modern synchronization improvements provided by System.Threading.Lock
.
Partial Properties and Indexers
We’re already familiar with partial
classes and methods, which allow splitting implementation across multiple files.
With C# 13, this flexibility is extended to partial
properties and partial
indexers. This means you can split the declaration and implementation of a property or indexer across different parts of a class, much like partial methods.
Here’s what you need to know:
- A declaring declaration defines the property or indexer without providing a body.
- An implementing declaration provides the actual logic.
- The signatures of both declarations must match exactly.
- Auto-properties can’t be used as the implementing declaration, you must provide a body.
- Properties or indexers that don’t declare a body are treated as the declaring declaration
public partial class Customer
{
// Declaring declaration
public partial string FullName { get; }
}
public partial class Customer
{
private string _firstName = "John";
private string _lastName = "Doe";
// Implementing declaration
public partial string FullName => $"{_firstName} {_lastName}";
}
New Escape Sequence
C# 13 adds a dedicated escape sequence for the ESC (Escape) character. Until now, developers had to write it using the Unicode form \u001b
, which wasn’t very readable.
With the new \e
escape sequence, writing console applications that interact with VT100/ANSI terminal codes becomes simpler and more intuitive.
For example:
// Before C# 13
Console.WriteLine("\u001b[31mHello, World!\u001b[0m");
// With C# 13
Console.WriteLine("\e[31mHello, World!\e[0m");
Implicit Index Access
C# 13 now allows the “from the end” index operator (^
) inside object initializers for single-dimension arrays or collections.
This is especially useful when you want to set values starting from the end of a collection, such as preparing buffers, countdowns, or recent logs.
Before C# 13
Previously, you couldn’t use ^
in an object initializer, so you had to assign by forward indexes:
public class LogBuffer
{
public string[] Messages { get; set; } = new string[5];
}
LogBuffer logs = new()
{
Messages =
{
[0] = "Trace: App started",
[1] = "Debug: Cleanup started",
[2] = "Info: Scheduled backup completed",
[3] = "Warning: High memory usage",
[4] = "Critical: Disk full"
}
};
With C# 13
LogBuffer logs = new()
{
Messages =
{
[^1] = "Critical: Disk full",
[^2] = "Warning: High memory usage",
[^3] = "Info: Scheduled backup completed",
[^4] = "Debug: Cleanup started",
[^5] = "Trace: App started"
}
};
Here, the most recent log (Critical: Disk full
) is placed at the end of the array, while older logs are positioned relative to it.
With C# 13, the intent is clearer, you’re directly saying “this goes at the end, that goes before it,” which is much more natural for certain data patterns.
ref
and unsafe
in Iterators and async
Methods
Before C# 13, developers faced strict limitations when working with ref
locals and ref struct
types inside async
or iterator methods. Declaring them simply wasn’t allowed, and iterator methods couldn’t contain unsafe code at all.
With C# 13, these restrictions are relaxed. Async methods can now declare ref
locals and ref struct
locals, provided they don’t cross an await
boundary. Similarly, iterator methods can take advantage of unsafe contexts, though all yield return
and yield break
statements must still remain in safe code.
These changes make it possible to use performance-focused types like Span<T>
and ReadOnlySpan<T>
in more scenarios, without compromising compiler-enforced safety.
allows ref struct
in Generics
Before C# 13, ref struct
types couldn’t be used as type parameters in generic types or methods. This was a limitation that made it harder to write generic algorithms that work with types like Span<T>
and ReadOnlySpan<T>
.
C# 13 solves this problem with a new anti-constraint called allows ref struct
. This constraint explicitly states that a type parameter can be a ref struct
.
The compiler still enforces all ref-safety rules, ensuring scoped lifetime checks remain valid.
public class Processor<T> where T : allows ref struct
{
// Use T as a ref struct:
public void Process(scoped T item)
{
// item must follow ref safety rules
}
}
Now, you can safely use ref struct
types in generic algorithms:
Span<int> span = [ 1, 2, 3 ];
Processor<Span<int>> processor = new();
processor.Process(span);
This feature enables more reusable and type-safe generic code, particularly when working with performance-oriented APIs that rely on Span<T>
or similar ref struct
types.
ref struct
Interfaces
In earlier versions of C#, ref struct
types couldn’t implement interfaces. That limitation is lifted in C# 13, allowing ref struct
types to declare that they implement an interface.
However, to maintain ref safety rules, there are a few important restrictions:
- A
ref struct
can’t be converted to an interface type, because such a conversion would require boxing, which could break ref safety. - Explicit interface implementations inside a
ref struct
can only be accessed through a type parameter that’s declared withallows ref struc
t. - As with normal structs and classes, all interface members must be implemented — including those with default implementations.
public interface IFormatter
{
void Format();
}
public ref struct LogFormatter : IFormatter
{
public void Format()
{
Console.WriteLine("Formatting log output...");
}
}
LogFormatter formatter = new();
formatter.Format();
Summary
C# 13 isn’t a revolutionary update, but it delivers meaningful improvements that:
- Simplify everyday coding (params collections, implicit index access).
- Modernize synchronization (lock object).
- Improve code generation scenarios (partial properties).
- Expand
ref struct
usability across generics, async/iterators, and interfaces. - Add polish to language syntax (escape sequence).
C# continues to move forward step by step, and this release is no exception. Stay tuned — C# 14 and .NET 10 are already on the horizon with even more exciting updates.
Takeaways
C# 13 shows how the language continues to balance productivity with safety. Developers get less boilerplate through params collections, implicit index access, and the new \e
escape sequence. At the same time, partial properties and indexers give more flexibility in designing and evolving APIs. Perhaps most importantly, ref struct
support has grown significantly from generics to async
and iterator methods, and now even interfaces, enabling advanced performance-oriented scenarios while maintaining compiler-enforced safety rules.
Overall, this release may feel incremental, but these refinements add up to a more powerful, developer-friendly language. And with C# 14 and .NET 10 on the way, it’s clear that the evolution of the language is far from slowing down.