C# 12 arrived in November with .NET 8, bringing several new features—primary constructors, collection expressions, inline arrays, and more—that make it simpler and easier to write more efficient code. Just as we walked through the .NET 8 highlights previously, in this article we’ll take a close look at the key new features in C# 12.
To work with the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.
Create a console application project in Visual Studio
First off, let’s create a .NET Core console application project in Visual Studio. Assuming you have Visual Studio 2022 installed, follow the steps outlined below to create a new .NET Core console application project in Visual Studio.
- Launch the Visual Studio IDE.
- Click on “Create new project.”
- In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
- Click Next.
- In the “Configure your new project” window, specify the name and location for the new project.
- Click Next.
- In the “Additional information” window shown next, choose “.NET 8.0 (Long Term Support)” as the framework version you would like to use.
- Click Create.
We’ll use this .NET 8 console application project to work with the new C# 12 features in the subsequent sections of this article.
Primary constructors in C# 12
Primary constructors are a new feature in C# 12 that enable you to declare constructors whose parameters are available throughout the body of the type. They enable you to declare constructors inline with the type declaration thereby making the syntax more precise and concise.
You can now create primary constructors in any struct or a class. You are no longer confined to creating primary constructors in record types only. By using primary constructors, you no longer need separate constructor definitions.
The following code snippet illustrates a primary constructor declared inside a struct.
public readonly struct Rectangle(double x, double y) { public readonly double Area { get; } = x * y; }
You would typically want to use a primary constructor in any of the following scenarios:
- To initialize a member or a field of the containing type
- As an argument when calling the base() constructor
- To reference a constructor parameter in an instance member of the containing type
Below is a simple implementation of a primary constructor declared inside a class.
public class Author(int Id, string firstName, string lastName) { public int Id { get; } = Id; public string FirstName { get; } = firstName; public string LastName { get; } = lastName; public override string ToString() => $"Author Id: {Id}, First Name: {FirstName}, Last Name: {LastName}"; }
Note that the ToString() method has been overridden in the Author class. As a result, you can create an instance of the Author class by passing parameters to its primary constructor and then call the ToString() method on the instance to display the value of those properties. This is shown in the code snippet given below.
Author author = new Author(1, "Joydip", "Kanjilal"); Console.WriteLine(author.ToString());
Collection expressions in C# 12
Before C# 12, you had to use a different syntax to initialize a List<int> collection compared to an int[] or Span<int>. With the introduction of the collection expressions feature in C# 12, you can now use a more concise syntax when creating collections such as arrays, lists, and dictionaries.
You can use collection expressions to populate collections with predefined values in different scenarios, and to directly initialize a collection with its elements, eliminating the need for multiple lines of code. Below is an example of using a collection expression to initialize a list of strings.
List daysOfWeek = new() { "Sunday","Monday", "Tuesday","Wednesday", "Thursday", "Friday", "Saturday" };
In the above code snippet, the collection expression { “Sunday”,”Monday”, “Tuesday”,”Wednesday”, “Thursday”, “Friday”, “Saturday” } is used to initialize the list. As you can see, this syntax is much more concise and readable than adding each element of the collection separately.
Inline arrays in C# 12
An inline array is a struct-based array of fixed size that you can use to increase the efficiency of your code when managing buffers. Before inline arrays, you could manipulate memory blocks using stackalloc or pointers—however, such techniques required you to mark your assembly as unsafe using the unsafe keyword. With C# 12, you can declare an inline array to work with a memory block without using the unsafe keyword.
Here is how you can declare an inline array in C# 12:
[System.Runtime.CompilerServices.InlineArray(50)] public struct Buffer { private int _element; }
You can now use your inline array much the same way you would use any other array in C#.
Default lambda parameters in C# 12
C# 12 allows you to specify default parameter values in lambda expressions much like you would for a method or a local function in C#. This feature can make your lambda expressions more flexible and expressive. The following code snippet illustrates how you can specify a default parameter value in a lambda expression in C# 12.
var AddIntegers = (int x, int y = 1) => x + y;
You can then call the lambda expression as shown below.
Console.WriteLine(AddIntegers(10)); Console.WriteLine(AddIntegers(10, 5));
Ref readonly parameters in C# 12
Support for ref readonly parameters was initially introduced in C# 7.2, enabling you to pass parameters by reference in a read-only context, hence turning off any modifications to the parameter.
C# 12 builds upon this feature by allowing you to use ref readonly parameters in several other scenarios. You should use ref readonly parameters in a method when you will not modify the parameter value but will only access its memory location.
Here’s an example of how we can use ref readonly parameters in C# 12:
void Display(in int x, in int y) { Console.WriteLine($"The value of x is : {x}, the value of y is : {y}"); }
In the above code snippet, the Display method accepts two parameters, x and y, both as in parameters. The usage of the in keyword in the method parameters implies that these parameters will be passed by readonly reference, due to which they cannot be modified. You might want to use ref readonly parameters when you want to pass large objects to methods by reference and avoid the cost of unnecessary method copying while ensuring the immutability of the parameters.
Interceptors in C# 12
C# 12 includes yet another exciting feature known as interceptors. Interceptors can help you to replace or intercept a method call with an alternative method. You can use this feature to reroute method calls without changing the original piece of code.
Interceptors are an experimental feature available in preview mode. They are not recommended for production use because Microsoft could make changes to this feature in future releases of the language. You can read more about interceptors here.
TargetFramework .NET 8
Note that you will need to have .NET 8 installed in your computer to work with C# 12. If you want to change your existing projects to use C# 12, you will need to specify the TargetFramework to .NET 8 as shown in the code snippet given below.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> ... </PropertyGroup> </Project>
C# 12 makes working with C# easier than ever. You can learn more about the new features in C# 12 here and here.