C# 13, released alongside .NET 9, introduces a variety of features aimed at enhancing developer productivity and code efficiency.

What's new is C# 13

You can download the latest .NET 9 SDK from the .NET downloads page. You can also download Visual Studio 2022, which includes the .NET 9 SDK.

Key Features of C# 13

C# 13 introduces several exciting features and enhancements that improve developer productivity and code expressiveness. Here are the key features of C# 13:

Enhanced params Collections

The params keyword now supports any collection type, not just arrays. This allows developers to pass a variable number of arguments to methods using types like List<T>, Span<T>, and IEnumerable<T>. This flexibility improves method signatures and usability significantly.

using System;

public class Program
{
    public static void Main()
    {
        // Using params with an array
        PrintNumbers(1, 2, 3, 4, 5);

        // Using params with a List
        var numbersList = new System.Collections.Generic.List<int> { 6, 7, 8 };
        PrintNumbers(numbersList.ToArray());

        // Using params with ReadOnlySpan
        ReadOnlySpan<int> span = stackalloc int[] { 9, 10, 11 };
        PrintNumbers(span);
    }

    // Method using params to accept any number of integers
    public static void PrintNumbers(params int[] numbers)
    {
        Console.WriteLine("Numbers: " + string.Join(", ", numbers));
    }
}

New Lock Type

A new Lock type has been introduced to improve thread synchronization. It includes the Lock. The EnterScope() method simplifies entering critical sections and automatically releases the lock when the scope ends, reducing boilerplate code and potential errors.

C# 13 introduces a new synchronization mechanism through the System.Threading.Lock type, significantly enhancing thread synchronization capabilities. Here’s how this new lock object improves thread synchronization:

For example

Lock myLock = new Lock();

using (myLock.EnterScope()) {
    // Critical section - only one thread can execute this at a time.
    Console.WriteLine("Thread-safe code here.");
}

This structure clearly delineates critical sections and ensures that locks are managed properly.

Allowing Ref Structs

In C# 13, a significant enhancement has been made regarding the use of ref struct types in generic type parameters. Prior to this version, ref struct types could not be used as type arguments for generics. However, with the introduction of the anti-constraint allows ref struct, developers can now specify that a type argument for a generic type or method can be a ref struct. This change allows for greater flexibility and enables the compiler to enforce reference safety rules on all instances of that type parameter.

Example of Using allows ref struct

using System;

public ref struct MyRefStruct
{
    public int Value;

    public MyRefStruct(int value)
    {
        Value = value;
    }

    public void Display()
    {
        Console.WriteLine($"Value: {Value}");
    }
}

public class Container<T> where T : struct
{
    private T _item;

    public Container(T item)
    {
        _item = item;
    }

    public void Show()
    {
        Console.WriteLine("Container holds:");
        if (_item is MyRefStruct myRefStruct)
        {
            myRefStruct.Display();
        }
    }
}

public class Program
{
    public static void Main()
    {
        MyRefStruct myStruct = new MyRefStruct(42);
        
        // Using the ref struct with a generic container
        Container<MyRefStruct> container = new Container<MyRefStruct>(myStruct);
        container.Show();
        
        // Modifying the value through the ref struct
        myStruct.Value = 100;
        Console.WriteLine("After modification:");
        container.Show();
    }
}

Benefits of Using allows ref struct

New Escape Sequence

C# 13 adds the escape sequence \e, which represents the ESCAPE character. This makes working with ANSI escape codes cleaner and less error-prone, especially useful in terminal applications.

Method Group Natural Type Improvements

Enhancements in method group handling streamline overload resolution by pruning non-applicable candidates early in the compilation process. This change reduces errors related to method group usage and improves code clarity.

Partial Properties and Indexers

Developers can now declare partial properties and indexers, allowing for better organization of code across multiple files. This is particularly useful in large projects or when dealing with auto-generated code.

Example of Partial Properties

Partial properties allow you to define a property in one part of a partial class and implement it in another. Here’s an example:

// File: PostSerializer.Partial.cs
partial class PostSerializer
{
    public partial int BufferSize { get; set; }
}

// File: PostSerializer.Implementation.cs
partial class PostSerializer
{
    private const int minBufferSize = 1024;
    private int bufferSize;

    public partial int BufferSize
    {
        get => bufferSize < minBufferSize ? minBufferSize : bufferSize;
        set => bufferSize = value;
    }
}

Example of Partial Indexers

Partial indexers work similarly, allowing you to declare an indexer in one part and implement it in another. Here’s how it looks:

// File: MyCollection.Partial.cs
partial class MyCollection
{
    public partial int this[int index] { get; set; }
}

// File: MyCollection.Implementation.cs
partial class MyCollection
{
    private int[] items = new int[10];

    public partial int this[int index]
    {
        get => items[index];
        set => items[index] = value;
    }
}

Support for ref Locals and Unsafe Contexts

C# 13 allows the use of ref locals and unsafe contexts within asynchronous methods and iterators, enhancing performance and flexibility in high-performance applications.

Example of Using Ref Locals in Async Methods

using System;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        int[] numbers = { 1, 2, 3 };

        // Using ref locals in an async method
        await ModifyArrayAsync(numbers);

        Console.WriteLine($"Modified Array: {string.Join(", ", numbers)}");
    }

    public static async Task ModifyArrayAsync(int[] array)
    {
        // Declare a ref local for the first element
        ref int firstElement = ref array[0];

        // Modify the first element
        firstElement += 10;

        // Simulate an asynchronous operation
        await Task.Delay(100);

        // The ref local can still be used here
        firstElement *= 2;
    }
}

Example of Using Unsafe Contexts in Iterators

using System;
using System.Collections.Generic;

public unsafe class UnsafeIteratorExample
{
    public static void Main()
    {
        foreach (var number in GetNumbers())
        {
            Console.WriteLine(number);
        }
    }

    public static IEnumerable<int> GetNumbers()
    {
        int value = 42;
        int* pointer = &value; // Unsafe context: obtaining a pointer to value

        yield return *pointer; // Yielding the value pointed by pointer

        *pointer = 100; // Modifying the value through pointer

        yield return *pointer; // Yielding the modified value
    }
}

Combining Ref Locals and Unsafe Contexts

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public unsafe class CombinedExample
{
    public static async Task Main()
    {
        int[] numbers = { 1, 2, 3 };
        
        await ModifyAndIterateAsync(numbers);
    }

    public static async Task ModifyAndIterateAsync(int[] array)
    {
        // Declare a ref local for the second element
        ref int secondElement = ref array[1];

        // Modify the second element
        secondElement += 5;

        // Simulate an asynchronous operation
        await Task.Delay(100);

        foreach (var number in GetUnsafeNumbers())
        {
            Console.WriteLine(number);
        }
    }

    public static unsafe IEnumerable<int> GetUnsafeNumbers()
    {
        int value = 42;
        int* pointer = &value;

        yield return *pointer; // Yielding the initial value
        
        *pointer = 100; // Modifying it through pointer

        yield return *pointer; // Yielding the modified value
    }
}

Implicit Index Access in Object Initializers

The language now supports implicit index access using the new "from the end" operator (^), allowing easier access to elements from the end of collections during initialization.

Code Example Implicit Index Access in Object Initializers

using System;

public class CountdownTimer
{
    // An array to hold timer values
    public int[] TimerValues = new int[10];
}

public class Program
{
    public static void Main()
    {
        // Using implicit index access to initialize TimerValues from the end
        var countdown = new CountdownTimer
        {
            TimerValues = 
            {
                [^1] = 0,  // Set the last element to 0
                [^2] = 1,  // Set the second last element to 1
                [^3] = 2,  // Set the third last element to 2
                [^4] = 3,  // Set the fourth last element to 3
                [^5] = 4,  // Set the fifth last element to 4
                [^6] = 5,  // Set the sixth last element to 5
                [^7] = 6,  // Set the seventh last element to 6
                [^8] = 7,  // Set the eighth last element to 7
                [^9] = 8,  // Set the ninth last element to 8
                [^10] = 9, // Set the first element to 9
            }
        };

        // Output the timer values
        Console.WriteLine("Countdown Timer Values: " + string.Join(", ", countdown.TimerValues));
    }
}

Overload Resolution Priority Attribute

This new feature allows library authors to designate one overload as better than others, improving method resolution in complex scenarios.

How OverloadResolutionPriorityAttribute Works?

The OverloadResolutionPriorityAttribute is part of the System.Runtime.CompilerServices namespace. It can be applied to methods, properties, and constructors to indicate their relative priority during overload resolution.

Priority Values: The attribute accepts an integer value as a parameter, where:

Usage: When multiple overloads are applicable for a given method call, the compiler will choose the one with the highest priority. If there are multiple overloads with the same priority, the compiler will revert to its standard overload resolution rules.

Example

Here’s a practical example demonstrating how to use the OverloadResolutionPriorityAttribute:

using System;
using System.Runtime.CompilerServices;

public class MathHelper
{
    [OverloadResolutionPriority(1)]
    public static void Calculate(double number = 20)
    {
        Console.WriteLine("Double overload called with value: " + number);
    }

    [OverloadResolutionPriority(2)]
    public static void Calculate(int number = 10)
    {
        Console.WriteLine("Integer overload called with value: " + number);
    }
}

class Program
{
    static void Main()
    {
        MathHelper.Calculate(); // Calls the integer overload due to higher priority
        MathHelper.Calculate(5); // Calls the integer overload
        MathHelper.Calculate(5.5); // Calls the double overload due to implicit conversion
    }
}

Practical Applications

Considerations

Conclusion

C# 13 focuses on enhancing flexibility, performance, and usability for developers. With these new features, C# aims to streamline coding practices, reduce errors, and improve overall efficiency in application development. Developers are encouraged to explore these features using Visual Studio 2022 or the .NET 9 Preview SDK to fully leverage the improvements offered by this release.

Explore all the exciting updates in ASP.NET Core 9! Check out my latest article: What’s New in ASP.NET Core in .NET 9