C# 13 Features: What's New and How to Use It
This article is featured on C# Advent Calendar 2024. Thank you Matt for putting this together year after year.
C# has always been one of the most popular programming languages among developers. It continuously evolves to meet timed features and trends. Its robustness and flexibility make it an all-purpose language and ideal for domains like desktop applications, enterprise systems, web development, games, and cross-platform and native mobile applications. With the launch of .NET 9, Microsoft introduced C# 13, equipped with new features to improve developer productivity and code quality. In this blog, I will explore some of the important features introduced in C# 13.
Enhanced params scope
C# params keyword allows you to pass an arbitrary number of arguments as the method parameter. It has been a useful feature in C# arrays where you don't need a fixed number of arguments and want it to keep flexible.
Code example
public void PrintNumbers(params int[] numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
// Usage
PrintNumbers(1, 2, 3); // Passing multiple arguments
PrintNumbers(new int[] { 4, 5 }); // Passing an array
PrintNumbers(); // Passing no arguments
However, params were only available with arrays. Now, C# 13 brings this flexibility to other collection types System.Span<T>
, System.ReadOnlySpan<T>
, and collections implementing System.Collections.Generic.IEnumerable<T>
. Let's light up on each example.
Params support with System.Span<T>
Span<T>
utilizes an in-place collection that modifies the original item without copying it in an intermediate location. It is a memory-efficient and type-safe solution optimal for many scenarios. You can now pass flexible params with this type as well.
Code example
public static void DoubleNumbers(Span<int> numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2; // Modify in-place
}
foreach (var number in numbers)
{
Console.WriteLine(number); // Output modified numbers
}
}
// Usage
Span<int> numbers = stackalloc int[5] { 2, 4, 6, 8, 10 };
DoubleNumbers(numbers);
stackalloc
allocates a contiguous memory stack for the collection. The memory is reclaimed When the method exits.
Params support with System.ReadOnlySpan<T>
ReadOnlySpan<T>
collections are stack-allocated read-only that reduce garbage collection pressure and is ideal for high-performance scenarios. Now, you can use the params flexibility approach with ReadOnlySpan<T>
. Real-time applications and games prefer this collection where high-speed operations are concerned. You can use
Code example
public static void PrintScores(ReadOnlySpan<int> scores)
{
foreach (var score in scores)
{
Console.WriteLine(score); // Process scores without allocations
}
}
// Usage
int[] scoresArray = { 80, 85, 88 }; // Passing ReadOnlySpan<int> directly PrintScores(scoresArray);
As discussed earlier, this ReadOnlySpan
is based on stack allocations and avoids heap allocations. Hence, it reduces garbage collection and performs fast.
Params support with IEnumerable<T>
IEnumerable has a range of implementations in important and most common types such as List<T>
and Array
. The IEnumerable
is popular because of its LINQ support and use with EF Core. They allow various shorthand operations to your collections. So, adding params in IEnumerable
synchronized is a breakthrough for all .NET developers. Let's consider a simple example of how you can leverage this new feature in IEnumerable
.
Code example
// Defining class
class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
static void PrintItems(params IEnumerable<Person>[] collections)
{
foreach (var collection in collections)
{
foreach (var item in collection)
{
// Process items from collections
Console.WriteLine(item.Name);
}
}
}
// Using params with IEnumerable<T>
PrintItems(new List<Person> {
new Person(){
Id = 1, Name="Head"
},
new Person(){
Id = 2, Name="Trevis"
}
});
Output
Advantages of enhancing params scope
Span<T>
, ReadOnlySpan<T>
and IEnumerable<T>
are preferred types for certain scenarios. The addition of flexible params
brings more flexibility to them. You can leverage the performance efficiency of Span<T>
and ReadOnlySpan<T>
with params by giving arbitrary arguments in methods. Those methods will be concising code of multiple methods. Similarly, params help you with IEnumerable
operations, which you may use with database applications.
New lock type and semantics
System.Threading.Lock
is a new lock type for synchronized operations. The new type provides a concise thread-safe code with a more structured approach. Unlike its counterpart System.Threading.Monitor
, it utilizes C# renowned resource-efficient using block. While Monitor
effectively locks, it requires more manual structuring, such as Enter
and Exit
methods. Missing any of them can lead to deadlock. The new Lock
offers a simplified API that simplifies implementation and improves code readability—the Lock.EnterScope()
using block will automatically Exit the lock when the block ends. Its ref struct
supports Dispose()
and allows it to do so. The compiler itself identifies if the lock is a Lock object. It uses the updated API.
Consider the example with Monitor
:
using System;
using System.Threading;
public class MonitorExample
{
private static readonly object _lockObject = new object();
private static int sharedResource = 0;
public static void IncrementResource()
{
Monitor.Enter(_lockObject); // Manually entering lock scope
try
{
for (int i = 0; i < 5; i++)
{
int temp = sharedResource;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} reads value: {temp}");
sharedResource = temp + 1;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} updates value to: {sharedResource}");
}
}
finally
{
Monitor.Exit(_lockObject); // Explicitly exiting lock scope
}
}
}
With Lock
you can do this:
using System;
using System.Threading;
public class LockExample
{
private static Lock myLock = new Lock();
private static int sharedResource = 0;
public static void IncrementResource()
{
using (myLock.EnterScope()) // Automatically enters and exits lock scope
{
for (int i = 0; i < 5; i++)
{
int temp = sharedResource;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} reads value: {temp}");
sharedResource = temp + 1;
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} updates value to: {sharedResource}");
}
}
}
}
In the above code, you can observe the conciseness and better maintainability in the later part.
New escape sequence for ESCAPE character
C# 13 introduces a special escape sequence \e
for the ESCAPE (Unicode U+001B) character. Previously, you had to use its unicode or hexadecimal \u001B
or \x1B
, respectively. They were prone to error and less readable. Hence C# introduces a designated escape sequence for this character.
Before C#13
char escapeChar1 = '\u001B'; // Using Unicode escape sequence.
char escapeChar2 = '\x1B'; // Using hexadecimal escape sequence.
In C#13
char escapeChar = '\e'; // Using the new escape sequence.
Console.WriteLine("a" + escapeChar + "b");
Output
Advantages of the new escape sequence
Writing unicode or hexadecimal for an escape sequence not only reduces code readability but also may lead to errors when interpreting hexadecimal. It also aligns with other C# escape sequences like \n
and \t
.
Implicit index access
Another novelty in C# is the implicit index in arrays, lists, or other indexable collections. With index, you can initialize these collections in reverse order. ^
(hat) operator in the index of the elements. Previously, to initialize values at the end of an array, you had to calculate positions manually from the start. You may find it confusing in scenarios for reverse or complex initializations. Initializing values from the end, as in countdown sequences, was more verbose and required extra tracking.
Before C#13, it was like:
var countdown = new int[10]
{
9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
Now, with the new update:
var countdown = new int[10]; // Initialize the array first
countdown[^1] = 0; //last value
countdown[^2] = 1; //second last value
countdown[^3] = 2;
countdown[^4] = 3;
countdown[^5] = 4;
countdown[^6] = 5;
countdown[^7] = 6;
countdown[^8] = 7;
countdown[^9] = 8;
countdown[^10] = 9; //first value
foreach (var num in countdown)
{
Console.WriteLine(num);
}
Output
We used the ^
index for initializing elements. ^1
represents the last element or element at the last index, and ^n
is the first one, where n is the size of the collection. In our case, it was 10.
Advantages of implicit index access
New indexing makes code more readable. Also, it eliminates the work to calculate and use reverse indexing. You can define it with ^1
as the last index, ^2
as the second last, and so on. You get rid of any intermediate steps for finding the index of the last element and defining indexes for each element.
Method group natural type
Another improvement in C# made is an optimisation of overload resolution in the method group. A method group is a collection of methods with the same name but different parameters or overloads. In such cases, the C# compiler assigns the most suitable overload to that group called the natural type. It is chosen based on the context and parameter matching in which the method group contains and is utilized when an overload is called. Traditionally, the selection of natural type was determined from the full set of candidate methods.
In C# 13, the compiler prunes methods that are evidently unsuitable. Overloads with wrong arity or dissatisfied constraints are eliminated from the natural type race. Hence, early filtering optimizes the time and processes of natural type selection, leading to overall performance improvement. This update will ease the application's higher reliance on overloads.
More partial members - properties and indexers
C# allows users to define a class in multiple locations by defining it as a partial class. This feature has been fruitful for developers for years. You can work in the same class along with your team simultaneously on different files. Now this powerful feature is introduced in properties and indexers. Similar to the class, you can create one declaring declaration and one implementing declaration. The signatures of the two declarations must match. The class should also be partial. you can't use an auto-property declaration for a partial property. Properties that don't declare a body are considered the declaring declaration.
Code example for partial property
// File1.cs
public partial class A
{
// Declaring declaration
public partial string Name { get; set; }
}
// File2.cs
public partial class A
{
// implementation declaration:
private string _name;
public partial string Name
{
get => _name;
set => _name = value;
}
}
Code example for partial Indexer
// File1.cs
public partial class DataCollection
{
// Declaring declaration
public partial int this[int index] { get; set; }
}
// File2.cs
public partial class DataCollection
{
// Implementing declaration
private int[] _data = new int[10];
public partial int this[int index]
{
get => _data[index];
set => _data[index] = value;
}
}
Overload resolution priority
The new feature extends one step towards developer control over his code. It allows you to define priority explicitly among overloads. Prioritizing helps when you want a high-performing overload to run every time and don't want to change the original codebase. Moreover, it reduces ambiguity and provides certainty about overload execution.
Code example
using System.Runtime.CompilerServices;
public class MyCollection
{
// Higher priority assigned to this overload to improve performance
[OverloadResolutionPriority(1)]
public void P(params ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default priority of 0
public void P(params int[] a) => Console.WriteLine("Array");
}
// Program.cs
var collection = new MyCollection();
int[] arr = { 1, 2, 3 };
collection.P(1, 2, 3, 4);
collection.P(arr);
collection.P(new[] { 5, 6, 7 });
Output
We set the priority of methods having ReadOnlySpan
optimization by [OverloadResolutionPriority(1)]
from the System.Runtime.CompilerServices
. A higher number means higher priority. When we execute the high-priority overload, it executes every time.
Conclusion
C# is a prominent programming language maintained by Microsoft. Its popularity is due to its robustness and versatility. It is one of a few languages used by various domains, including web development, game development, desktop applications, and native and cross-platform mobile apps. Microsoft keeps improving it with version-wise features. C# 13 is a newer version that brings many important features, easing the developer community by enhancing performance and readability. With the launch of .NET 9, C# 13 brings novelties to make code handy and high-performing. Updates like enhanced params support, implicit index, new escape sequence, new lock type, natural type optimization, and prioritization of overload are included in the latest version. We discussed these important updates of C# 13, along with examples.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website