Learn All About The New C# 12 Features!

NET category image

Here are code examples and explanations so that you can learn all about the new C# 12 features that were published recently.

New year, new .NET version. And along with .NET8 comes C# 12 which brings us some fancy new features. Learn all about the new c# 12 features in this article!

How to enable it

Upgrade your .NET projects to .NET8 by changing the <TargetFramework> property in your .csproj files to net8.0. Then, you can use all new C# 12 features.

Enable C# 12 by setting the target framework to .NET 8 in a .csproj file
Enable C# 12 by setting the target framework to .NET 8 in a .csproj file

Alias any type

They have been part of the language since the beginning and you probably know them. The news is also rather small but you can now use any type instead of only named types when creating aliases. Here are a few examples:

C#
using NewAlias = int;          // referencing the int type instead of System.Int32
using Point = (int x, int y);  // referencing a tuple
using List = [1,2,3];          // referencing an array

full specification

This change is nice but irrelevant to me. I don’t use aliases at all. The only reason for me to create an alias would be if there are conflicting imports with identical names. But apart from that, I’d rather use classes or struct instead of aliases.

Optional Lambda expression parameters

Optional parameters for methods in C# are known to everyone. But with C# 12, your Lambda function can also have optional parameters and even parameter arrays. Here is how it works:

C#
// Lambda function with default parameter
var addDefault = (int toAdd = 5) => toAdd + 1;
addDefault();  // 6
addDefault(4); // 5
addDefault(0); // 1

// Lambda function with parameter list
var counter = (params int[] numbers) => numbers.length;
counter();      // 0
counter(1,1,1); // 3
counter(1,2,3); // 3

// method group with default parameter
var addDefault = Add;
addDefault();  // 6
addDefault(4); // 5
addDefault(0); // 1

// method group with parameter list
var counter = Count;
counter();      // 0
counter(1,1,1); // 3
counter(1,2,3); // 3

// method with default parameter
int Add(int toAdd = 5) {
  return toAdd + 1;
}

// method with parameter list
int Count(params int[] numbers) {
  return numbers.Length;
}

full specification

Once again, this is irrelevant for me. The only possible way for me to use this would be ICommand handlers. Maybe, if you work with callbacks a lot, this could be something for you. Apart from that, I am not sure how this could be useful.

Collection expressions

Creating collections with C# 12 got a lot easier than before. Instead of using a different syntax for different collection types, there is now a unified syntax that works with all collections. Again, here is some code:

C#
// collections with C# 11
int[] x1 = new int[] { 1, 2, 3, 4 };
int[] x2 = Array.Empty<int>();
WriteByteArray(new[] { (byte)1, (byte)2, (byte)3 });
List<int> x4 = new() { 1, 2, 3, 4 };
Span<DateTime> dates = stackalloc DateTime[] { GetDate(0), GetDate(1) };
WriteByteSpan(stackalloc[] { (byte)1, (byte)2, (byte)3 });

// collections with C# 12
int[] x1 = [1, 2, 3, 4];
int[] x2 = [];
WriteByteArray([1, 2, 3]);
List<int> x4 = [1, 2, 3, 4];
Span<DateTime> dates = [GetDate(0), GetDate(1)];
WriteByteSpan([1, 2, 3]);

// you can also combine lists with the spread (..) operator
List<int> x5 = [1, .. x4, 2, .. x2, 3, .. x1];

full specification

That is a great enhancement in my opinion. I especially love the spread (..) operator because I already know it from other languages. An easier and more unified syntax is always appreciated.

Ref readonly parameters

ref readonly parameters close a gap between in and ref parameters. There are some tricky edge cases that cannot be solved properly with in or ref and that’s where the new parameter comes into play. There is a great explanation of such a case on StackOverflow. Feel free to read it for details.

full specification

While I can live with in and out parameters, ref has always been something that I tried to avoid if possible. ref readonly is only for very specific use cases and the majority of developers won’t ever use it.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!


Inline arrays

Inline arrays are a way to squeeze some extra performance out of your .NET app and make it as fast as possible. It’s an array with a fixed size in a struct type. The compiler can make use of the known information and optimize it to the max. If you work with System.Span<T> or System.ReadOnlySpan<T>, you are already using inline arrays in C# 12.

Here is a code example of how to declare an inline array:

C#
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
    private int _element0;
}

Working with inline arrays is not different from working with a normal array. Here is how you use it:

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

foreach (var i in buffer)
{
    Console.WriteLine(i);
}

full specification

Nice if you need some extra speed boost in your app!

Primary constructors

Primary constructors were introduced on records before, but can now also be used on classes and structs. It reduces the boilerplate code and gives us an easier option to initialize member fields or properties.

Here is a comparison between C# 11 with standard constructors and C# 12 with primary constructors:

C#
// C# 12
public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = accountID;
    public string Owner { get; } = owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

// C# 11
public class BankAccount
{
    public string AccountID { get; }
    public string Owner { get; }

    public BankAccount(string accountID, string owner) 
    {
        AccountID = accountID;
        Owner = owner;
    }

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

You can also get the same behavior as the code example above by using the required and init keywords. With that approach, you can get rid of the constructor entirely if there is no call to a base class involved.

C#
public class BankAccount
{
    public required string AccountID { get; init; }
    public required string Owner { get; init; }

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

full specification

I like this new feature. It gives us a new option to initialize properties and fields in classes. There should now be a matching style for every type of developer available in C#.


Here is the official blog post of Microsoft about the announcement of C# 12.

Conclusion

In this article, you could learn all about the new C# 12 features. Microsoft put a lot of effort into improving the language, adapting to new trends, and simplifying existing concepts. Overall, C# is in a good state, but you can also see that big new changes are not on the agenda.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Become A Firebase Expert!

Take a deep dive into Firebase and learn how to really handle their services. This ebook makes you an instant Firebase professional!

Flutter โค๏ธ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!