Today, we are happy to announce the release of C# 10 as part of .NET 6 and Visual Studio 2022. In this post, we’re covering a lot of the new C# 10 features that make your code prettier, more expressive, and faster.
Read the Visual Studio 2022 announcement and the .NET 6 announcement to find out more, including how to install.
Global and implicit usings
using
directives simplify how you work with namespaces. C# 10 includes a new global using
directive and implicit usings to reduce the number of usings you need to specify at the top of each file.
Global using directives
If the keyword global
appears prior to a using
directive, that using applies to the entire project:
global using System;
You can use any feature of using
within a global using
directive. For example, adding static
imports a type and makes the type’s members and nested types available throughout your project. If you use an alias in your using directive, that alias will also affect your entire project:
global using static System.Console;
global using Env = System.Environment;
You can put global usings in any .cs
file, including Program.cs
or a specifically named file like globalusings.cs
. The scope of global usings is the current compilation, which generally corresponds to the current project.
For more information, see global using directives.
Implicit usings
The Implicit usings feature automatically adds common global using
directives for the type of project you are building. To enable implicit usings set the ImplicitUsings
property in your .csproj
file:
<PropertyGroup>
<!-- Other properties like OutputType and TargetFramework -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Implicit usings are enabled in the new .NET 6 templates. Read more about the changes to the .NET 6 templates at this blog post.
The specific set of global using
directives included depend on the type of application you are building. For example, implicit usings for a console application or a class library are different than those for an ASP.NET application.
For more information, see this implicit usings article.
Combining using features
Traditional using
directives at the top of your files, global using
directives, and implicit usings work well together. Implicit usings let you include the .NET namespaces appropriate to the kind of project you’re building with a single line in your project file. global using
directives let you include additional namespaces to make them available throughout your project. The using
directives at the top of your code files let you include namespaces used by just a few files in your project.
Regardless of how they are defined, extra using
directives increase the possibility of ambiguity in name resolution. If you encounter this, consider adding an alias or reducing the number of namespaces you are importing. For example, you can replace global using
directives with explicit using directives at the top of a subset of files.
If you need to remove namespaces that have been included via implicit usings, you can specify them in your project file:
<ItemGroup>
<Using Remove="System.Threading.Tasks" />
</ItemGroup>
You can also add namespace that behave as though they were global using
directives, you can add Using
items to your project file, for example:
<ItemGroup>
<Using Include="System.IO.Pipes" />
</ItemGroup>
File-scoped namespaces
Many files contain code for a single namespace. Starting in C# 10, you can include a namespace as a statement, followed by a semi-colon and without the curly brackets:
namespace MyCompany.MyNamespace;
class MyClass // Note: no indentation
{ ... }
This simplifies the code and removes a level of nesting. Only one file-scoped namespace declaration is allowed, and it must come before any types are declared.
For more information about file-scoped namespaces, see the namespace keyword article.
Improvements for lambda expressions and method groups
We’ve made several improvements to both the types and the syntax surrounding lambdas. We expect these to be widely useful, and one of the driving scenarios has been to make ASP.NET Minimal APIs even more straightforward.
Natural types for lambdas
Lambda expressions now sometimes have a “natural” type. This means that the compiler can often infer the type of the lambda expression.
Up until now a lambda expression had to be converted to a delegate or an expression type. For most purposes you’d use one of the overloaded Func<...>
or Action<...>
delegate types in the BCL:
Func<string, int> parse = (string s) => int.Parse(s);
Starting with C# 10, however, if a lambda does not have such a “target type” we will try to compute one for you:
var parse = (string s) => int.Parse(s);
You can hover over var parse
in your favorite editor and see that the type is still Func<string, int>
. In general, the compiler will use an available Func
or Action
delegate, if a suitable one exists. Otherwise, it will synthesize a delegate type (for example, when you have ref
parameters or have a large number of parameters).
Not all lambdas have natural types – some just don’t have enough type information. For instance, leaving off parameter types will leave the compiler unable to decide which delegate type to use:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
The natural type of lambdas means that they can be assigned to a weaker type, such as object
or Delegate
:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
When it comes to expression trees we do a combination of “target” and “natural” typing. If the target type is LambdaExpression
or the non-generic Expression
(base type for all expression trees) and the lambda has a natural delegate type D
we will instead produce an Expression<D>
:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Natural types for method groups
Method groups (that is, method names without argument lists) now also sometimes have a natural type. You have always been able to convert a method group to a compatible delegate type:
Func<int> read = Console.Read;
Action<string> write = Console.Write;
Now, if the method group has just one overload it will have a natural type:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Return types for lambdas
In the previous examples, the return type of the lambda expression was obvious and was just being inferred. That isn’t always the case:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
In C# 10, you can specify an explicit return type on a lambda expression, just like you do on a method or a local function. The return type goes right before the parameters. When you specify an explicit return type, the parameters must be parenthesized, so that it’s not too confusing to the compiler or other developers:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Attributes on lambdas
Starting in C# 10, you can put attributes on lambda expressions in the same way you do for methods and local functions. They go right where you expect; at the beginning. Once again, the lambda’s parameter list must be parenthesized when there are attributes:
Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";
Just like local functions, attributes can be applied to lambdas if they are valid on AttributeTargets.Method
.
Lambdas are invoked differently than methods and local functions, and as a result attributes do not have any effect when the lambda is invoked. However, attributes on lambdas are still useful for code analysis, and they are also emitted on the methods that the compiler generates under the hood for lambdas, so they can be discovered via reflection.
Improvements to structs
C# 10 introduces features for structs that provide better parity between structs and classes. These new features include parameterless constructors, field initializers, record structs and with
expressions.
Parameterless struct constructors and field initializers
Prior to C# 10, every struct had an implicit public parameterless constructor that set the struct’s fields to default
. It was an error for you to create a parameterless constructor on a struct.
Starting in C# 10, you can include your own parameterless struct constructors. If you don’t supply one, the implicit parameterless constructor will be supplied to set all fields to their default. Parameterless constructors you create in structs must be public and cannot be partial:
public struct Address
{
public Address()
{
City = "<unknown>";
}
public string City { get; init; }
}
You can initialize fields in a parameterless constructor as above, or you can initialize them via field or property initializers:
public struct Address
{
public string City { get; init; } = "<unknown>";
}
Structs that are created via default
or as part of array allocation ignore explicit parameterless constructors, and always set struct members to their default values. For more information about parameterless constructors in structs, see the struct type.
record structs
Starting in C# 10, records can now be defined with record struct
. These are similar to record classes that were introduced in C# 9:
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
You can continue to define record classes with record
, or you can use record class
for clarity.
Structs already had value equality – when you compare them it is by value. Record structs add IEquatable<T>
support and the ==
operator. Record structs provide a custom implementation of IEquatable<T>
to avoid the performance issues of reflection, and they include record features like a ToString()
override.
Record structs can be positional, with a primary constructor implicitly declaring public members:
public record struct Person(string FirstName, string LastName);
The parameters of the primary constructor become public auto-implemented properties of the record struct. Unlike record classes, the implicitly created properties are read/write. This makes it easier to convert tuples to named types. Changing return types from a tuple like (string FirstName, string LastName)
to a named type of Person
can clean up your code and guarantee consistent member names. Declaring the positional record struct is easy and keeps the mutable semantics.
If you declare a property or field with the same name as a primary constructor parameter, no auto-property will be synthesized and yours will be used.
To create an immutable record struct, add readonly
to the struct (as you can to any struct) or apply readonly
to individual properties. Object initializers are part of the construction phase where readonly properties can be set. Here is just one of the ways you can work with immutable record structs:
var person = new Person { FirstName = "Mads", LastName = "Torgersen"};
public readonly record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Find out more about record structs in this article.
sealed
modifier on ToString()
in record classes
Record classes have also been improved. Starting in C# 10 the ToString()
method can include the sealed modifier, which prevents the compiler from synthesizing a ToString
implementation for any derived records.
Find out more about ToString()
in records in this article.
with
expressions on structs and anonymous types
C# 10 supports with
expressions for all structs, including record structs, as well as for anonymous types:
var person2 = person with { LastName = "Kristensen" };
This returns a new instance with the new value. You can update any number of values. Values you do not set will retain the same value as the initial instance.
Learn more about with
in this article
Interpolated string improvements
When we added interpolated strings to C#, we always felt that there was more that could be done with that syntax down the line, both for performance and expressiveness. With C# 10, that time has come!
Interpolated string handlers
Today the compiler turns interpolated strings into a call to string.Format
. This can lead to a lot of allocations – the boxing of arguments, allocation of an argument array, and of course the resulting string itself. Also, it leaves no wiggle room in the meaning of the actual interpolation.
In C# 10 we’ve added a library pattern that allows an API to “take over” the handling of an interpolated string argument expression. As an example, consider StringBuilder.Append
:
var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");
Up until now, this would call the Append(string? value)
overload with a newly allocated and computed string, appending that to the StringBuilder
in one chunk. However, Append
now has a new overload Append(ref StringBuilder.AppendInterpolatedStringHandler handler)
which takes precedence over the string overload when an interpolated string is used as argument.
In general, when you see parameter types of the form SomethingInterpolatedStringHandler
the API author has done some work behind the scenes to handle interpolated strings more appropriately for their purposes. In the case of our Append
example, the strings "Hello "
, args[0]
and ", how are you?"
will be individually appended to the StringBuilder
, which is much more efficient and has the same outcome.
Sometimes you want to do the work of building the string only under certain conditions. An example is Debug.Assert
:
Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");
In most cases, the condition will be true and the second parameter is unused. However, all of the arguments are computed on every call, needlessly slowing down execution. Debug.Assert
now has an overload with a custom interpolated string builder, which ensures that the second argument isn’t even evaluated unless the condition is false.
Finally, here’s an example of actually changing the behavior of string interpolation in a given call: String.Create()
lets you specify the IFormatProvider
used to format the expressions in the holes of the interpolated string argument itself:
String.Create(CultureInfo.InvariantCulture, $"The result is {result}");
You can learn more about interpolated string handlers, in this article and this tutorial on creating a custom handler.
Constant interpolated strings
If all the holes of an interpolated string are constant strings, then the resulting string is now also constant. This lets you use string interpolation syntax in more places, like attributes:
[Obsolete($"Call {nameof(Discard)} instead")]
Note that the holes must be filled with constant strings. Other types, like numeric or date values, cannot be used because they are sensitive to Culture
, and can’t be computed at compile time.
Other improvements
C# 10 has a number of smaller improvements across the language. Some of these just make C# work in the way you expect.
Mix declarations and variables in deconstruction
Prior to C# 10, deconstruction required all variables to be new, or all of them to be previously declared. In C# 10, you can mix:
int x2;
int y2;
(x2, y2) = (0, 1); // Works in C# 9
(var x, var y) = (0, 1); // Works in C# 9
(x2, var y3) = (0, 1); // Works in C# 10 onwards
Find out more in the article on deconstruction.
Improved definite assignment
C# produces errors if you use a value that has not been definitely assigned. C# 10 understands your code better and produces less spurious errors. These same improvements also mean you’ll see less spurious errors and warnings for null references.
Find out more about C# definite assignment in the what’s new in C# 10 article.
Extended property patterns
C# 10 adds extended property patterns to make it easier to access nested property values in patterns. For example, if we add an address to the Person
record above, we can pattern match in both of the ways shown here:
object obj = new Person
{
FirstName = "Kathleen",
LastName = "Dollard",
Address = new Address { City = "Seattle" }
};
if (obj is Person { Address: { City: "Seattle" } })
Console.WriteLine("Seattle");
if (obj is Person { Address.City: "Seattle" }) // Extended property pattern
Console.WriteLine("Seattle");
The extended property pattern simplifies the code and makes it easier to read, particularly when matching against multiple properties.
Find out more about extended property patterns in the pattern matching article.
Caller expression attribute
CallerArgumentExpressionAttribute
supplies information about the context of a method call. Like the other CompilerServices attributes, this attribute is applied to an optional parameter. In this case, a string:
void CheckExpression(bool condition,
[CallerArgumentExpression("condition")] string? message = null )
{
Console.WriteLine($"Condition: {message}");
}
The parameter name passed to CallerArgumentExpression
is the name of a different parameter. The expression passed as the argument to that parameter will be contained in the string. For example,
var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);
// Output:
// Condition: true
// Condition: b
// Condition: a > 5
A good example of how this attribute can be used is ArgumentNullException.ThrowIfNull(). It avoids have to pass in the parameter name by defaulting it from the provided value:
void MyMethod(object value)
{
ArgumentNullException.ThrowIfNull(value);
}
Find out more about CallerArgumentExpressionAttribute
Preview features
C# 10 GA includes static abstract members in interfaces as a preview feature. Rolling out a preview feature in GA allows us to get feedback on a feature that will take longer than a single release to create. Static abstract members in interfaces is the basis for a new set of generic math constraints that allow you to abstract over which operators are available. You can read more about generic math constraints in this article.
Closing
Install .NET 6 or Visual Studio 2022, enjoy C# 10, and tell us what you think!
- Kathleen Dollard (PM for the .NET Languages) and Mads Torgersen (C# Lead Designer)
Nice features, thanks !
I don’t like the syntax changes in most cases. They don’t save any appreciable headspace and they add mental friction.
I see that kitchen sink languages are still not dead. When did people lose focus of the most essential feature of all tools?
Simplicity!
There's incredible beauty and power in simplicity. Just as a reminder: the universe is based on a handful of quarks. There are not specific quarks for every possible use. Quarks form atoms. Atoms form molecules. Atoms and molecules form every known substance and entity in the universe.
The software industry should have learned...
You NASA shuttle pilot analogy is missing an extremely important detail. They have never trained a pilot from zero. Every shuttle pilot has hundreds is not thousands of hours fly other various aircraft before even entering the shuttle pilot program (many were flight instructors and test pilots.)
This would be more like taking C, C++, C# of any other programming language and giving it to a senior engineer that has several years of experience....
Agree wholeheartedly. <3
Waiting for a way to define private class properties only in the constructor, without need to declare in class, something like typescript does.
Hi,
Please make sure there is no LDAP lookup in these fancy interpolators 🙂
Thanks,
Regards,
Evgeny
Nice writeup Kathleen! I will not miss typing func<… A lot to digest here. I need to come back and read it on my next airplane ride. … Wait a second, I’m not traveling anymore until the red lights stop flashing. Guess I need to figure out a way to read stuff when there is other things I should be doing, but can’t.
Thanks for the great stuff, Kudos! 😎
I’m wondering why the compiler does not automatically infer and promote the return type of
to object? I’m not saying I’m going to need this, but I’m just curious 😂… Thank you so much.
I assume this is because it would be too dangerous. Anything could be casted to object, and thus you would lose the point of a typed language.
Parameterless struct constructors
SERIOUSLY!!! why did it take so long to fix this bug?
Recently, I tried something like this to force my matrix values to be initialized to identity matrix,
it compiles without any warning on VS 2019. But it never worked, so I had to add extra flag to check
that the matrix instances are initially set to identity. Crazy for a language this old and this useless.
<code>
That’s not a parameterless constructor, and “this useless” is trolling.
What about primary constructor for class? Are there any blockers for this – why it is only for records. In ASP.NET core where there is lot of DI it could simplify code a lot. This feature is waiting couple years and is very simple
We are still considering primary constructors for classes.
This was last discussed in the C# language design meeting on Oct 20.
There are definitely still some things to work out, but we are eager to get it in.
“Structs that are created via default or as part of array allocation ignore explicit parameterless constructors, and always set struct members to their default values.”
I would say it would be much better not to introduce struct constructor at all, than adding it but with such partial support. Now it is basically a dangerous feature because it would lead to nasty surprises.
I may have abbreviated this point too much in the post. Let's focus on as creating an array of something in C# is the logical equivalent to putting in every position of the array.
When you use or you "zero out" the value. With a class, you do not run a parameterless constructor, you get null. The design we took aligns with behavior for other types.
Let me know if I misunderstood your point.
Kathleen, thank you for the reply. I was not clear -- my point is, so far "default" was equivalent to default struct constructor, so using those two was basically developer preference. If anyone would like to add custom default constructor (because it is handy) he/she should recheck entire code using this type if new/default are used according to the new guidelines.
Quick example:
<code>
I am impressed with all the work done with each new version of C#,...
my point is, so far “default” was equivalent to default struct constructor, so using those two was basically developer preference
That's actually never been the case. It has always been possible to define "parameterless struct constructors" in IL, via reflection Emit, or even in certain other languages.
If C# encountered one, it would correctly emit a call to and not simply zero init the value. There was a minor edge case in the libraries...
I see, thank you very much for background information, it really helps to straighten up bad habits starting even with older version of .Net.