We are pleased to announce the release of C# 10 as part of.net 6 and Visual Studio 2022. In this article, we’ll introduce many of the new features of C# 10 that make your code prettier, more expressive, and faster.
Read the Visual Studio 2022 bulletin and.net 6 bulletin for more information, including how to install.
Global and implicit USings
The using directive simplifies the way you use 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 directive
If the keyword global appears before a using directive, then using applies to the entire project:
global using System;
You can use any functionality of using in the global USING directive. For example, add a static import type and make its members and nested types available throughout the project. If you use an alias in a using directive, that alias also affects your entire project:
global using static System.Console;
global using Env = System.Environment;
Copy the code
You can put global use in any.cs file, including program.cs or specially named files such as globalUsings.cs. The scope of global USings is currently compiled and generally corresponds to the current project.
For more information, see the global using directive.
Implicit usings
The implicit USings feature automatically adds a generic global using directive for the type of project you are building. To enable ImplicitUsings, set the ImplicitUsings property in the.csproj file:
<PropertyGroup> <! -- Other properties like OutputTypeand TargetFramework -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Copy the code
Implicit USings is enabled in the new.NET 6 template. Read more about the.NET 6 template changes in this blog post.
Some specific sets of global using instructions depend on the type of application you are building. For example, implicit USings for console applications or class libraries are different from implicit USings for ASP.NET applications.
See this implicit USings article for more information.
Combining the using function
The traditional, global, and implicit using directives at the top of the file work well together. Using implicitly allows you to include.net namespaces in your project files that are appropriate for the type of project you are building. The global using directive allows you to include additional namespaces to make them available throughout your project. The using directive at the top of the code file allows you to include namespaces that are used by only a few files in your project.
No matter how they are defined, additional using directives increase the likelihood of ambiguity in name resolution. If this happens, consider adding aliases or reducing the number of namespaces to import. For example, you can replace the global using directive with an explicit using directive at the top of the file subset.
If you need to remove namespaces that are included through implicit USings, you can specify them in the project file:
<ItemGroup>
<Using Remove="System.Threading.Tasks" />
</ItemGroup>
Copy the code
You can also add namespaces as if they were global using directives. You can add using items to project files, for example:
<ItemGroup>
<Using Include="System.IO.Pipes" />
</ItemGroup>
Copy the code
File scoped namespace
Many files contain code for a single namespace. Starting with C# 10, you can include namespaces as statements, followed by semicolons and no braces:
namespace MyCompany.MyNamespace;
class MyClass // Note: no indentation{... }Copy the code
He simplified the code and removed the nesting level. Only one file-scoped namespace declaration is allowed, and it must precede any type declaration. For more information about file-scoped namespaces, see the namespace Keyword article.
Improvements to lambda expressions and method groups
We have made a number of improvements to the syntax and type of lambda. We expect these to be widely useful, and one of the driving solutions is to make the ASP.NET Minimal API simpler.
The natural type of lambda
Lambda expressions now sometimes have a “natural” type. This means that the compiler can usually infer the type of a lambda expression.
So far, lambda expressions must be converted to delegate or expression types. In most cases, you will use overloaded Func<… > or Action <… > one of the delegate types:
Func<string, int> parse = (string s) => int.Parse(s);
However, starting with C# 10, if a lambda doesn’t have such a “target type,” we’ll try to calculate 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 the available Func or Action delegate (if one exists). Otherwise, it synthesizes a delegate type (for example, when you have ref arguments or a large number of arguments).
Not all lambda expressions have natural types — some just don’t have enough type information. For example, abandoning parameter types would make it impossible for the compiler 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 weaker types, 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>
Copy the code
When it comes to expression trees, we combine the “target” and “natural” types. If the target type is LambdaExpression or non-generic Expression (the base type of all Expression trees) and the lambda has a natural delegate type D, we will generate Expression instead:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Copy the code
The natural type of method group
Method groups (that is, method names without argument lists) now sometimes have natural types as well. You can always convert a method group to a compatible delegate type:
Func<int> read = Console.Read;
Action<string> write = Console.Write;
Copy the code
Now, if the method group has only 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
Copy the code
The return type of lambda
In the previous example, the return type of a lambda expression is obvious and inferred. This is not 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 as you would on a method or local function. The return type precedes the parameter. When you specify an explicit return type, the arguments must be enclosed in parentheses so that the compiler or other developers are not too confused:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Attributes on lambda
Starting with C# 10, you can place attributes on lambda expressions just as you would on methods and local functions. When lambda has attributes, the argument list must be enclosed in parentheses:
Func<string.int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";
Copy the code
Just like local functions, if the attribute is valid on AttributeTargets.Method, you can apply the attribute to the lambda.
Lambda is called differently from methods and local functions, so attributes have no effect when Lambda is called. However, the attributes on the lambdas are still useful for code analysis and can be found by reflection.
The improvement of user-defined structs
C# 10 introduces functionality to structs to provide better parity between structs(structures) and classes. These new features include parameterless constructors, field initializers, record structures, and with expressions.
Before C# 10, every structure had an implicit public no-argument constructor that set the structure’s fields to default values. It is an error to create a no-argument constructor on a structure.
Starting with C# 10, you can include your own parameterless structure constructors. If you do not, an implicit no-argument constructor is provided to set all fields to default values. The parameterless constructors you create in a structure must be public and cannot be partial:
public struct Address
{
public Address()
{
City = "<unknown>";
}
public string City { get; init; }}Copy the code
You can initialize fields in a no-argument constructor as described above, or you can initialize them through a field or property initializer:
public struct Address
{
public string City { get; init; } = "<unknown>";
}
Copy the code
Structures created by default or as part of array allocation ignore the explicit no-argument constructor and always set the structure members to their default values. For more information about parameterless constructors in structures, see Structure Types.
Record structs starting with C# 10, you can now define a Record using a Record struct. These are similar to the record class introduced in C# 9:
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }}Copy the code
You can continue to define the record class using Record, or you can use the Record class to make it clear.
Structures already have value equality — when you compare them, it’s by value. Record structure adds IEquatable support and the == operator. Record structures provide a custom implementation of IEquatable to avoid performance issues with reflection, and they include recording capabilities such as ToString() overwriting.
Record structures can be positional, and the main constructor implicitly declares public members:
public record struct Person(string FirstName, string LastName);
The arguments to the main constructor become common auto-implementation properties of the record structure. Unlike the Record class, implicitly created properties are read/write. This makes it easier to convert tuples to named types. Changing the return type from a tuple like (String FirstName, String LastName) to a named type of Person cleans up your code and ensures consistent member names. Declaring a location record structure is easy and maintains mutable semantics.
If you declare a property or field with the same name as the main constructor parameter, no automatic properties are synthesized and used.
To create immutable record structures, add ReadOnly to a structure (just as you can add it to any structure) or apply it to a single attribute. An object initializer is part of the construction phase where read-only properties can be set. This is just one way to use immutable record structures:
var person = new Person { FirstName = "Mads", LastName = "Torgersen"};
public readonly record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }}Copy the code
Learn more about record structures in this article.
The sealed modifier Record class on ToString() in the Record class has also been improved. As of C# 10, the ToString() method can include the seal modifier, which prevents the compiler from synthesizing the ToString implementation for any derived record.
Learn more about ToString() in the notes in this article.
C# 10 supports with expressions for all constructs, including record constructs, as well as anonymous types:
var person2 = person with { LastName = "Kristensen" };
This returns a new instance with a new value. You can update any number of values. Values that you do not set will remain the same as the original instance.
Learn more about with in this article
Interpolation string improvements
When we added interpolated strings to C#, we always felt we could do more with this syntax in terms of performance and expressiveness.
Inline string handlers Today, the compiler converts an inline string into a call to String.format. This results in a lot of allocation — the boxing of parameters, allocation of parameter arrays, and of course, the result string itself. Moreover, it leaves no room for manoeuvre in the meaning of actual interpolation.
In C# 10, we added a library pattern that allows the API to “take over” the processing of interpolated string parameter expressions. For example, consider stringBuilder.append:
var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");
Copy the code
So far, this calls Append(String? Value) overload to append it to a block of StringBuilder. However, Append now have a new overloading Append (ref. StringBuilder AppendInterpolatedStringHandler handler), when using interpolation string as a parameter, it takes precedence over string overloading.
Usually, when you see SomethingInterpolatedStringHandler parameter types, in the form of API, the authors made some work behind the scenes in a more properly handle interpolation string to meet the goal. In our Append example, the strings “Hello”, args[0], and “, how are you?” Attach to StringBuilder separately for greater efficiency and the same results.
Sometimes you just want to finish building strings under certain conditions. An example is debug.assert:
Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");
In most cases, the condition is true and the second parameter is not used. However, each call evaluates all the parameters, slowing down execution unnecessarily. Debug.assert now has an overload with a custom interpolated string builder that ensures that the second argument is not 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 () allows you to specify expressions in holes that the IFormatProvider uses to format the interpolation String parameters themselves: String.Create(CultureInfo.InvariantCulture, $”The result is {result}”);
You can learn more about inline string handlers in this article and in this tutorial on creating custom handlers.
Constants interpolate strings
If all holes in the interpolated string are constant strings, the generated string is now constant. This allows you to use string interpolation syntax in more places, such as attributes:
[Obsolete($"Call {nameof(Discard)} instead")]
Note that you must fill the hole with a constant string. Other types, such as numbers or date values, cannot be used because they are culturally sensitive and cannot be evaluated at compile time.
Other improvements
C# 10 makes a number of minor improvements to the overall language. Some of them just make C# work the way you want it to.
Mix declarations and variables in deconstruction
Prior to C# 10, deconstruction required that all variables be new, or that all variables must be declared beforehand. 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
Copy the code
Learn more about deconstruction in this article.
A clear allocation of improvements
C# generates an error if you use a value that has not been explicitly assigned. C# 10 can understand your code better and produce fewer false errors. These same improvements also mean that you will see fewer false errors and warnings for empty references.
Learn more about C# deterministic assignment in the new features in C# 10 article.
Extended property schema
C# 10 added the extended property schema to make it easier to access nested property values in the schema. For example, if we add an address to the Person record above, we can do pattern matching in one of two ways:
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");
Copy the code
The extended attribute pattern simplifies code and makes it easier to read, especially when matching multiple attributes.
Learn more about extended attribute patterns in the pattern matching article.
Caller expression property
CallerArgumentExpressionAttribute provides information about method invocation, in context. Like the other CompilerServices properties, this property applies to optional parameters. In this case, a string:
void CheckExpression(bool condition,
[CallerArgumentExpression("condition")] string? message = null )
{
Console.WriteLine($"Condition: {message}");
}
Copy the code
The argument names passed to CallerArgumentExpression are the names of the different arguments. The expression passed to the parameter as an argument is 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
Copy the code
ArgumentNullException. ThrowIfNull () is how to use this property a good example. It avoids having to pass in parameter names by providing values by default:
void MyMethod(object value)
{
ArgumentNullException.ThrowIfNull(value);
}
Copy the code
For more information about CallerArgumentExpressionAttribute
The end of the
Install.net 6 or Visual Studio 2022, enjoy C# 10, and tell us what you think!