We’re excited to preview three new features for C# 12:
- Primary constructors for non-record classes and structs
- Using aliases for any type
- Default values for lambda expression parameters
In addition to this overview, you can also find detailed documentation in the What’s new in C# article on Microsoft Learn.
To test out these features yourself, you can download the latest Visual Studio 17.6 preview or the latest .NET 8 preview. Find out what else is coming for developers in Announcing .NET 8 Preview 3 and other posts on the .NET blog.
Primary constructors for non-record classes and structs
Primary constructors let you add parameters to the class declaration itself and use these values in the class body. For example, you could use the parameters to initialize properties or in the code of methods and property accessors. Primary constructors were introduced for records in C# 9 as part of the positional syntax for records. C# 12 extends them to all classes and structs.
The basic syntax and usage for a primary constructor is:
public class Student(int id, string name, IEnumerable<decimal> grades)
{
public Student(int id, string name) : this(id, name, Enumerable.Empty<decimal>()) { }
public int Id => id;
public string Name { get; set; } = name.Trim();
public decimal GPA => grades.Any() ? grades.Average() : 4.0m;
}
The primary constructor parameters in the Student
class above are available throughout the body of the class. One way you can use them is to initialize properties. Unlike records, properties are not automatically created for primary constructor parameters in non-record classes and structs. This reflects that non-record classes and structs often have more complexity than records, combining data and behavior. As a result, they often need constructor parameters that should not be exposed. Explicitly creating properties makes it obvious which data is exposed, consistent with the common use of classes. Primary constructors help avoid the boilerplate of declaring private fields and having trivial constructor bodies assign parameter values to those fields.
When primary constructor parameters are used in methods or property accessors (the grades
parameter in the Student
class), they need to be captured in order for them to stay around after the constructor is done executing. This is similar to how parameters and local variables are captured in lambda expressions. For primary constructor parameters, the capture is implemented by generating a private backing field on the class or struct itself. The field has an “unspeakable” name, which means it will not collide with other naming and is not obvious via reflection. Consider how you assign and use primary constructor parameters to avoid double storage. For example, name
is used to initialize the auto-property Name
, which has its own backing field. If another member referenced the parameter name
directly, it would also be stored in its own backing field, leading to an unfortunate duplication.
A class with a primary constructor can have additional constructors. Additional constructors must use a this(…)
initializer to call another constructor on the same class or struct. This ensures that the primary constructor is always called and all the all the data necessary to create the class is present. A struct type always has a parameterless constructor. The implicit parameterless constructor doesn’t use a this()
initializer to call the primary constructor. In the case of a struct, you must write an explicit parameterless constructor to do if you want the primary constructor called.
Find out more in the What’s new in C# 12 article with links to all new content for primary constructors.
You can leave feedback on primary constructors in the CSharpLang GitHub repository at Preview Feedback: C# 12 Primary constructors.
Using directives for additional types
C# 12 extends using directive support to any type. Here are a few examples:
using Measurement = (string, int);
using PathOfPoints = int[];
using DatabaseInt = int?;
You can now alias almost any type. You can alias nullable value types, although you cannot alias nullable reference types. Tuples are particularly exciting because you can include element names and types:
using Measurement = (string Units, int Distance);
You can use aliases anywhere you would use a type. For example:
public void F(Measurement x)
{ }
Aliasing types lets you abstract the actual types you are using and lets you give friendly names to confusing or long generic names. This can make it easier to read your code.
Find out more in the What’s new in C# 12 article.
You can leave feedback on aliases for any type in the CSharpLang GitHub repository at Preview Feedback: C# 12 Alias any type.
Default values for lambda expressions
C# 12 takes the next step in empowering lambda expressions by letting you specify default values for parameters. The syntax is the same as for other default parameters:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
Similar to other default values, the default value will be emitted in metadata and is available via reflection as the DefaultValue
of the ParameterInfo
of the lambda’s Method
property. For example:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault.Method.GetParameters()[0].DefaultValue; // 2
Prior to C# 12 you needed to use a local function or the unwieldy DefaultParameterValue
from the System.Runtime.InteropServices
namespace to provide a default value for lambda expression parameters. These approaches still work but are harder to read and are inconsistent with default values on methods. With the new default values on lambdas you’ll have a consistent look for default parameter values on methods, constructors and lambda expressions.
Find out more in the article on What’s new in C# 12.
You can leave feedback on default values for lambda parameters in the CSharpLang GitHub repository at Preview Feedback: C# 12 Default values in lambda expressions.
Next steps
We hope you will download the preview and check out these features. We are experimenting in C# 12 with a dedicated issue for each feature. We4 hope this will focus feedback and make it easier for you to upvote what other people are saying. You can find these at Preview Feedback: C# 12 Primary constructors, Preview Feedback: C# 12 Alias any type, and Preview Feedback: C# 12 Default values in lambda expressions.
You can follow our implementation progress for C# 12 at Roslyn Feature Status. You can also follow the design process at the CSharpLang GitHub repository where you will find proposals, discussions and meeting notes.
We look forward to hearing from you!
Does type aliasing support anonymous type?
Rather than multiple language features that just add different syntax for doing the same things, I'd much rather the team focused on a core set of feature changes that add significant value (e.g. like when generics were added).
I would gladly ditch all of these recent alternative syntax features for Extension Everything (e.g. extension properties etc). This has massive community backing, and it's something that'll add huge value to C#. We got a little...
I wanted to respond with a thank you Kathleen.
I have been hoping this exact primary constructor feature would be added for a number of years. Anyone who heavily uses dependency injection, (which is used throughout ASP.NET) will love this feature. This will greatly improve the readability of my projects and given I have noted some negative reactions in the comments, I wanted to take the time to say this will be a great...
Hi Kathleen,
While I am having trouble finding the totality of usefulness for all these new features, I can foresee instances where I will like having them. My nitpicking concern here is more well-spoken in the other comments - I am not sure these new features will be intuitive enough to be appreciated by newer C# developers.
That aside, thank you for the update and I appreciate the work you and the rest of the Dot Net...
You MS guys are killing the language. And you are doing it fast. What a fantastic, clear and simple language it used to be, and what it is now. When reading modern C# code in GitHub I often simply do not understand what’s going on. I would need to spend 1 month each year just to keep up with new features. It took me couple weeks to learn C# 2 when I moved from Java,...
For me C# is getting more and more complex on every version. It took a long time until I figured out when to use classes and when structs. And I'm not alone, look at stackoverflow, there are top votes questions about the differences. Then records got added. I never used them, I still have no idea what their purpose it. The new switch syntax is so complicated to write, I always write the normal one...
Make wheel, again.. again.. again.. lol
Nice features! Thanks for sharing! 😀
Will “safe fixed buffer” become part of C# 12?
We are discussing it, which you can watch in the LDM notes. We would like it to be part of C# 12, but have not yet settled the details. Some of the details are challenging.
Would limiting it to unmanaged structs make it simpler? This would add a lot of value, for people who deal with low level network datagrams, IoT and embedded. Like struct MyDatagram { public Point points[10]; }. Fingers crossed!
Anyone want to champion local static variables? https://github.com/dotnet/csharplang/discussions/832
I still miss static extension methods thou:-)
Yes please! Do this – I have been waiting for this since extension methods came out.