Microsoft unveils new features in C# 12 Preview. C# 12 along with .NET 8 will be officially released in November 2023. Let’s explore the latest impressive enhancements in this post.
Primary Constructors syntax now usable on class and struct in C# 12
Since C#9 we have the convenient primary constructor syntax for class record
(or just record
) and struct record
:
1 2 3 4 5 |
var p = new Person("Seth", "Gecko"); Assert.IsTrue($"{p.FirstName} {p.LastName}" == "Seth Gecko"); // p.FirstName = "Alan"; ERROR record are immutable by default public record Person(string FirstName, string LastName); |
C# 12 introduces primary constructors for non-record classes and structs, but it’s important to note that they work differently. Actually the motivation behind their implementation varies:
- In records, the primary constructor offers a succinct method to generate public read-only properties. This is useful for creating simple immutable objects meant to store states.
- In C# 12 classes and structs, the primary constructor provides a concise way to generate private fields. Since classes and structs often have internal logic that relies on internal states, the primary constructor aids in initializing these states during construction.
Primary constructors eliminate the need for declaring private fields and manually linking parameter values to those fields in constructor bodies. Say goodbye to that tedious process:
Notice that in non-record classes and structs, properties aren’t automatically generated for primary constructor parameters. You need to create them manually to indicate which data is accessible. This aligns with the typical usage of classes
More about primary constructor can be found in this post: C#12 class and struct Primary Constructors (naming conflict, interaction with other constructors, property, advanced usage of fields generated…).
C# 12 Using directives to alias any types
C# has long supported aliases for namespaces and named types (classes, delegates, interfaces, records, and structs). This feature effectively resolves naming conflicts when using directives may introduce ambiguity. Additionally, it simplifies the usage of complex generic types by providing concise alternatives. However, the language’s growing collection of complex type symbols has created a demand for aliases that are currently not permitted. Consider tuples, nullable and function-pointers, which often have lengthy and intricate textual representations that are cumbersome to write repeatedly and challenging to read. By allowing more aliases in C# 12, developers can assign shorter names to replace these extensive structural forms, thereby enhancing code readability and developer productivity.
1 2 3 4 5 6 7 8 |
using Person = (string FirstName, string LastName, int BorthYear); using ints = int[]; using DatabaseInt = int?; Method(("Bill", "Gates", 1955), new int[5], null); // Usage void Method(Person p, ints i, DatabaseInt db) { /*...*/} |
C# 12 Default values for lambda expressions
C# 12 boosts lambda expressions by enabling developers to set default parameter values. The syntax resembles that of other default parameters. For instance, (int val = 10) => val + 1;
. The default value 10
is utilized when no value is provided in the lambda call.
1 2 3 |
var lambdaWithDefaultParam = (int val = 10) => val + 1; Assert.IsTrue(lambdaWithDefaultParam() == 11); Assert.IsTrue(lambdaWithDefaultParam(4) == 5); |
C# 12 nameof operator improvement
The nameof
operator now works in C# 12 with member names, including static members, initializers and in attributes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System.Diagnostics; var a = new Assert(); // At debug time we get {Assert "Length"} for this object string playerName = "John"; // These 3x usages of nameof already worked before C# 12 Assert.IsTrue(nameof(playerName) == "playerName"); Assert.IsTrue(nameof(Assert.IsTrue) == "IsTrue"); Assert.IsTrue(nameof(Assert.S) == "S"); // This usage of nameof in attribute is now possible in C# 12 [DebuggerDisplay("Assert {nameof(S.Length)}")] class Assert { public static void IsTrue(bool b) { Debug.Assert(b); } public string S { get; } = ""; public static int StaticField; // These 2 usages of nameof are now possible in C# 12 public const string SLength = nameof(S.Length); public string MaxValue = nameof(StaticField.MaxValue); } |
C# 12 Inline arrays
The InlineArrayAttribute
has been introduced with .NET 8 and can be used from C# 12 programs. The principal purpose of this attribute is to designate a type as eligible for representation as a contiguous sequence of primitives. The size of an inline array is fixed at compile time. This makes it faster because the exact layout is known by the compiler that generates optimized IL code to access elements of the inline array. Also an inline array is a struct
which makes it allocated on the thread stack and not on the heap as with regular .NET arrays. Here is a short program based on .NET 8 inline arrays, notice how inline array element access is similar to regular array element access through inlineArray[i]
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var inlineArray = new MyInlineArray<int>(); InlineArrayAccess(inlineArray); // Print 0 since the original array is passed by value // It is left untouched by the call to InlineArrayAccess() Console.WriteLine(inlineArray[4]); static void InlineArrayAccess(MyInlineArray<int> inlineArray) { for (int i = 0; i < 10; i++) { inlineArray[i] = i * i; } // Print 16 since a copy of the array with squares is passed PrintElem(inlineArray, 4); } static void PrintElem(MyInlineArray<int> inlineArray, int index) { Console.WriteLine(inlineArray[index]); } [System.Runtime.CompilerServices.InlineArray(10)] // Size 10 is fixed here! public struct MyInlineArray<T> { // The definition of an inline array requires such unused field! private T _elem; } |
Clearly this C# 12 new feature is a niche one. It is chiefly aimed at utilization by the compiler, the .NET BCL, and select third-party libraries. But I guess for those that need more performance like in the game industry, inline arrays are welcome.
C# 12 Interceptors
C# 12 interceptors is a powerful – yet strange – new feature that we are going to demonstrate first. Then we’ll go through potential use-cases. To make work this short program you need a C# 12 compatible project with 2 files, Program.cs and MyInterceptors.cs. The csproj must look like this:
1 2 3 4 5 6 7 8 9 10 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <Features>InterceptorsPreview</Features> </PropertyGroup> </Project> |
Here is the content of Program.cs:
1 2 3 4 5 6 7 8 9 10 |
var obj = new Class(); obj.Method("1"); obj.Method("2"); obj.Method("3"); class Class { public void Method(string str) { Console.WriteLine($"Hi {str}"); } } |
Here is the content of MyInterceptors.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace System.Runtime.CompilerServices { // So far we must define ourselves this attribute! [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute { } } static class GeneratedCode { [System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\Users\pat\source\repos\ConsoleApp170\ConsoleApp170\Program.cs", 3,5)] public static void InterceptorMethod1(this Class obj, string str) { Console.WriteLine($"Hello {str}"); } [System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\Users\pat\source\repos\ConsoleApp170\ConsoleApp170\Program.cs", 4, 5)] public static void InterceptorMethod2(this Class obj, string str) { Console.WriteLine($"Bonjour {str}"); } } |
This program outputs:
1 2 3 |
Hi 1 Hello 2 Bonjour 3 |
If we decompile the assembly generated it with ILSpy we can see that calls to Method()
have been hijacked by the compiler:
1 2 3 4 5 6 7 |
// Program private static void <Main>$(string[] args) { Class obj = new Class(); obj.Method("1"); obj.InterceptorMethod1("2"); obj.InterceptorMethod2("3"); } |
Notice that if you change anything in:
System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\Users\pat\source\repos\ConsoleApp170\ConsoleApp170\Program.cs", 3,5)]
the compiler emits an error because online a valid triple of (absolute file path + line + column) can work.
Why are they planning such a weird feature in C# 12? The primary rationale is Ahead-Of-Time compilation (AOT) that becomes first citizen with .NET 8. While interceptors aren’t exclusively tailored to AOT, their design unmistakably takes AOT into account. Through the employment of interceptors, it becomes feasible to transform code that previously posed challenges for AOT into versions generated directly from the source.
The GeneratedCode
class will be generated by a tool and developer won’t have to even change their code.
Conclusion
This article delves into new features introduced in C# 12 Preview. Microsoft is dedicated to enhancing the C# language, prioritizing efficiency and developer-friendliness. Expect further advancements in the near future.