What's new in C# 12?
Another release, another set of C# features. In this one we will explore the new features that were added in C# 12. This release does not bring any groundbreaking features but there are some useful ones.
Primary Constructors
Previously, defining constructors in C# required more verbose syntax. For each property, you had to explicitly declare private fields (if needed), write a constructor with parameters, and then assign these parameters to your properties or fields.
public class Cat
{
public string Name { get; }
public Cat(string name)
{
Name = name;
}
public void Meow()
{
Console.WriteLine($"{Name} shouts: Meow");
}
}
Let's see an example with the new feature used:
public class Cat(string name)
{
public string Name { get; } = name;
public void Meow()
{
Console.WriteLine($"{Name} shouts: Meow");
}
}
public struct Dog(string name)
{
public string Name { get; set; } = name;
public void Bark()
{
Console.WriteLine($"{Name} shouts: Bark");
}
}
As we saw this feature can also be used with structs.
Some advantages of this feature are:
- Reduced Boilerplate: The primary constructor syntax allows for declaring constructor parameters directly with the class or struct declaration.
- Conciseness: This feature makes the code more concise and readable, focusing on the essential aspects of the class or struct without cluttering it with repetitive constructor logic.
- Clarity: By directly associating constructor parameters with the class or struct declaration, it's immediately clear what parameters are essential for creating an instance, enhancing code clarity.
But not everything is great about this feature. Unlike records, which promote immutability by default, primary constructors initialize private fields that are mutable. This mutability means that any method within the class can change these values, potentially leading to unintended side effects or making the class's state management more complex.
Collection Expressions
Until today the way to initialize enumerables was as simple as this:
int[] numbers = new int[] {1, 2, 6};
Span<int> span = new Span<int>(new int[] {9, 4, 5});
var combined = numbers.Concat(span.ToArray()).ToArray();
List<Cat> list = new List<Cat> { new Cat("John"), new Cat("Snow") };
This feature allows us to simplify our initializations for most enumerables. Now we can do it like this:
int[] numbers = [1,2,6];
Span<int> span = [9,4,5];
int[] final = [..numbers, ..span]; // [1, 2, 6, 9, 4, 5]
Cat[] array = [new Cat("John"), new Cat("Snow")];
Advantages of this feature:
-
Simplicity and Readability: The new syntax simplifies collection initialization, making code easier to read and write. It's more intuitive, especially for those familiar with collection literals in other programming languages.
-
Conciseness: Collection expressions reduce the need for explicit type declarations and the new keyword in certain contexts, allowing for more concise code.
-
Flexibility: This feature introduces the ability to easily concatenate collections with the spread operator (..), simplifying the combination of multiple collections into one.
This feature is not yet complete since we cannot initialize a dictionary this way unless it is empty. (No jokes about dics this time). Another issue is the learning curve because most C# devs might not be familiar with this syntax.
Ref Readonly Parameters
In order to prevent value types passed by reference to be changed inside the method, you can use the in keyword. From C# 12 you can also use the ref readonly. Let's use an example for better understanding.
class RefReadonlyExample
{
public void Method(in int number)
{
number++; // won't compile because it is a readonly variable
}
public void Method2(ref readonly int number)
{
number++; // will compile because it is a reference variable
}
}
This is mostly added for Microsoft and you can read more about it here.
Default Lambda Parameters
Before C# 12 we could not have default values for our lambdas. Now, we able to do so.
var lambda = (int yearsForDiscriminatedUnions = 420) => $"This is how many years are left: {yearsForDiscriminatedUnions}"
This feature brings a consistent experience across C#. It simplifies scenarios where a default value makes sense for the lambda's operation, reducing the need for additional overloads or wrapper methods and it can lead to cleaner code.
A great quality of life feature.
Alias Any Type
For this feature Microsoft loosened the rules where the using alias directive can be used.
Now we can do this:
using Point2d = (int x, int y);
Point2d point = (9, 6);
Aliasing allows for more readable and concise code. It can turn verbose type declarations into clear and understandable aliases. Very handy feature for any C# developer.
Inline Arrays
This is a feature that will be mostly by the runtime team in Microsoft.
Let's see an example real quick:
using System.Runtime.CompilerServices;
[InlineArray(10)]
public struct InlineBuffer
{
private int _element0;
}
You can read more about it here.
Basically it is a way to create an array of fixed size in a struct type.
Experimental Attribute
This allows Microsoft and library authors to annonate something as experimental to warn others that what they are using might be changed or be removed.
[Experimental("WillChange")]
public class TestClass
{
}
Another feature made for Microsoft, let's move on to the last one that is really interesting.
Interceptors
This is a preview feature and in order to use it you must specify which namespaces are allowed to use interceptors.
This is how you can make it work:
public static class Interceptor
{
[InterceptsLocation(
filePath: "path",
line: 120,
character: 300
)]
public static void InterceptMethod1(
this Example example)
{
Console.WriteLine("Intercepted Method1");
}
}
public class Example
{
public void Method1()
{
Console.WriteLine("Method1");
}
}
Final Thoughts
Most of these features are released to the public but are mostly made for Microsoft. What do you think? Which is your favorite feature? Which ones are you going to use? Leave a comment down below and as always, keep coding!
Discussion