C# 13, released alongside .NET 9, introduces a variety of features aimed at enhancing developer productivity and code efficiency.
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:
- The Lock.EnterScope() method allows developers to enter an exclusive execution scope. This method returns a ref struct that implements the Dispose pattern, ensuring that the lock is automatically released when the scope ends, even if an exception occurs. This reduces boilerplate code and minimizes the risk of forgetting to release the lock, which can lead to deadlocks or resource contention.
- By integrating with the existing lock statement, C# 13 allows for a more intuitive syntax. When using a Lock object in a lock statement, it effectively translates to using EnterScope(), making code easier to read and maintain.
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.
- The new lock type is designed to avoid common pitfalls associated with traditional locking mechanisms. It provides better control over shared resources, reducing the likelihood of race conditions and improving overall thread safety in applications.
- The new lock type recognizes when it is being used in a traditional lock statement and optimizes its behavior accordingly. This means developers can transition from older locking patterns with minimal changes to their existing codebase while gaining the benefits of improved performance and safety.
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();
}
}
- The MyRefStruct is defined as a ref struct, which means it is allocated on the stack and cannot escape to the heap. It includes a simple integer field and a method to display its value.
- The Container<T> class is defined with a generic type parameter T, constrained by where T: allows ref struct. This constraint allows MyRefStruct to be used as a type argument for this container.
- In the Main method, an instance of MyRefStruct is created and passed to the Container. The Show method displays the value stored in the ref struct.
- After modifying the value of myStruct, calling Show again reflects this change, demonstrating that changes to the original ref struct are visible in the container.
Benefits of Using allows ref struct
- By allowing ref struct types as generic type parameters, developers can create more versatile and reusable components without sacrificing performance or safety.
- The compiler enforces safety rules for ref struct, preventing common pitfalls associated with heap allocations and boxing.
- Since ref structs are allocated on the stack, they provide efficient memory usage, which is especially beneficial in performance-critical applications.
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;
}
}
- The BufferSize property is declared as a partial property in one file.
- Its implementation, which includes logic to enforce a minimum buffer size, is provided in another file. This separation allows for better organization of code, especially when dealing with generated code.
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;
}
}
- The indexer is declared as a partial indexer, allowing for its definition to be separated from its implementation.
- The actual logic for getting and setting values in the items array is provided in another file, maintaining clarity and separation of concerns.
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;
}
}
- In this example, the ModifyArrayAsync method uses a ref local to reference the first element of an integer array.
- The value of firstElement is modified directly, demonstrating that changes affect the original array.
- The method includes an await statement, but since the ref local is not used across the await, it remains valid.
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
}
}
- The GetNumbers method is marked as unsafe, allowing pointer operations.
- A pointer to an integer variable is created, and its value is yielded.
- The value is modified through the pointer, showcasing how unsafe contexts can be utilized within iterators.
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
}
}
- In this combined example, ModifyAndIterateAsync uses a ref local to modify an element of an array.
- After modification, it is called GetUnsafeNumbers, which operates within an unsafe context to yield values using pointers.
- This showcases how C# 13 allows both features to coexist without violating safety rules.
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));
}
}
- CountdownTimer Class contains an integer array TimerValues with a size of 10. It is intended to store countdown values.
- The Main method is where execution starts. A new instance of CountdownTimer is created using an object initializer. The TimerValues array is initialized using implicit index access with the ^ operator.
- Implicit Index Access the ^ operator allows you to set values in the TimerValues array starting from the end. For example, [^1] refers to the last element of the array. [^2] refers to the second-to-last element, and so on. This syntax simplifies setting values without needing to calculate indices manually.
- It prints out the initialized timer values using a string.Join to format them as a comma-separated string.
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:
- Higher values indicate higher priority. For example, an overload with a priority of 2 will be preferred over one with a priority of 1.
- The default priority is 0, meaning no specific preference is set.
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
- This attribute helps manage ambiguities in method calls, especially when dealing with numeric types that can implicitly convert between each other (e.g., int to double).
- When adding new overloads to existing APIs, developers can assign lower priorities to new methods, allowing legacy methods to remain preferred without breaking existing code.
- API authors can guide users toward more efficient or appropriate overloads based on common usage patterns by setting priorities.
Considerations
- If multiple overloads have the same priority, developers may encounter ambiguity warnings or errors during compilation.
- While this feature enhances usability in certain scenarios, overusing it can lead to confusion about which method will be invoked, so it should be used judiciously.
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