Overview of innovations in C# 13

A new version of the C# language will be released very soon, and we continue our series of annual reviews of innovations. There are more changes this year than last year, which is encouraging. There are both important changes and very highly specialized ones. Let's look at them in more detail.

Initializing an object by index “from the end”

Let's start covering the new changes with a fairly simple innovation – initialization of an object using an implicit index operator “from the end” – ^.

Previously, it was possible to specify object indices during initialization 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 the new version of the language, you can specify the index by counting the element number “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

Obviously, this functionality is supported in all classes that implement indexer overloading with a structure type argument Index:

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 stated earlier, the change is simple and straightforward. Typically an initializer is used to continuously define index values. Less commonly used is specifying the values ​​of specific indexes. There is an assumption that this case will be used even less often.

Partial properties and indexers

The new version of the language offers to expand the ability to partially declare and implement the contents of classes. Before this, it was possible to point out the factor of partiality to classes, structures, interfaces and methods. Now it is possible to specify a modifier partial properties and indexers. The logic is familiar: in one part the declaration is indicated, and in the other – the implementation.

For example, let's look at the previously declared test collection TestCollection and modify the code a little:

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

    public partial int Count { get; } // Объявление свойства
    public partial T this[Index index] { get; set; } // Объявление индексатора
    // ....
}
partial class TestCollection<T>
{
    public partial int Count => _items.Length; // Реализация свойства
    public partial T this[Index index] // Реализация индексатора
    {
        get => _items[index];
        set => _items[index] = value;
    }
}

This class now has a class and property indexer declaration Count in one part, and implementation in another.

It's no secret that partial classes are used to generate source code. For example, when using regular expressions compiled at build time using the attribute GeneratedRegexAttribute. Or for data validation when inheriting from a class ObservableValidator. This small innovation will help both expand the scope of application of code generation, and to a greater extent distinguish between the points of declaration and implementation of code in their own bulk classes.

Params collections

Incredible but true! In C# version 13, such desired (by the author, at least) support for collections with a modifier will be added params. Now the methods for which we worked so hard to translate collections into arrays will become easier to use. There will be less code and increased readability.

One clear case for passing collections to such methods is when working with databases. It is often necessary to pass some sample obtained using LINQ method Whereor a list of record identifiers using Select. When used, the result is a collection IEnumerablewhich has to be converted to an array T[]since methods with a modifier params in arguments limit their use. In the next language update, this action will not be required – you can easily write methods that take as an argument params collections.

In addition to arrays, to specify the type with the keyword params will become available: ReadOnlySpan, Span and heirs implementing IEnumerable (List, ReadOnlyCollection, ObservableCollection and the like).

It is interesting to look at the priority of method calls in the presence of overloads. Consider a situation with several overloads of one method and passing a literal as an argument:

void Check()
{
    ParamsMethod(1);
}
void ParamsMethod(params int[] values) // До C# 13
{
    // (1)
}
void ParamsMethod(params IEnumerable<int> values) // После C# 13
{
    // (2)
}
void ParamsMethod(params Span<int> values) // После C# 13
{
    // (3)
}
void ParamsMethod(params ReadOnlySpan<int> values) // После C# 13
{
    // (4) <=
}

Method Execution Check() will lead to calling overload number 4, as a type params which is indicated ReadOnlySpan. There is an idea that there is an optimization point here in order to avoid additional memory allocation when working with the collection.

If we limit the selection of method overloads to Span And ReadOnlySpanthen passing the array will lead to calling a method with the argument type Span. This behavior is caused by the fact that there is an implicit conversion of the array to Span. If you pass it to the method ParamsMethod array that is initialized when called, then the overload will be called ReadOnlySpansince we actually have 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, priority is almost always given to ReadOnlySpanif there is no implementation for a specific type. And even transmission List will result in a method call params ReadOnlySpannot IEnumerablewhich looks very unobvious and potentially dangerous.

This expansion of functionality is quite significant, although at first glance it seems insignificant. In projects, you often have to resort to converting lists into arrays, since most often you work with lists or similar data structures. Application ToArray() each time felt redundant, since in fact this work is done only to ensure that the code compiles, without any additional logic. Now you can get rid of this feeling and directly pass collections to similar methods.

Congestion Prioritization Attribute

A new namespace attribute has been added to add priority to one of the overloads System.Runtime.CompilerServices – OverloadResolutionPriorityAttribute. The name is cumbersome, but it accurately reflects the essence. As Microsoft itself says, this attribute is mostly needed for API developers who want to “gently” transfer their users from one method overload to another, where there may be a better implementation.

Taking into account the not very obvious priority of the compiler's choice of overloads, you can explicitly indicate which method will be used first. For example, in the context of two overloads ParamsMethod with argument types ReadOnlySpan And Spanyou can tell the compiler to prioritize a method with type Span:

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)
{
    // ....
}

You can notice that the attribute takes one value – overload priority. The higher this value, the more priority the method. The default value for each method is 0.

Important! If method overloads are in different classes (for example, extension methods), then you need to take into account the fact that prioritization only works within its own classes. That is, the priority of one class of extension methods will not affect another.

The above scoping feature can present an unexpected problem for the developer. For example, outdated code with irrelevant logic may be called, which can lead to trouble (especially in a running application). To prevent such situations, there are tools on the market that help developers find non-obvious errors – static code analyzers. This innovation gave us the idea of ​​adding a new diagnostic rule for our C# analyzer PVS-Studio in addition to the hundreds already existing.

New Lock class

Thread synchronization has been improved. To replace object a full class has arrived Lock from namespace System.Threading. This class is designed to make the code more understandable and efficient. In addition to this, the class has the following methods to work with it:

  • Enter() — entrance to the blocking area.

  • TryEnter() — an attempt to immediately enter the blocking area, if this is permissible. Returns the result of the login attempt as bool.

  • EnterScope() — obtaining a Scope structure that can be applied together with the operator using.

  • Exit() — exit from the blocking area.

There is also a property IsHeldByCurrentThreadwhich can be used to determine whether the current thread is holding the lock.

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

class LockObjectCheck
{
    Lock _lock = new();

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

From the code example above you can see that the new implementation differs from the old one insignificantly. When using the additional functionality of the new class, the code takes the following form:

class LockObjectCheck
{
    Lock _lock = new();

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

As stated earlier, you can use an instance of the structure Scopeobtained by calling the method EnterScopeto ensure correct use IDisposable copy. The code in this case looks like this:

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

The addition of a code blocking class brings clarity for newbies and more controls for experienced C# programmers. The transition to the new format is expected to be simple and painless, since complex refactoring will not be required.

New escape sequence

Language developers introduced a new escape character \e. It was added to replace the existing \x1bwhich is not recommended. The current problem is that the following characters may be interpreted as valid hexadecimal values, which will cause them to become part of the specified escape sequence. This case will help you avoid unforeseen situations.

Improved performance of method groups with natural types

C# 13 improves the matching algorithm when working with method groups and natural types. Natural types are types defined by the compiler (for example, using var).

Previously, the compiler, in the case of natural types, considered each of the candidates. But in the new implementation, the compiler will discard those that are definitely not suitable (for example, template methods with delimiters). This change is purely technical, but it should provide fewer compiler errors when working with groups of methods.

Interfaces and ref struct

In distant C# 7.2 they were added ref structwhich received significant changes in version 13 of the language. Recall that the main feature of this design is the exclusive allocation of memory on the stack, without the possibility of moving to the managed heap. This can be used to improve application security and performance (more details). A prominent representative of such structures is the familiar Span and its Readonly analogue – ReadOnlySpan.

Inheritance

Until this moment, ref struct there were some limitations, including inheritance from interfaces. This was previously prohibited to avoid boxing. An attempt like this could result in an error “ref structs cannot implement interfaces”. Now this restriction has been lifted, which allows inheritance from interfaces:

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

But it's not that simple. When we try to cast a structure instance to an interface, we get an 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 restrictions when using ref structproviding reference safety.

Anti-restriction allows ref struct

This anti-restriction allows you to pass instances of *ref *structures to template methods. When trying to pass a similar structure in C# 12, you could get 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)'”. Now, in the place where the method limiters are specified, you can add a construction that allows the use ref struct:

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

And yes, everything works great. In this method, you can easily describe all the generalized logic of interest for heirs IExampleincluding RefStructExample.

Note. allows ref struct – the first anti-restriction. Previously, such designs only prohibited the use of third-party types.

Looking at this innovation, we can say that language developers allow us to get more and more involved in building hierarchies. And that's good! Such inheritances can be used both to combine logic and to add implementation obligations for inherited entities.

I would like to highlight a new type of anti-limiters using the example allows ref structwhich changes the approach to specifying method specifications. The very idea of ​​including some functionality looks interesting and promising. It will be interesting to see what new types of anti-constrainers the C# development team will prepare for us in the future.

ref and unsafe in iterators and async methods

Continuing the topic ref struct It is impossible not to note the innovation, which will expand the places of their use. Asynchronous methods can now declare ref local variables and instances ref structures.

For example, when trying to declare an instance of a structure ReadOnlySpan in an early version of C#, the compiler throws an error “Parameters or locals of type 'ReadOnlySpan' cannot be declared in async methods or async lambda expressions”. The new version of the language does not have this problem, but it is worth considering that there is still a restriction that prohibits having ref in the arguments of such methods.

In iterator methods (methods that use the operator yield) now you can also use local ref variables, but there is a limitation on their output:

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
}

Also in the new version of the language, iterators and asynchronous methods will support the modifier unsafeallowing you to perform any operations with pointers. In this case, iterators will require a safe context for such constructs as yield return And yield break.

Conclusion

The list of changes in the new version of the C# language is presented this time in a larger volume than last year. Some of them provide functionality that was previously impossible, while others serve to simplify the lives of developers. As a result, we can say that for the average specialist there are not many changes, since there are many niche innovations that may not even be noticed during development.

What do you think about this? Is Microsoft moving towards language development or is it standing still, revealing capabilities that for some reason have not been implemented until now? Write your opinion in the comments.

Microsoft documentation on the changelog for C# 13 is available at link. If you would like to read our previous review articles on innovations in the C# language, then here is a list of all articles from previous years:

Traditionally, we expect a release in early November. While you can subscribe to news from our blog so as not to miss our other theoretical articles.

If you want to share this article with an English-speaking audience, please use the translation link: Valentin Prokofiev. What's new in C# 13: overview.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *