What's new in C# 10.0
G.L Solaria
Posted on November 6, 2021
C# was first released around 20 years ago and recently the pace of releases has increased with new releases being unveiled every year for the past 4 years.
C# 10.0 is likely to be launched with .NET 6.0 at .NET Conf 2021 coming up on November 9 (with the conference concluding on November 11). So I thought I would summarise some of the new features introduced in C# 10.0. But first a bit of history ...
Language Evolution
Initially C# was released every few years. Each release typically included a signature feature that required significant thought in how to use it. For example, C# 2.0 introduced Generics, C# 3.0 introduced LINQ, C# 5.0 introduced async/await for asynchronous programming.
With the latest releases now happening every year, they are focusing more on code simplification with smaller updates. Big new features, however, are now likely to evolve over a few releases. For example, pattern matching continues to evolve now spanning 4 releases. Records is another example of this evolutionary approach.
The C# language design also moved from a closed, secret development model to an open development model around 2013. Now you can even contribute to the C# language design forum hosted on GitHub to help further evolve the language. Design meetings and road-maps are published on the site publicly to enable open discussions and feedback.
New Features
I have chosen not to describe all proposed features of C# 10.0 because this would be a very long post (it's very long already!). If you are interested in the full list of proposals, see the C# Language Version History.
File scoped namespace declarations which is a dispenses with the wasteful indentation and braces that comes with namespace declarations thereby removing one level of indentation and braces for all C# files.
Global using directives which allow you to define using directives centrally so that they do not need to be defined in every file.
Constant interpolated strings which allows interpolated strings to be able to be declared constant where possible.
Extended property patterns which simplifies the use of nested properties in pattern matching statements.
Lambda improvements which include the ability for attributes to be defined on lambdas, ability to use var when defining lambdas, and the ability to specify the return type of a lambda.
Caller argument expression which adds a new attribute to help identify expression information at compile time.
Record structs which evolves the C# 9.0 record class reference-type to add a new record struct value-type to manipulate small records efficiently.
Incremental generators which takes source code generators to the next level allowing for generation of not just source code. It also allows for improved generation performance.
Improved definite assignment analysis which fixes a few obscure cases where the compiler would complain that a variable had not been initialised.
File scoped namespace declarations
Instead of ...
namespace WhatsNew10
{
public class Foo
{
}
}
... you can dispense with the top level braces by declaring the namespace using the following ...
namespace WhatsNew10;
public class Foo
{
}
Global using directives
The using directive imports all types from the specified namespace.
Adding the keyword "global" to the directive in any code file will import those namespace types and make the types available to all code in the project.
global using System;
Implicit global using directives are also created by the compiler depending on the .NET 6.0 project SDK type. The implicit usings for 2 of the major project types are shown below ...
Project SDK Type | Implicit Global Usings |
---|---|
Microsoft.NET.Sdk | System |
System | |
System.IO | |
System.Net.Linq | |
System.Net.Http | |
System.Threading | |
System.Threading.Tasks | |
Microsoft.NET.Sdk.Web | System |
System.Net.Http.Json | |
Microsoft.AspNetCore.Builder | |
Microsoft.AspNetCore.Hosting | |
Microsoft.AspNetCore.Http | |
Microsoft.AspNetCore.Routing | |
Microsoft.Extensions.Configuration | |
Microsoft.Extensions.DependencyInjection | |
Microsoft.Extensions.Hosting | |
Microsoft.Extensions.Logging |
So together with the C# 9.0 Top Level Statements feature, the complete code file for a Program.cs can look like this ...
Console.WriteLine("Hello, World!");
Constant interpolated strings
The following use of the constant keyword is now permitted for interpolated strings where previously a compile error would be generated ...
const string s1 = $"Hello world";
const string s3 = $"{s1} C#10";
Extended property patterns
Consider the following ...
var aa = new A { Property1 = "foo" };
var bb = new B { Property2 = aa };
public class A
{
public string Property1 { get; set; }
}
public class B
{
public A Property2 { get; set; }
}
To use pattern matching on an instance of B you previously had to ...
bool isFoo = bb switch
{
{ Property2 : { Property1: "foo"} } => true,
_ => false
};
... but you can do this more simply now by ...
bool now = bb switch
{
{ Property2.Property1 : "foo" } => true,
_ => false
};
Lambda improvements
Automatic inference of the type of a lambda is a nice simplification introduced in C# 10.0.
Func<string, bool> isFooInC9 = ss => ss == "foo"; // C# 9.0
var isFooIn10 = (string ss) => ss == "foo"; // C# 10.0
Lambdas can also have attributes defined on them so instead of ...
[HttpGet("/")] Foo GetFoo() => new(Id: 0, Name: "Name");
app.MapAction((Func<Foo>)GetFoo);
... you can now write ...
app.MapAction([HttpGet("/")] () => new Foo(Id: 0, Name: "Name"));
The return type of a lambda can also now be explicitly specified ...
var foo = () => 1; // foo is type Func<Int32>
var bar = () => (long) 1; // bar is type Func<Int64>
There are a few other improvements including direct lambda invocation and enhanced overload resolution that can be reviewed in the proposal for lambda improvements.
Caller Argument Expression
You can now get an expression as a string by ...
// Writes "(1 == 2 ? true : false) is False" to the console.
Print((1 == 2 ? true : false));
static void Print(
bool result,
[CallerArgumentExpression("result")] string expr = default
)
{
Console.WriteLine($"({expr}) is {result}");
}
Record Structs
Record classes introduced in C# 9.0 are useful because it makes it easier to create immutable objects that are thread-safe (by using the init keyword instead of the set keyword). Record classes are also helpful because the compiler creates some nice value-like built-in methods like:
- equality operators,
- a numeric hash code based on the property values (so you can use the record in a hash-based collections for fast look-ups), and
- a way to convert the values of the record properties to a string.
Record structs also provide those compiler generated record built-ins (unlike plain structs which only generate getters and setters). Record classes, however, are reference types so instances of the type are allocated within the heap. Record structs, on the other hand, are value types so they are allocated on the stack. This means record structs can be manipulated efficiently.
So now we can create an immutable record struct in one nice line of code ...
public readonly record struct Foo(string Fiz, string Faz);
... and use it ...
var foo1 = new Foo { Fiz = "Fizzy", Faz = "Fazzy" };
var foo2 = new Foo { Fiz = "Fizzy", Faz = "Fazzy" };
var foo3 = foo1 with { Faz = "Zazzy" };
Console.WriteLine($"{foo1}"); // Foo { Fiz = Fizzy, Faz = Fazzy }
Console.WriteLine($"{foo2}"); // Foo { Fiz = Fizzy, Faz = Fazzy }
Console.WriteLine($"{foo3}"); // Foo { Fiz = Fizzy, Faz = Zazzy }
Console.WriteLine($"{foo1.GetHashCode()}"); // 129686175
Console.WriteLine($"{foo2.GetHashCode()}"); // 129686175
Console.WriteLine($"{foo3.GetHashCode()}"); // 784730341
Console.WriteLine($"{(foo1 == foo2)}"); // True
Console.WriteLine($"{(foo1 == foo3)}"); // False
Incremental generators
Source generators were introduced in .NET 5 which lets ...
C# developers inspect user code and generate new C# source files that can be added to a compilation. This is done via a new kind of component that we’re calling a Source Generator. (Introducing C# Source Generators)
Incremental generators are a new feature that allows for the generation of source code, artefacts and embedded files. Pipelines can be implemented that use cached data to perform further transformations thereby improving performance.
I admit I don't fully understand how to use this feature and I will be looking forward to .NET Conf for some simple explanations and examples.
Improved definite assignment analysis
Definite assignment analysis is the static analysis done by the compiler to ensure that variables are assigned before they are used. C and C++ don't have this feature although Java does.
There were a few interesting cases that previously gave an error and are now okay with C# 10.0. The cases are obscure but if you are really interested, you can delve into the cases.
Conclusion
So there have been some nice simplifications proposed for C# 10.0. They are continuing to evolve features such as pattern matching and records in response to developer feedback which I think is good to see. C# might even survive another 20 years!
References
Posted on November 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.