C# 12 is the latest, just released, version of the popular programming language that runs on the .NET 8 platform. It introduces several new features that aim to improve the expressiveness, performance, and safety of the language. In this post, we will explore some of these features and see how they can benefit your code.

Every day C# 12 features

Those features might be used every day by developers adopting C# 12.

Primary constructors

Primary constructors allow you to declare parameters for a class or struct in the same line as the type name. These parameters are in scope for the entire body of the type, and can be used to initialize fields, properties, or methods. For example:

1
2
3
4
5
6
class Student(string name, int score)
{
public string Name => name;
public int Score => score;
public void Greet() => Console.WriteLine($"Hello, {name}!");
}

Primary constructors were previously available for record types, which are immutable types that support value equality and deconstruction. Record types can also use positional syntax to declare primary constructor parameters and public properties in one line. For example:

1
record Point(int X, int Y);

Public properties for primary constructor parameters are generated by the compiler only in record types, which can be either record class or record struct types.

Collection expressions

Collection expressions are a new syntax to create collections of elements, such as arrays, System.Span<T> and System.ReadOnlySpan<T>, or types supporting collection initializer like List<T>. You can use the spread operator .. to inline other collections into a collection expression. For example:

1
2
3
4
5
6
7
8
9
10
11
12
int[] someOtherNumbers = [4, 5, 6];
List<int> numbers = [1, 2, 3, ..someOtherNumbers, 7, 8, 9];

string[] moreFruits = ["orange", "pear"];
IEnumerable<string> fruits = ["apple", "banana", "cherry", ..moreFruits];

List<(string, int)> otherScores = [("Dave", 90), ("Bob", 80)];
(string name, int score)[] scores = [("Alice", 90), ..otherScores, ("Charlie", 70)];

Student[] students = [new Student("Alice", 90), new Student("Bob", 80)];

Span<char> span = ['h', 'e', 'l', 'l', 'o' ];

Collection expressions can be used anywhere you need a collection of elements, such as initializing a field, passing an argument, or returning a value.

Optional parameters in lambda expressions

You can now define default values for parameters in lambda expressions, just like you can for methods or local functions. This can make your code more concise and flexible when using delegates or functional programming patterns. For example:

1
2
3
var incrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(incrementBy(2)); // prints 3
Console.WriteLine(incrementBy(2, 3)); // prints 5

Alias any type

You can now use the using directive to create aliases for any type, not just named types. This can be useful for creating semantic aliases for complex types, such as tuples, arrays, pointers, or other unsafe types. For example:

1
2
3
4
5
using Point = (int X, int Y);
Point p = (3, 4);

using Matrix = int[][];
Matrix aMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

Advanced C# 12 features

Those features will be used mostly in advanced scenario of C# 12.

ref readonly parameters

You can now use the ref readonly modifier on parameters to indicate that they are passed by reference, but cannot be modified. This can improve the performance of your code by avoiding unnecessary copying, and also enforce the immutability of the arguments. For example:

1
2
3
4
5
6
7
var one = 1;
var two = 2;

var max = Max(ref one, ref two);

static ref readonly int Max(ref readonly int x, ref readonly int y)
=> ref x > y ? ref x : ref y;

Inline arrays

Inline arrays are a way to create an array of fixed size in a struct type, without using unsafe code or fixed size buffers. Inline arrays can improve the performance of your code by reducing allocations and copying. For example:

1
2
3
4
5
6
7
8
9
10
11
12
// Indicates that the instance's storage is sequentially replicated Length times.
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer
{
private int _element0;
}

var buffer = new Buffer();
for (int i = 0; i < 5; i++)
{
buffer[i] = i;
}

Inline arrays are mainly used by the runtime team and other library authors to implement types such as Span<T> or ReadOnlySpan<T>, which are used to represent slices of memory.

Experimental attribute

You can now mark types, methods, or assemblies with the ExperimentalAttribute to indicate that they are experimental features that may change or be removed in the future. The compiler will issue a warning if you access a member that is marked with this attribute. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Experimental]
class Foo
{
[Experimental]
public void Bar() { }
}

class Program
{
static void Main()
{
var foo = new Foo(); // warning: 'Foo' is experimental
foo.Bar(); // warning: 'Foo.Bar()' is experimental
}
}

Interceptors

Interceptors are an experimental feature that allow you to substitute a call to a method with a call to another method at compile time. This can be useful for modifying the semantics of existing code by adding new code to a compilation, for example in a source generator.

To use interceptors, you need to enable the InterceptorsPreview feature flag in your project file.

Interceptors are still in preview mode and may be subject to breaking changes or removal in a future release.

Conclusion

These are the new features of C# 12 that you can start to use now. You can learn more about these features and other improvements in C# 12 by reading the official documentation or the feature specifications. Happy coding!

References