Welcome to C# 10

Kathleen Dollard

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)

22 comments

Discussion is closed. Login to edit/delete existing comments.

  • Marcin Drabek 0

    Well, I guess static abstract members in interfaces could also be of use when coping with static factory methods, e. g. Create(). Other than that definitely going to love Interpolated String Handlers 😀

  • Thorsten Sommer 0

    I would like to thank the whole team for your great work on C# 10. The new features are great and make the daily work easier. I am looking forward to the future innovations that you will implement in next versions. Keep the good work.

  • Maciej Pilichowski 0

    “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.

    • Kathleen DollardMicrosoft employee 0

      I may have abbreviated this point too much in the post. Let’s focus on default as creating an array of something in C# is the logical equivalent to puttingdefault in every position of the array.

      When you use default 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 default behavior for other types.

      Let me know if I misunderstood your point.

      • Maciej Pilichowski 0

        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:

           struct NEW_Point
            {
                public int X {get;}
                public NEW_Point()
                {
                    X = 5;
                }
            }
            
            public static void LEGACY_Make(out T a,out T b)
                where T : new()
                {
                    a = new T();
                    b = default(T);
                }
            public static void Main()
            {
                LEGACY_Make(out var a,out var b);
                Console.WriteLine($"{a.X} {b.X}");
            }
        

        I am impressed with all the work done with each new version of C#, but such feature as this one… it would be better without it for now. Because of this C# not only becomes more powerful language, but also more quirky (with more pitfalls).

        • Tanner Gooding Microsoft employee 0

          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 new SomeType::.ctor() and not simply zero init the value. There was a minor edge case in the libraries where Activator.CreateInstance for some generic T wouldn’t always do the same, but that has since been resolved.

          There were several libraries and IL rewriters that took advantage of this existing behavior and so this is just unblocking the ability for C# to do what was always supported and allowed (technically speaking).

          • Maciej Pilichowski 0

            I see, thank you very much for background information, it really helps to straighten up bad habits starting even with older version of .Net.

  • Dominik Jeske 0

    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

    • Kathleen DollardMicrosoft employee 0

      We are still considering primary constructors for classes.

  • Paul Selormey 0

    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.

    public struct Matrix
    {
         public Matrix(bool isIdentity = true)
        {
                 //....
        }
    }
    • William Kempf 0

      That’s not a parameterless constructor, and “this useless” is trolling.

  • Siavash Mortazavi 0

    Thanks for the great stuff, Kudos! 😎
    I’m wondering why the compiler does not automatically infer and promote the return type of

    (bool b) => b ? 1 : "two";

    to object? I’m not saying I’m going to need this, but I’m just curious 😂… Thank you so much.

    • Arni Leibovits 0

      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.

  • Peter Kellner 0

    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.

  • Evgeny Muryshkin 0

    Hi,

    Please make sure there is no LDAP lookup in these fancy interpolators 🙂

    Thanks,
    Regards,
    Evgeny

  • paulo larini 0

    Waiting for a way to define private class properties only in the constructor, without need to declare in class, something like typescript does.

  • Stefan Metzeler 2

    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 the lesson when the simple RISC won the competition against the complex CISC processor architecture.

    If you think that you need more features for your programming language, you probably didn’t understand the problem you are trying to solve.

    As a rule, it should be possible to learn any programming language within at most one day and at that point, it should be possible for a new programmer to understand the syntax and semantics of every possible piece of code he might ever encounter.
    The head of IT development at NASA once mentioned that they needed 3 years to train a new C++ programmer to the point where he could independently write reliable, robust code within NASA specifications, which was kind of embarrassing as training a Space Shuttle pilot took only 18 months.

    Making the programming language more complex automatically means that you introduce new points of failure and inconsistencies and you make it harder to read and understand the code, not to mention the problem of backward compatibility. You probably create a lot of dead-end features that will be abandoned later on.

    Did anyone here have a look at a chart of all the C++ versions and their implementations, with detailed explanations of how each compiler implements specific features and if they are compatible or not? You start having to worry about the correct version of a compiler for a given piece of code. You can spend a day on that table. Result: those who want to be productive strictly limit the features allowable for real-world projects to what is universally supported in a uniform way by all the compilers. Which turns all the nifty new compiler features into a dead weight.

    Everything that is not strictly necessary to generate code should not be part of the programming language. That’s what libraries are for. And those, too, should be kept very simple. If it’s complex, it should be turned into a complete sub-system which should have a black box interface.

    Prof.Wirth explained the complexity issue like this: “One single person should be able to understand the entire system you are trying to build down to a reasonable level of detail. If the complexity of the system exceeds the ability of one person to understand it entirely, split it up into completely independent systems that interact in a well defined manner”.

    For every proposed “feature” of a programming language, ask if is actually required to achieve something that could not be done without it or if it could be solved with existing features and maybe added library functions.

    I still use Oberon-2, which fits in a 20 page manual and has a syntax that can be defined on 1 page.

    If you wonder what one could possibly do without following “industry standards”: my clients over the last 35 years included HP, DuPont, IBM, Logitech, Deutsche Bank, Royal Bank of Canada and many others. The 2 healthcare emergency services in Geneva, Switzerland, used one of my applications. I sold software to India. Clients care about solutions, not about the tools you use.

    For those who wonder: “What is Oberon-2?”, it was at the very heart of the design of .NET. Two of the first 10 compilers available for .NET were Oberon-2 compilers. Many concepts integrated into C# were borrowed directly from Oberon-2, because one of the core architects of .NET was Dr.Szypersky, who got his PhD from Prof.Wirth, the author of Pascal, Modula-2 and Oberon-2. He was also a winner of the Turing award.

    Dr.Szypersky’s PhD involved component software and was based on the Oberon system. The brilliance of his analysis of component software is what got him hired by Microsoft. He had learned to think in clear terms about fundamental issues and that’s what it is all about.

    • The Tasty Beefalo 0

      Agree wholeheartedly. <3

    • Matthew Whited 0

      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. For such a person it days only weeks to learn new programming languages… days if they are just learning the new features of a given language. And for new engineers they are learning more than just the language, patterns, best practices, frameworks (many of which are even more complicated than the space shuttle.)

      On top of all of that no one is forcing you to use any new feature or framework. If you want less powerful languages and platforms go for it not on is stopping you from using outdated or overly verbose technology.

      Oh… and the RISC versus CISC there is no clear winner. RISC continues to work our better in lower power environments but in raw performance CISC processor and vector streaming still rule the day. (Every complex RISC device includes multiple coprocessors such as 3D accelerators and video codecs… features built into many CISC chips.)

Feedback usabilla icon