>
>
>
What's new in C# 13: overview

Valentin Prokofiev
Articles: 2

What's new in C# 13: overview

A new C# version is coming soon, continuing our annual overview series. This year's release brings more changes than the last. That's nice to see! There are both major changes and specialized ones. Let's take a closer look at them.

Object initialization using "from the end" index

We'll start our overview with a fairly simple innovation, which is the initialization of an object using the implicit "from the end" index operator (^).

Previously, during initialization, you could specify object indices only "from the beginning":

var initialList = Enumerable.Range(1, 10);
var anotherList = new List<int>(initialList)
{
    [9] = 15
};
Console.WriteLine(string.Join(" ", anotherList));
// 1 2 3 4 5 6 7 8 9 15

In C# 13, you can now specify an index by counting elements "from the end":

var initialList = Enumerable.Range(1, 10);
var list = new List<int>(initialList)
{
    [^1] = 15
}; 
Console.WriteLine(string.Join(" ", anotherList));
// 1 2 3 4 5 6 7 8 9 15

Of course, all classes that implement indexer overloading with the Index structure type argument support the feature:

void Check()
{ 
    var initTestCollection = new TestCollection<int>(Enumerable.Range(1, 10));
    var anotherTestCollection = new TestCollection<int>(initTestCollection)
    {
        [^5] = 100
    };
    Console.WriteLine(string.Join(" ", anotherTestCollection));
    // 1 2 3 4 5 100 7 8 9 10
}

class TestCollection<T> : IEnumerable<T>
{
    T[] _items;

    public T this[Index index]
    {
        get =>_items[index];
        set => _items[index] = value;
    }
    // ....
}

As mentioned above, the change is simple and straightforward. The initializer is typically used to define index values sequentially. A less common practice is to specify the values of particular indices. Developers will probably use the new object initialization option even less frequently.

Partial properties and indexers

The new language version extends the capability to partially declare and implement class members. Previously, partial declarations were limited to classes, structures, interfaces, and methods. Now, you can add the partial modifier for properties and indexers. The logic is standard: you specify the declaration in one part and the implementation in another.

To give an example, we'll look at the previously declared TestCollection<T> and tinker with the code a bit:

partial class TestCollection<T> : IEnumerable<T>
{
    private T[] _items;

    public partial int Count { get; } // Property declaration
    public partial T this[Index index] { get; set; } // Indexer declaration
    // ....
}
partial class TestCollection<T>
{
    public partial int Count => _items.Length; // Property implementation
    public partial T this[Index index] // Indexer implementation
    {
        get => _items[index];
        set => _items[index] = value;
    }
}

Now the class has the declaration of the class indexer and the Count property in one part and the implementation in another.

It's no secret that programmers use partial classes to generate source code. For example, they often do it using regular expressions compiled at build time via the GeneratedRegexAttribute attribute; or for data validation when inheriting from the ObservableValidator class. This small enhancement will help broaden the scope of code generation. It will also better separate the code declaration and implementation in your large classes.

Params collections

It's unbelievable but true! C# 13 will introduce the much-requested (at least by the author) support for collections when using the params modifier. Methods that required us to convert collections to arrays are now easier to work with. This means less code and better readability.

Working with databases is a clear example of passing collections to such methods. We often need to pass a selection obtained using the LINQ Where method, or a list of entry identifiers using Select. This results in the IEnumerable<T> collection, which needs to be converted to the T[] array, because the use of methods with the params modifier in their arguments is limited. In the upcoming language update, we won't need to do this. We will be able to write methods that take params collections as arguments without any issues.

In addition to arrays, ReadOnlySpan<T>, Span<T>, and the children that implement IEnumerable<T> (List<T>, ReadOnlyCollection<T>, ObservableCollection<T>, and the like) will become available for specifying the type with the params keyword.

Let's take a look at the method call precedence in case of overloads. Here's an example when multiple overloads of a method are applied and a literal is passed as an argument:

void Check()
{
    ParamsMethod(1);
}
void ParamsMethod(params int[] values) // Before C# 13
{
    // (1)
}
void ParamsMethod(params IEnumerable<int> values) // After C# 13
{
    // (2)
}
void ParamsMethod(params Span<int> values) // After C# 13
{
    // (3)
}
void ParamsMethod(params ReadOnlySpan<int> values) // After C# 13
{
    // (4) <=
}

Executing the Check() method results in a call to the overload number four, which has ReadOnlySpan<int> as its params type. I think it might be due to optimization to avoid additional memory allocation when working with the collection.

If we restrict the method overloads to Span<int> and ReadOnlySpan<int>, then passing an array results in the method call with the Span<int> argument type. Implicit array conversion to Span<int> causes such behavior. If we pass an array to ParamsMethod, which is initialized when called, then the ReadOnlySpan<int> overload is called, since there are no references to the collection in the code:

void Check()
{
    int[] array = [1, 2, 3];
    ParamsMethod(array);     // (1)
    ParamsMethod([1, 2, 3]); // (2)
}
void ParamsMethod(params Span<int> values) // <= (1)
{
    // ....
}
void ParamsMethod(params ReadOnlySpan<int> values) // <= (2)
{
    // ....
}

When working with template methods, ReadOnlySpan<T> almost always takes precedence if there's no type-specific implementation. Even passing List<T> results in calling the ReadOnlySpan<T> params method instead of IEnumerable<T>. This doesn't seem obvious and is potentially dangerous.

This feature enhancement is quite significant, even though it may seem minor at first glance. In projects, we often resort to converting lists to arrays, because most of the time we work with lists or similar data structures. The use of ToArray() has always seemed redundant, as it's mainly done just to make the code compile without any additional logic. However, now we can shrug this feeling off and directly pass collections to such methods.

Overload resolution priority attribute

A new System.Runtime.CompilerServices namespace attribute, the OverloadResolutionPriorityAttribute, has been introduced to prefer one overload over another. The name is cumbersome, but it perfectly captures the point. As Microsoft explains, the attribute is mainly for API developers who want to "softly" move their users from one method overload to another that may have a better implementation.

Given the not-so-obvious overloading priority chosen by the compiler, we can now explicitly specify which method to use first. For example, in the context of two ParamsMethod overloads with the ReadOnlySpan<T> and Span<T> argument types, we can direct the compiler to prioritize the method with the Span<T> type:

void Check()
{
    int[] array = [1, 2, 3];
    ParamsMethod(array);     // (1)
    ParamsMethod([1, 2, 3]); // (2)
}
[OverloadResolutionPriority(1)]
void ParamsMethod(params Span<int> values) <= (1)(2)
{
    // ....
}
void ParamsMethod(params ReadOnlySpan<int> values)
{
    // ....
}

We can see that the attribute takes one value, the overload priority. The higher the value is, the higher is the method priority. The default value of each method is 0.

Important note! If method overloads are in different classes (for example, extension methods), note that prioritization works only within their own classes. That is, a priority from one class of extension methods doesn't affect the other.

The above scope characteristic can be an unexpected problem for a developer. For example, legacy code with irrelevant logic may cause issues when executed (especially in a running application). Certain tools that detect such non-obvious errors can help you prevent this. Those are static code analyzers. This update inspired us to add a new diagnostic rule for our C# PVS-Studio analyzer, in addition to hundreds of existing ones.

New Lock class

C# 13 provides better thread synchronization. A full-fledged Lock class from the System.Threading namespace has replaced object. The class is designed to enhance code readability and efficiency. In addition to this, the class has the following methods for handling it:

  • Enter() enters the locked section.
  • TryEnter() tries to immediately enter the locked section if it's possible. It returns the result of the entrance attempt in the bool form.
  • EnterScope() gets the Scope structure that can be used with the using statement.
  • Exit() exits from the locked section.

There's also the IsHeldByCurrentThread property, which helps us see whether the lock is held by the current thread.

If we use the lock operator in the usual format, the code takes the following form:

class LockObjectCheck
{
    Lock _lock = new();

    void Do()
    {
        lock (_lock)
        {
            // ....
        }
    }
}

The code example above shows that the new implementation doesn't differ much from the old one. That's what the code looks when applying the additional features of the new class:

class LockObjectCheck
{
    Lock _lock = new();

    void Do()
    {
        _lock.Enter();
        try
        {
            // ....
        }
        // ....
        finally
        {
            if (_lock.IsHeldByCurrentThread)
                _lock.Exit();
        }
    }
}

As mentioned earlier, we can utilize an instance of the Scope structure obtained by calling the EnterScope method to ensure that the IDisposable instance is used correctly. In such a case, the code looks as follows:

using (var scope = _lock.EnterScope())
{
    // ....
}

The introduction of the code-locking class brings clarity for beginners and enhanced control for experienced C# programmers. The transition to the new format is to be easy and painless, as no major refactoring is required.

New escape sequence

The language developers introduced a new escape sequence character, \e. It's designed to replace the existing \x1b. Using \x1b isn't recommended because the next characters following 1b may be interpreted as valid hexadecimal values, which makes them part of the escape sequence. This case will help avoid unexpected situations.

Method group natural type enhancements

C# 13 refines the rules for determining applicable candidate methods when working with method groups and natural types. Natural types are types defined by the compiler (such as when using var).

Previously, when processing natural types, the compiler would consider each candidate. However, in the new implementation, the compiler discards those that definitely don't fit (e.g., template methods with constraints). The change is technical but should reduce compiler errors related to method groups.

Interfaces and ref struct

A long time ago, C# 7.2 introduced ref struct. It's been reworked in the 13th version of the language. Let's refresh our memory on that: the main construct feature is the exclusive memory allocation on the stack, with no way to move that memory to the managed heap. We can leverage this to enhance the security and performance of the application (learn more here). The familiar Span<T> and its read-only analogue, ReadOnlySpan<T>, are notable examples of such structures.

Inheritance

Up to this point, ref struct had specific constraints, including prohibition of inheritance from interfaces to avoid boxing. Attempts to perform such inheritance resulted in the "ref structs cannot implement interfaces" error. This constraint has now been removed, enabling inheritance from interfaces:

interface IExample
{
    string Text { get; set; }
}
ref struct RefStructExample : IExample
{
    public string Text { get; set; }
}

It's not that simple, though. When trying to cast a struct instance to an interface, we get the following error: "Cannot convert type 'RefStructExample' to 'IExample' via a reference conversion, boxing conversion, unboxing conversion, wrapping conversion, or null type conversion". This is one of the new constraints when using ref struct, which ensures referential safety.

Anti-constraint for allows ref struct

This anti-constraint enables the passing of ref structure instances to template methods. An attempt to pass such a structure in C# 12 could result in the following message: "The type 'RefStructExample' may not be a ref struct or a type parameter allowing ref structs in order to use it as parameter 'T' in the generic type or method 'ExampleMethod<T>(T)'". Now, we can add a construct to enable the use of ref struct where method constraints are specified:

void Check()
{
    var example = new RefStructExample();
    ExampleMethod(example);
}
void ExampleMethod<T>(T example) where T: IExample, allows ref struct
{
    // ....
}

Yes, everything works correctly. In such a method, we can easily describe all the general logic needed for IExample derivatives, including RefStructExample.

Note: allows ref struct is the first anti-constraint. Such constructs used to only prohibit third-party types.

Looking at this update, we can say that the language developers let us have more fun when building hierarchies. And that's good! Such inheritance is useful both for combining logic and for adding implementation commitments to inherited entities.

The introduction of the allows ref struct anti-constraint marks a shift in how method specifications are defined. The idea of enabling some features looks interesting and promising. It will be exciting to see what new types of anti-constraints the C# development team will prepare for us.

ref and unsafe in async methods and iterators

Continuing the topic of ref struct, it's impossible to overlook an improvement that will widen the scope of their use. In asynchronous methods, we can now declare ref local variables and instances of ref structures.

For example, when you try to declare an instance of the ReadOnlySpan<int> structure in an early version of C#, the compiler issues the following error, "Parameters or locals of type 'ReadOnlySpan<int>' cannot be declared in async methods or async lambda expressions". The new language version eliminates this error, but it's important to note that the restriction against having ref in the arguments of such methods still applies.

Now, developers can introduce local ref variables in iterator methods (methods that use the yield operator). However, there's a restriction on the output of these variables:

IEnumerable<int> TestIterator()
{
    int[] values = [1, 2, 3, 4, 5];

    ref int firstValue = ref values[0];
    ref int lastValue = ref values[values.Length - 1];
    yield return firstValue;
    yield return lastValue;
    // A 'ref' local cannot be preserved across 'await' or 'yield' boundary
}

In the new version of the language, iterators and asynchronous methods will support the unsafe modifier, which enables you to perform any pointer operations. This will require a safe context in iterators for constructs such as yield return and yield break.

Conclusion

The list of changes in the new C# version is larger than last year. Some add features that were impossible before, while others are designed to make developers' lives easier. The update may not seem significant to a general expert due to its niche innovations, which may not even be noticeable during the development process.

What do you think of this? Is Microsoft progressing in the evolution of the language, or is it stagnating by unveiling features that for some reason haven't yet been implemented? Share your opinions in the comments.

For Microsoft's documentation on the changes in C# 13, click here. If you'd like to read our overviews on previous C# updates, here are the articles:

As per tradition, expect the release in early November. While you're waiting, why not subscribe to our blog newsletter so you don't miss out on our other theoretical articles?