File-Scoped Namespaces

Let’s start with one of the simplest but perhaps most significant changes. You can now apply a namespace to an entire file via a new syntax option for the namespace keyword. The remaining code inside the file will be automatically namespaced, even if it’s not indented inside a namespace block. This works similarly to namespace declarations in PHP.

In C# 9, you needed this code to namespace a class:

In C# 10, you can use the following instead:

This saves you some horizontal space in your editor by cutting out a level of indentation. If the file needs to contain multiple classes, you can write them all beginning at column zero – the namespace declaration applies to the whole file.

Global Usings

Some namespaces are used very widely across projects. However, you still need to include them manually in each file with a using statement.

C# 10 changes this to support a global using variant. This will make the referenced namespace accessible across all the files in your project.

Global usings can be added to any file that will be included in your compilation. They support all the features of standard using statements, including the static keyword and aliasing with =.

Adopting global usings will cut down the number of lines you write but they’re still best reserved for commonly referenced namespaces that present little chance of naming collisions. Beware that relying on a global using could make your code more opaque, as new contributors may not immediately realize how a namespaced resource has been included.

A companion feature to global usings is implicit usings. This automatically creates global using statements appropriate for your project’s type. The capability is turned on by default in the .NET 6 project templates. It can be disabled with the ImplicitUsings setting in your .csproj file.

Improved Structs

Structs have received several enhancements which bring them into closer parity with classes. These include parameterless constructors, field initializers, full support for with expressions, and the option of creating record structs:

This example creates a “positional” record struct where the X and Y constructor parameters become implicit public members. You can also manually define members using the existing syntax:

A record struct should be used in scenarios where you need to encapsulate some data without attaching custom behaviors as class methods. A record struct offers integrated value equality checks and features such as ToString(). It can be either mutable or immutable via the readonly keyword.

Lambda Expression Enhancements

C# 10 adds several improvements to lambda expressions covering their types and syntax. The objective is to make lambdas more akin to regular methods and local functions. Defining one will now be a more familiar experience.

The concept of a “natural” type has been introduced. This lets the compiler infer a lambda’s type without manually converting it to a delegate or expression. This creates more readable code when assigning a lambda to a variable:

The compiler will infer the type of toInt as Func<string, int> and this will be displayed when you view the code in Visual Studio. Inferences will use Func, Action, or synthesized delegates.

Natural types will only work when your lambda expression is already fully typed. If you omit parameter types, the compiler won’t be able to create a compatible type definition.

A related change is support for explicit return types. Like with a regular function, the return type goes before the lambda’s parameter list:

Finally, lambdas can now take attributes in the same way as methods and functions. They’re positioned at the start of the lambda expression, before the return type and parameter list. You can use attributes to attach extra metadata to your lambdas to facilitate greater introspection and code analysis.

Versatile Deconstructed Assignments

Deconstruction assignments can now both initialize new variables and assign values to existing ones in the same line. Previously you needed to use separate deconstructions for these operations.

In C# 9, this resulted in code that looked like this:

Whereas in C# 10, you can do this:

Now x, y, and z will all be initialized with values using the single deconstruction assignment, reducing repetition in your code.

Other Changes

String interpolation has been improved and now works with constant strings too. You need to ensure all the strings used to fill your placeholder holes during interpolation are themselves constant values. More broadly, optimizations to the interpolation process may reduce memory use and increase performance through the use of dedicated handlers.

Property patterns have been simplified to provide more readable access to nested properties. You can now use dot syntax to access nested property values, instead of multiple layers of parentheses:

Elsewhere, compiler optimizations mean you’ll benefit from fewer false positives during definite assignment and null-state checks. Several C# 9 issues which triggered spurious warnings at compile time have been addressed, resulting in more accurate checks that are better equipped to help you debug issues that actually matter. The problems were linked to using null coalescing expressions and variable comparisons against boolean constants.

Conclusion

C# 10 adds several new capabilities which will help to make development more straightforward. However, many other new features including the field keyword and required properties have been pushed back to the next major release. Generic attributes and static abstract members for interfaces made it into 10 but with the preview tag still attached.

You can start using C# 10 today by downloading Visual Studio 2022. The language is also available as part of the standalone .NET 6 SDK which works across Windows, Mac, and Linux.