No trial. No credit card required. Just your GitHub account.
Announcing C# 12
C# 12 is available today! You can get it by downloading.NET 8, the latestVisual Studio, orVisual Studio Code’s C# Dev Kit.
For your existing projects, you’ll also need to indicate that you want to change your language version. You can change your language version by changing yourTargetFramework
to .NET 8:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework>... </PropertyGroup></Project>
C# 12 brings better developer productivity with simplified syntax, and faster execution. You can check out details on each feature in theWhat’s new in C# 12 article on MS Learn. TheWhat’s new article includes links to updates across the C# documentation on MS Learn that reflect the new features.
Simplifying your code
Every version of C# helps you write better code – simpler code that better expresses your intent. Compared with the code you previously wrote, the new approach is as fast or faster and has the same or fewer allocations. You can adopt these new features with confidence. One design goal for new features is to ensure that adopting the new feature doesn’t degrade performance.
C# 12 introduces collection expressions, primary constructors for all classes and structs, syntax to alias any type, and default parameters for lambda expressions that simplify your code.
Collection expressions
Prior to C# 12, creating collections required different syntax for different scenarios. Initializing aList<int>
required different syntax than anint[]
orSpan<int>
. Here are just a few of the ways collections might be created:
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 });
Collection expressions are a unified syntax:
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]);
Not only can you use a single syntax, but the compiler creates fast code for you. In many cases, the compiler sets the collection capacity and avoids copying data.
And if this wasn’t enough – you can use the new spread operator to include the elements of one or more collections or enumerable expressions within a collection expression:
int[] numbers1 = [1, 2, 3];int[] numbers2 = [4, 5, 6];int[] moreNumbers = [.. numbers1, .. numbers2, 7, 8, 9];// moreNumbers contains [1, 2, 3, 4, 5, 6, 7, 8, ,9];
The implementation of any spread expression is optimized, and will often be better than the code you might write to combine collections.
We are very interested infeedback on possible future work on collection expressions. We are considering expanding collection expressions to include dictionaries and support forvar
(natural types) in a future version of C#.
Like many new C# features, analyzers can help you check out the new feature and update your code:
Find out more aboutcollection expressions in this article on MS Learn.
Primary constructors on any class or struct
C# 12 extends primary constructors to work on all classes and structs, not just records. Primary constructors let you define constructor parameters when you declare the class:
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}";}
The most common uses for a primary constructor parameter are:
- As an argument to a base() constructor invocation.
- To initialize a member field or property.
- Referencing the constructor parameter in an instance member.
- To remove boilerplate in dependency injection.
You can think of a primary constructor parameter as a parameter that is in scope for the entire class declaration.
You can add primary constructors to any type:class
,struct
,record class
andrecord struct
. When used onclass
andstruct
types, primary constructor parameters are in scope in the entireclass
orstruct
definition. You can use the parameters to initialize fields or properties, or in the body of other members. When used onrecord
types, the compiler generates a public property for each primary constructor parameter. Those properties are simply one of the many members automatically generated forrecord
types.
We are very interested infeedback on possible future work on primary constructors. We are considering allowing you to mark primary constructor parameters asreadonly
and to indicate that you want a public property created.
Find outmore about primary constructors in this article. To dive deeper into using primary constructors for records and non-records, check outTutorial: Explore primary constructors.
Alias any type
Aliasing types is a convenient way to remove complex type signatures from your code. Starting with C# 12, additional types are valid inusing
alias directives. For example, these aliases are not valid in earlier versions of C#:
using intArray = int[]; // Array types.using Point = (int x, int y); // Tuple typeusing unsafe ArrayPtr = int*; // Pointer type (requires "unsafe")
You can check out the feature specAllow using alias directive to reference any kind of Type forusing
alias with pointer and unsafe types.
Like otherusing
aliases, these types can be used at the top of a file and inglobal using
statements.
Find outmore about alias any type in this article.
Default lambda parameters
Starting in C# 12, you can declare default parameters in lambda expressions:
var IncrementBy = (int source, int increment = 1) => source + increment;Console.WriteLine(IncrementBy(5)); // 6Console.WriteLine(IncrementBy(5, 2)); // 7
Default lambda parameters let calling code skip passing values and lets you add parameters to existing lambda expressions without breaking calling code. This simplifies accessing lambda expressions in the same way default parameters in methods simplify calling methods.
Find outmore about default lambda parameters in this article.
Making your code faster
We continue to improve your ability to work with raw memory to improve application performance.
The performance improvements we have made in C# over the years are important, whether you directly use them or not. Most applications get faster because the .NET runtime and other libraries leverage these enhancements. Of course, if your application uses buffers of memory in hot paths, you can also take advantage of these features. They’ll make your app that much faster.
In C# 12, we addref readonly
parameters and inline arrays.
ref readonly parameters
The addition ofref readonly
parameters provides the final combination of passing parameters by reference or by value. An argument to aref readonly
parameter must be a variable. Similar toref
andout
arguments, the argument shouldn’t be a literal value or a constant. A literal argument generates a warning and the compiler creates a temporary variable. Likein
parameters, aref readonly
parameter can’t be modified. A method should declareref readonly
parameters when that method won’t modify the argument, but needs its memory location.
Find outmore aboutref readonly
parameters in this article.
inline arrays
Inline arrays provide a safe way to work with memory buffers. An inline array is a struct-based, fixed length array type. You have been able to manipulate a block of memory usingstackalloc
storage or pointers. But those techniques required that your assembly enable unsafe code. When your application needs to work with a block of memory to store an array of structures, you can now declare an inline array type. That type represents a fixed size array. You can use them in safe code, and improve your app’s performance when manipulating buffers.
Find outmore about inline arrays in this article.
Helping us go faster
Occasionally we add features to C# as experiments or to make developing C# or .NET more efficient. C# 12 brings two of these features: the experimental attribute and interceptors.
Experimental attribute
We occasionally place features into released versions of .NET or C# because we want feedback or the feature cannot be completed in a single cycle. In these cases, we want to make it clear that we are not yet committed to the feature or the implementation. We added theSystem.Diagnostics.CodeAnalysis.ExperimentalAttribute
to better clarify when this occurs.
When code uses types or members that are experimental, an error will occur unless the calling code is also marked as experimental. Each use ofExperimentalAttribute
includes a diagnostic ID, which lets you suppress the error for individual experimental features by an explicit compiler option or #pragma so you can explore an experimental feature.
Types, members, and assemblies can be marked with theExperimentalAttribute
. If a type is marked as experimental, all of its members are considered experimental. If an assembly or module is marked as experimental, all of the types in it are marked as experimental.
We highly recommend that library authors with dependencies on anything with anExperimental
attribute also mark all code using it with theExperimentalAttribute
. We also encourage library authors to useExperimentalAttribute
if they have experimental features in their libraries.
Find outmore about the experimental attribute in this article.
Interceptors
Interceptors are an experimental feature, available in preview mode with C# 12. The feature may be subject to breaking changes or removal in a future release. Therefore, it is not recommended for production or released applications. If you use interceptors, mark your library with theExperimentalAttribute
.
Interceptors allow redirection of method calls. For example, this would allow an optimized version of a method generated for the specific parameters to replace a less efficient generalized method.
If you’re interested, you can learn more about interceptors by reading theInterceptors feature specification.
Next steps
C# 12 is just one part of the exciting .NET 8 release. You can learn about other features inthe .NET 8 blog post.
Download.NET 8,Visual Studio 2022 17.8 and check out C# 12!
Author
Kathleen Dollard loves to code and loves to teach and talk about code. She’s written tons of articles, a book, and spoken around the world. She’s on the .NET Team at Microsoft where she works on the .NET Core CLI and SDK and managed languages (C# and Visual Basic.NET). She’s always ready to help developers take the next step in exploring the wonderful world we call code.
12 comments
Discussion is closed.Login to edit/delete existing comments.
Andrew Mansell· Edited Primary constructors are useful, however it’s a real shame their params can’t be marked as
readonly
. We’ve just spent quite some time ensuring our codebase usesreadonly
wherever possible to stop obvious bugs creeping in. You can assign the PC parameter to areadonly
backing field, but that just reintroduces boilerplate that the feature was trying to address.MaxiTB Primary constructors result in way less readable code, because they result in sometimes insanely long lines hiding the most important part of a type: It’s inheritance.
For records it’s often no big deal, but for everything else it actually matters.Is there an option in Visual Studio Code Style settings to modify which constructor variant is preferred for each record & non-record types ?
Damien Villeneuve Can we create a Dictionary<string, int> d = [ [ “key1”, 0 ], [ “key2”, 1 ] ]; ?
Alexey Gvozdikov· Edited I would prefer:
Dictionary<string, int> d = { "key1" = 0, “key2” = 1 };
Kathleen Dollard We’re looking at dictionaries and what syntax makes the most sense. The comma used for both the key/value separator and the collection delimiter feels a bit odd to me – perhaps a semicolon, equals or colon for key/value pairs.
Muneeb Baig Good to see the spread operator for collection expressions 🫡. Next stop is to add the same for properties as used in JS.; with optimization off course.
MgSam Congrats on the release!!
You left out
int[] a = { 1, 2, 3}
as an existing array initialization format. I hope the analyzers are shipped with VS and are extremely aggressive in changing the older initialization expression types. I fear we’re quickly becoming C++ with 100 different ways to do the identical action.Can you clarify the state of generics in “Alias any type”? I follow the language design closely and even I don’t remember where this was left off. Thanks!
Kathleen Dollard · Edited
Read moreYou are correct - there are a lot of ways to make lists prior to collection expressions - we just wanted to give a handful of examples.
For alias any type - you can use concrete generics, but not open generics.
<code>
We had too many questions about how people would expect open generics to behave, and we did not feel we had enough understanding of value.
Read lessYou are correct – there are a lot of ways to make lists prior to collection expressions – we just wanted to give a handful of examples.
For alias any type – you can use concrete generics, but not open generics.
using listOfInt = System.Collections.Generic.List<int>;using thisDoesNotWork = System.Collections.Generic.List<T>; // Error that T is not a type
We had too many questions about how people would expect open generics to behave, and we did not feel we had enough understanding of value.
Alexey Gvozdikov· Edited Why open generics are not allowed? If you do NOT understand smth – ask community!
Plain example:using abc<T> = Tuple<int, T>;
Why it’s still disallowed, despite it has absolutely clear logic?
Michael Taylor I think the code block editor messed up your example. From what I can see the 2 lines are identical except the type alias name.
Kathleen Dollard Thank you for pointing that out! I fixed it.