Karl Gorman
12
January
2022
Everything You Need to Know About C#10

Everything You Need to Know About C#10 Release

C# is nothing new to us. We have known it since January 2002 to be more precise. The language has undergone quite a few changes in the last two decades. However, as we wait for the 20th birthday of this popular language, it has already got a version update – C#10. C#10 was released on November 9th, along with .NET 6. They both bring along tons of new exciting features. These features have greatly enhanced the programming language. In this blog, we will have a look at the major features of C#10 and how they have transformed it.

Visit the links for the Visual Studio 2022 and the .NET 6 announcement for more information.

Global and implicit usings

using directives makes working with namespaces much easier. To minimize the number of usings that are supposed to be mentioned at the head of all files, C# 10 adds a new global using directive and implicit usings.

Global using directives

When the global keyword comes before a using directive, it means that the keyword will apply to the complete project:

global using System;

Within the directive of global using, you can utilise any functionality of using. For instance, the static keyword imports a type, and makes available all its members and nested types, across your project. Thus, you can include an alias to apply it to the complete project through the using directive:

global using static System.Console;

global using Env = System.Environment;

global usings can be included in any .cs file. Thus, you can put it in Program.cs and files with unique names like usingsglobal.cs. The compilation running currently, usually corresponding to the scope of the running project, is the scope of global usings.

Read the global using directives for further information.

Implicit usings

The functionality of implicit usings automatically adds standard derectives of global using for the project type you’re working on. Adjust the ImplicitUsings attribute to set it in the .csproj file to true to enable implicit usings:

<PropertyGroup>
<!– Other properties like OutputType and TargetFramework –>;

<ImplicitUsings>enable</ImplicitUsings>

</PropertyGroup>

The new C#10 and .NET 6 templates support implicit usings. This blog post has more information pertaining to the modifications to the new templates of .NET 6.

Depending on your application type, you’ll have a different set of global using directives. See this article about implicit usings for more information.

Combining using features

Be it the global using directives, traditional using directives, or implicit usings all operate nicely together. With implicit usings, you may include the appropriate .NET namespaces for the type of project or application you’re working on by adding just one line at the top in the main project file. You can also add more namespaces in global using directives so that they are available across the entire project. Additionally, you can include a handful of namespaces that are used by only some files directly into your project with the help of using directives by mentioning them at the beginning of the code files.

Extra using directives, no matter in what way they are specified, increases the likelihood of name resolution obscurity. If this happens, try increasing a single alias or decreasing the namespaces you’re importing. You can, for instance, at the beginning of a collection of files, switch global using directives by changing them against the explicit using directives.

To exclude namespaces that are automatically added through implicit usings, mention them in the project file:
<ItemGroup>
<Using Remove=“System.Threading.Tasks”/>
</ItemGroup>

Namespaces that act as if they are global using directives can be added. Also, you can include Using items to the file, for instance:

<ItemGroup>
<Using Include=“System.IO.Pipes”/>
</ItemGroup>

File-scoped namespaces

The code for one and only one namespace is spread over several files. Starting with C# 10, you have the option to add a namespace as a standard statement without the curly brackets, which is succeeded by a ; (semi-colon):

namespace MySchool.MyNamespace;
class MyStudents // Note: no indentation

{ … }

This reduces the code’s complexity and eliminates a nesting level. But you can only declare a single file-scoped namespace, and that too must be declared before declaring any other types.

Improvements for lambda expressions and method groups

Numerous improvements to the types and syntax of lambdas are made by us. We anticipate that they will be broadly helpful, with one of the accelerating situations being to ensure ASP.NET Minimal APIs become even easier to use.

promo image3
Looking for a new challenge?

Interesting projects

Friendly team

Decent payment

Natural types for lambdas

Till now, you had to convert a lambda expression to either a type of expression or delegate. Usually, to do this, you’d use an overloaded Func<…> or Action<…> types of delegates in the BCL:

Func<string, int> parse = (string s) => int.Parse(s);

With the release of C#10, if a lambda expression lacks such a “target type,” the processor will automatically compute one for it:

var parse = (string s) => int.Parse(s);

You can check this by using your favourite editor to hover over var parse. You will notice that the type will be Func<string, int>. In general, if an appropriate Func or Action delegate is present, the compiler will use it. Otherwise, a delegate type will be created (for instance, if you have multiple parameters or ref parameters).

Natural types aren’t found in all lambdas because some don’t contain appropriate type information. Think of omitting argument types prevents the compiler from determining what delegate type should be used, as an example:

var parse = s => int.Parse(s);

Lambdas’ natural type allows them to be given to a feeble type like object or Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>

Delegate parse = (string s) => int.Parse(s);   // Func<string, int>

We use a blend of “target” and “natural” type when it is about the expression trees. Hence, if the non-generic Expression or the LambdaExpression are the target types and the natural delegate type is E, we will create an Expression<E>>:

LambdaExpression parseExp = (string s) => int.Parse(s);   // Expression<Func<string, int>>

Expression parseExp = (string s) => int.Parse(s);   // Expression<Func<string, int>>

Natural types for method groups

Since C#10, Method groups (all method names that don’t have parameters lists) can have natural types. Also, you always have the option to change a method group to a delegate type that works with it:

Func<int>read =Console.Read;

Action<string> write = Console.Write;

Now, in a scenario where the method group contains only one single overload, it will instead contain 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

The lambda expression’s return type was evident in the preceding cases and was only inferred. This, however, is not the case always:

var var choose = (bool b) => b ? 1 : “two”; // ERROR: Can’t infer return type

A lambda expression can now have an explicit return type, exactly like a local or a method function, in C# 10. The return type is listed first, followed by the arguments. The arguments must be parenthesized when specifying an explicit return type, so the compiler and other developers don’t get confused:

var var choose = object (bool b) => b ? 1 : “two”; // // Func<bool, object>

Attributes on lambdas

Lambda expressions can now have properties, much as local and methods functions, starting with C# 10. When there are characteristics, the lambda’s list of parameters should be parenthesized once more:

Func<string, int> parse = [Example(1)] (s) => int.Parse(s);

var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : “two”;

You can apply the attributes to the lambdas in the same way that local functions can if they are valid on AttributeTargets.Method.

Improvements to structs

Parameterless constructors, with expressions, field initializers, and record structs are among the new features.

Parameterless struct constructors and field initializers

Before the release of C# 10, all structs used to have an implicit parameterless constructor, which was public and the fields of structs were set to default. Hence, it threw an error when you tried to use a struct to create a constructor without any parameters.

C# 10 allows you to include your struct constructors that are parameterless. But if you don’t provide one, the compiler will supply an implicit constructor to set each field to its respective default values. The only rule to create parameterless constructors instructs is that they should be public and not partial:

public struct Class

{

publicClass()

{

Division = “<unknown>”;

}

public string Division { get; init; }

}

You may either use the example given above to initialise fields in an argumentless constructor, or initialise using a property or field initialiser:

public struct Class

{

public string Division { get; init; } = “<unknown>”;

}

Structs built by default or parts of an array allocation will always disregard explicit argumentless constructors and assign default values to struct elements. See the struct type for additional details about argumentless constructors.

record structs

You can now define records with a record struct in C#10. You can see these as comparable with record classes from C# 9:

public record struct Student

{

public string Name { get; init; }

public string Hobby { get; init; }

}

You may continue using record to define or record class for clarity.

Even prior to C#10, structs had value equality, so you may compare them based on their value. The == operator and IEquatable<T> support are added to Record structs. Record structs include a bespoke implementation of the IEquatable<T> to avoid reflection’s performance concerns and record features such as the ToString() override.

Positional record structs may be created using the primary constructor that declares public members implicitly:

public record struct Student(string Name, string Hobby);

The record struct’s primary constructor’s parameters become public auto-implemented properties. The automatically generated properties, dissimilar to the record classes, are read/write. Thus, this makes converting tuples into a named type much easier. Changing from a tuple like (string Name, string Hobby) to a named type of Student will help you make your code cleaner and ensure that member names are consistent. The positional record struct is simple to declare and maintains the changeable semantics.

Add readonly or apply it to specific properties of a struct to build an immutable record struct. The initializers for Objects are used to establish read-only attributes during the building process. Here’s an example of how you can use unchangeable record structs:

var student = new Studentt { Name = “Chris Morris”, Hobby = “Cricket”};

 

public readonly record struct Student

{

public string Name { get; init; }

public string Hobby { get; init; }

}

You can get additional information on record structs here.

sealed modifier on ToString() in record classes

The quality of the record classes has also been increased. The sealed modifier may be used with the ToString() method starting from C# 10, which prohibits synthesizing a ToString() implementation for all the derived records.

Read more on the use of ToString() for records here.

with expressions on structs and anonymous types

with expressions are supported for all structs in C# 10. These include record and any anonymous struct types:

var student2 = student with { Hobby = “Football” };

This creates a newly created instance of the object with a new value. With this, you can change as many values as you like. If you don’t change any of the values, they’ll stay the same as in the first instance.

Interpolated string improvements

We always thought that doing more is possible with interpolated strings in C# down the road, both in terms of expressiveness and performance. That moment is here with C# 10!

Interpolated string handlers

Interpolated strings are now converted to a call to string.Format by the compiler. This can result in a number of allocations, including arguments’ boxing, the argument array’s allocation, and the resultant string. It also eliminates any ambiguity in interpolation’s actual meaning.

A library pattern has been introduced to C# 10 that allows an API to “take over” the processing of an interpolated string parameter expression. Consider StringBuilder.Append for better understanding:

var sb = new StringBuilder();

sb.Append($“Hey {args[0]}, hope you are doing good.”);

Previously, this would execute the Append(string? value) overload and a freshly allocated and calculated string, attaching it in one chunk to the StringBuilder. However, when an interpolated string is supplied as an input, the new overload Append(ref StringBuilder.AppendInterpolatedStringHandler handler) takes priority over the overload of the string.

When you find SomethingInterpolatedStringHandler’s parameter types, the API author would have done some backend work to manage the interpolated strings better. The strings “Hey “, args[0] and “, hope you are doing good.” will be independently attached to the StringBuilder in our Append example, which is significantly efficient and achieves the same result.

You might wish to conduct the string-building process only under particular circumstances. Debug.Assert is a good example:

Debug.Assert(condition, $“{SomethingExpensiveHappensHere()}”);

The condition will almost always be true. Also, the second parameter in most cases will be ignored. However, every call computes all the parameters, which slows down processing unnecessarily. Debug.Assert now supports a custom interpolated string builder overload that ensures the second parameter isn’t accessed and used until the condition is true.

Here’s how to alter string interpolation behaviour in a call: String.Create() to define the IFormatProvider that is used for expression formatting occurring in the interpolated string’s holes parameter:

String.Create(CultureInfo.InvariantCulture, $“The result is {result}”);

This article and tutorial about designing a custom handler will teach you more about interpolated string handlers.

Constant interpolated strings

If an interpolated string’s holes are all constant strings, the final string is also constant, enabling you to use string interpolation syntax in multiple places, such as the attributes:

[Obsolete($“Call {nameof(Discard)} instead”)]

It’s important to keep in mind that the gaps should be filled using consistent strings. Other kinds, such as date or numeric values, are incompatible with Culture and cannot be calculated during the build.

Other improvements

C# 10 includes several minor enhancements throughout the language.

Improved definite assignment

If you utilise any value that hasn’t been definitively allocated in C#, you’ll get an error. C# 10, on the other hand, understands the code better and generates fewer false positives. Null references will experience fewer bogus errors as a result of these enhancements.

Extended property patterns

Extended patterns for properties have been added to C# 10 to make accessing nested property values easier. If we add a class to the Student record, for example, we would be able to pattern match in the following ways:

object obj = new Student

{

Name = “Luke Wright”,

Hobby = “Fishing”,

Class = new Class { Division = “B” }

};

 

if (obj is Student { Class: { Division = “B” } })

Console.WriteLine(“B”);

 

if (obj is Student { Class.Division: “B” }) // Extended property pattern

Console.WriteLine(“B”);

Extended property patterns have been added to C# 10 to make accessing nested property values in patterns easier. If we add an address to the Person record above, for example, we may pattern match in both of the following ways:

Read more on extended property patterns here.

Caller expression attribute

The CallerArgumentExpressionAttribute provides details about the method call’s context. This is applied to a parameter that is optional, much like the other CompilerServices attributes. A string is used here as an example:

void CheckExpression(bool condition,

[CallerArgumentExpression(“condition”)] string? message = null )

{

Console.WriteLine($“Condition: {message}”);

}

The name of the argument supplied to CallerArgumentExpression is that of another parameter. The string will contain the expression supplied as an input for that parameter. For instance,

var i = 9;

var j = true;

CheckExpression(true);

CheckExpression(j);

CheckExpression(i > 5);

 

// Output:

// Condition: true

// Condition: b

// Condition: a > 5

ArgumentNullException.ThrowIfNull() is a nice illustration of how to use this property. By default the name of the parameter from the supplied value, it eliminates having to supply it in:

void MyMethod (object value)

{

ArgumentNullException.ThrowIfNull(value);

}

Read more on CallerArgumentExpressionAttribute

Closing

This article gave a glimpse of the new features that C#10 will bring. The features will completely change how C# has been used. They also bring limitless possibilities to developers and end-users. We can’t wait for you to try them all out.

The way we organize and write C# projects is going to change forever with C#10! Have you already worked on C#10? Do let us know. We would love to hear from you.

 

Talk to us
Let’s talk about your project!
We will contact you as soon as possible