In this article, we are going to take a look at what’s new in C# 10. .NET 6 supports the latest C# version, and to use it in our projects, we will need the latest .NET 6 SDK or Visual Studio 2022 which includes the SDK.
So, let’s start.
Record Structs in C# 10
Beginning with C# 9, we could use the record keyword to define ‘out-of-the-box’ immutable reference type objects in our code with value equality semantics.
From now on, we can declare value type records with record struct
keywords to have them behave like other value types. If we omit struct
, they will be reference types, but we can be more explicit and write record class
to achieve the same behavior.
Custom Interpolated String Handlers
The string interpolation handler is a type that processes the placeholder expression in an interpolated string:
[InterpolatedStringHandler] public ref struct CustomInterpolatedStringHandler { StringBuilder builder; public CustomInterpolatedStringHandler(int literalLength, int formattedCount) { builder = new StringBuilder(literalLength); } public void AppendLiteral(string s) { builder.Append(s); } public void AppendFormatted<T>(T t) { if (t is string) { var s = t?.ToString() ?? string.Empty; var notToken = "not "; var index = s.IndexOf(notToken); builder.Append(index < 0 ? s : s.Remove(index, notToken.Length)); } else { builder.Append(t?.ToString()); } } public string GetFormattedText() => builder.ToString(); }
We are just going to note that we need a constructor with at least two arguments. The first two arguments in the constructor are integer constants populated by the compiler, representing the literal length of the interpolation, and the number of interpolation components.
With this implementation in place, we can modify the Program
class:
var attribute = "not awesome"; var processedMessage = ProcessMessage($"Code maze is {attribute}."); string ProcessMessage(CustomInterpolatedStringHandler builder) => builder.GetFormattedText();
Now, as a result, the value of the processedMessage
variable is "Code maze is awesome."
and we can see that we change how the placeholder is shown in our implementation.
Global Using Directives in C# 10
We don’t need to accumulate using
directives at the beginning of each file. We can extract using
directives that we currently have in our Program
and CustomInterpolatedStringHandler
files.
To do that, let’s create the Usings.cs
file with all the using directives:
global using System.Diagnostics; global using System.Runtime.CompilerServices; global using System.Text; global using WhatsNewInCSharp10;
With this approach, we can extract all global
usings to one file and use it in our project.
But that’s not all. A new feature in the .NET 6 SDK is to support the implicit global using directives. That is one of the reasons why we can have Minimal APIs up and running with just four lines of code.
Extended Property Patterns
We are able to reference nested properties or fields within a property pattern. You can read more about it in our Extended Property Patterns in C# article.
Improvements of Structure Types
In order to support record struct
we also got improvements for ‘normal’ struct
types. struct
types can have a parameterless constructor and can initialize an instance field or property at its declaration:
public struct Maze { // Parameterless constructor with property initialization public Maze() { Size = 10; } // Initialization of the property at its declaration public int Size { get; set; } = 10; }
File-scoped Namespace Declaration in C# 10
We don’t need to put our code in indented namespace code blocks anymore:
namespace WhatsNewInCSharp10; public struct Maze { … }
As we can see, we don’t have those parentheses for a namespace that we had in previous C# versions.
Lambda Expression Improvements
Lambda expressions are now more similar to methods and local functions. They can have a natural type, and the compiler will infer a delegate type from the lambda expression or a method group. Also, we can apply attributes to lambda expressions. To show these improvements in action let’s write one lambda method:
var lambda = [DebuggerStepThrough]() => "Hello world";
In the previous version of C#, we would get a compiler error that we cannot assign lambda expression to an implicitly-typed variable and that lambda attributes are not supported.
Constant Interpolated Strings
From C#10, we are able to generate constants from interpolated constant strings.
To show that, let’s initialize $"Code maze is {constantAttribute}."
interpolated string as a const
:
const string constantAttribute = "awesome"; const string constantMessage = $"Code maze is {constantAttribute}.";
We are able to do that if our placeholder is also a const.
Record Types Can Seal ToString in C# 10
When we are using record types in our code, the compiler is generating an implementation of the ToString
method for us.
Let’s create a simple hierarchy of record types:
public record Article(string Author, string Title); public record CodeMazeArticle(string Author, string Title, string Comment) : Article(Author, Title);
Since we don’t want to include Comment
in the representation of the CodeMazeArticle
record, C# 10 allows us to include sealed
modifier, which prevents the compiler from generating a ToString
implementation for any derived records:
public record Article(string Author, string Title) { public sealed override string ToString() { return $"{Author}: {Title}"; } }
And now we can call a ToString
method on the CodeMazeArticle
:
var codeMazeArticleRepresentation = new CodeMazeArticle("Author", "Title", "Comment").ToString();
As a result, the value of codeMazeArticleRepresentation
is "Author: Title"
and we have accomplished our goal to consistently represent our articles.
Assignment and Declaration in the Same Deconstruction
The nifty thing is to be able to mix assignments and declarations when deconstructing our tuples:
var articles = (new Article("Author", "Title"), new CodeMazeArticle("Author", "Title", "Comment")); var article = new Article("Another author", "Another title"); (article, CodeMazeArticle codeMazeArticle) = articles;
After the deconstruction, the article.Author
and article.Title
properties hold "Author"
and "Title"
values respectively. Also, we can use initialized codeMazeArticle
variable further in our code and compare properties:
var areAuthorsEqual = article.Author == codeMazeArticle.Author;
Before C# 10, we could only assign all variables to existing variables or initialize newly declared variables.
Worthy Mentions of Other C# 10 Improvements
In the end, we are just going to mention without further explanation that in C# 10, we got improved definite assignments. We are going to have fewer warnings for false-positive analysis. Also, we can put the AsyncMethodBuilder
attribute on methods and use CallerArgumentExpression
attribute diagnostics in our code.
Conclusion
In this article, we’ve seen new C# 10 features that we can use in our projects.