We're excited to announce the release of C# 10 as part of .NET 6 and Visual Studio 2022. In this post, we'll cover the many new features of C# 10 that make your code prettier, more expressive, and faster.
Read the Visual Studio 2022 Bulletin and the .NET 6 Bulletin for more information, including how to install.
Global and implicit usings
The using directive simplifies how 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, using applies to the entire project:
global using System;
You can use any functionality of using in a global using directive. For example, adding a statically imported type and making members and nested types of that type 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;
You can put global uses in any .cs file, including Program.cs or a specially named file such as globalusings.cs. The scope of global usings is the current compilation, which generally corresponds to the current project.
For more information, see Global using directives.
implicit usings
The implicit usings feature automatically adds a generic global using directive for the type of project you are building. To enable implicit usings, set the ImplicitUsings property in the .csproj file:
<PropertyGroup>
<!-- Other properties like OutputType and TargetFramework -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Implicit usings are enabled in the new .NET 6 templates. Read more about the .NET 6 template changes in this blog post.
Some specific sets of global using directives depend on the type of application you are building. For example, the implicit usings of a console application or class library are different from the implicit usings of an ASP.NET application.
See this implicit usings article for details.
Combining using function
Traditional using directives at the top of the file, global using directives, and implicit using work well together. Implicit using allows you to include in your project file the .NET namespace appropriate for the type of project you are building. The global using directive allows you to include additional namespaces to make them available throughout the project. The using directive at the top of your code file allows you to include namespaces that are used by only a few files in your project.
Regardless of how they are defined, additional using directives increase the likelihood of ambiguity in name resolution. If this is the case, consider adding aliases or reducing the number of namespaces to import. For example, you can replace a global using directive with an explicit using directive at the top of a subset of files.
If you need to remove namespaces included through implicit usings, you can specify them in your project file:
<ItemGroup>
<Using Remove="System.Threading.Tasks" />
</ItemGroup>
You can also add namespaces as if they were global using directives, and you can add Using items to your project file, for example:
<ItemGroup>
<Using Include="System.IO.Pipes" />
</ItemGroup>
file-scoped namespace
Many files contain code for a single namespace. Starting with C# 10, you can include namespaces as statements, followed by a semicolon and without curly braces:
namespace MyCompany.MyNamespace;
class MyClass // Note: no indentation
{ ... }
He simplified the code and removed nesting levels. Only one file-wide namespace declaration is allowed, and it must appear before any type is declared.
For more information on file-scoped namespaces, see the namespace keywords article.
to lambda expressions and method groups
We've made several improvements to the syntax and types of lambdas. We expect these to be widely useful, and one of the driving scenarios is to make the ASP.NET Minimal API simpler.
Natural type of lambda
Lambda expressions now sometimes have a "natural" type. This means that the compiler can usually infer the type of the lambda expression.
By now, the lambda expression had to be converted to a delegate or expression type. In most cases, you will use one of the overloaded Func<...> or Action<...> delegate types in the BCL:
Func<string, int> parse = (string s) => int.Parse(s);
However, since C# 10, if the lambda doesn't have such a "target type", we'll try to compute 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 a suitable delegate exists). Otherwise, it will synthesize a delegate type (eg when you have ref parameters or a large number of parameters).
Not all lambda expressions have natural types - some just don't have enough type information. For example, dropping 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 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>
When it comes to expression trees, we combine "target" and "natural" types. If the target type is a LambdaExpression or a non-generic Expression (the base type for all expression trees) and the lambda has a natural delegate type D, we would instead generate Expression<D>:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Natural type of method group
Method groups (i.e. method names without parameter lists) now also sometimes have natural types. You can always convert method groups to compatible delegate types:
Func<int> read = Console.Read;
Action<string> write = Console.Write;
Now, if the method group has only one overload, it will have the natural type:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Return type of lambda
In the preceding example, the return type of the 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 like on a method or local function. The return type comes before the parameter. When you specify an explicit return type, the parameter must be enclosed in parentheses so that the compiler or other developers don't get too confused:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Properties on lambda
Starting with C# 10, you can put properties on lambda expressions, just like methods and local functions. When there are attributes, the lambda's parameter 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";
Just like local functions, attributes can be applied to lambdas if they are valid on AttributeTargets.Method.
Lambdas are invoked differently than methods and local functions, so properties have no effect when the lambda is invoked. However, properties on lambdas are still useful for code analysis, and they can be discovered through reflection.
Improvements to structs
C# 10 introduced functionality for structs that provides better parity between structs (structures) and classes. These new features include parameterless constructors, field initializers, record structures, and with expressions.
01 No-argument struct constructor and field initializers
Before C# 10, every struct had an implicit public parameterless constructor that set the struct's fields to default values. It is an error to create a parameterless constructor on a struct.
Starting with C# 10, you can include your own parameterless struct constructor. If you don't provide it, an implicit no-argument constructor will be provided to set all fields to default values. The parameterless constructor you create in a struct must be public and cannot be partial:
public struct Address
{
public Address()
{
City = "<unknown>";
}
public string City { get; init; }
}
您可以如上所述在无参数构造函数中初始化字段,也可以通过字段或属性初始化程序初始化它们:
public struct Address
{
public string City { get; init; } = "<unknown>";
}
Structs created by default or as part of array assignment ignore explicit parameterless constructors and always set struct members to their default values. For more information on parameterless constructors in structs, see Struct Types.
02 Record structs
As of C# 10, it is now possible to define a record using a record struct. These are similar to the record classes introduced in C# 9:
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
You can continue to use record to define the record class, or use the record class to make it clear.
Structs already have value equality - when you compare them, it's by value. The record structure adds IEquatable<T> support and the == operator. Recording structures provide custom implementations of IEquatable<T> to avoid performance issues with reflection, and they include recording functionality such as ToString() overrides.
Record structures can be positional, and the primary constructor implicitly declares public members:
public record struct Person(string FirstName, string LastName);
The arguments to the primary constructor become public auto-implemented 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 can clean up your code and ensure 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 primary constructor parameter, no automatic properties will be synthesized and yours will be used.
To create an immutable record structure, add readonly to the structure (as you can add to any structure) or apply readonly to a single property. Object initializers are part of the construction phase where read-only properties can be set. This is just one way to use an immutable record structure:
var person = new Person { FirstName = "Mads", LastName = "Torgersen"};
public readonly record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Learn more about record structure in this article.
03 Seal modifier on ToString() in Record class
Recording classes have also been improved. Starting with C# 10, the ToString() method can contain the seal modifier, which prevents the compiler from synthesizing a ToString implementation for any derived record.
Learn more about ToString() in the documentation in this article.
04 Expressions for structures and anonymous types
C# 10 supports with expressions for all structures, including record structures, and anonymous types:
var person2 = person with { LastName = "Kristensen" };
This will return a new instance with the new value. You can update any number of values. Values you don't set will remain the same as the initial instance.
Learn more about with in this article
Interpolated String Improvements
When we added interpolated strings in C#, we always felt that we could do more with that syntax in terms of performance and expressiveness.
01 Interpolated String Handler
Today, compilers convert interpolated strings into calls to string.Format. This results in a lot of allocations - boxing of parameters, allocation of parameter arrays, and of course the result string itself. Also, it doesn't have any wiggle room in what it means to actually interpolate.
In C# 10, we added a library mode that allows the API to "take over" the handling of interpolated string parameter expressions. For example, consider StringBuilder.Append:
var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");
So far, this calls the Append(string? value) overload with the newly allocated and computed string, appending it to a block of StringBuilder. However, Append now has a new overload Append(ref StringBuilder.AppendInterpolatedStringHandler handler) that takes precedence over the String overload when using an interpolated string as an argument.
Often, when you see parameter types of the form SomethingInterpolatedStringHandler, the API authors have done some work behind the scenes to handle interpolated strings more appropriately for their purposes. In our Append example, the strings "Hello", args[0], and ", how are you?" will be appended to the StringBuilder individually, which is more efficient and has the same result.
Sometimes you just want to do the job of building a string 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, all parameters are calculated for each call, slowing down execution unnecessarily. Debug.Assert now has an overload with a custom interpolated string builder which ensures that the second argument is not even evaluated unless the condition is false.
Finally, here's an example that actually changes the behavior of string interpolation in a given call: String.Create() allows you to specify an expression in the hole that IFormatProvider uses to format the interpolated string parameter itself:
String.Create(CultureInfo.InvariantCulture, $"The result is {result}");
You can learn more about interpolated string handlers in this article and this tutorial on creating custom handlers.
02 Constant Interpolated String
If all holes of the interpolated string were constant strings, the resulting string is now also constant. This allows you to use string interpolation syntax in more places, such as properties:
[Obsolete($"Call {nameof(Discard)} instead")]
Note that the holes must be filled with constant strings. Other types, such as numeric or date values, cannot be used because they are culture-sensitive and cannot be computed at compile time.
Other improvements
C# 10 brings many smaller improvements to the entire language. Some of them just make C# work the way you expect.
Mixing declarations and variables in destructuring
Before C# 10, destructuring required all variables to be new, or that all variables had to 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
Learn more in our article on destructuring.
Improved explicit assignment of
C# will generate an error if you use a value that has not been explicitly assigned. C# 10 understands your code better and produces fewer spurious errors. These same improvements also mean you'll see fewer spurious errors and warnings for null references.
Learn more about C# Deterministic Assignment in the What's New in C# 10 article.
Extended Attribute Mode
C# 10 adds the extended property schema to make it easier to access nested property values in schemas. For example, if we add an address to the Person record above, we can pattern match in 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");
The extended attribute pattern simplifies the 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 Attribute
CallerArgumentExpressionAttribute provides information about the context of the method call. Like 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}");
}
The parameter names passed to CallerArgumentExpression are the names of different parameters. The expression passed as an argument to this parameter will be included in the string. E.g,
var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);
// Output:
// Condition: true
// Condition: b
// Condition: a > 5
ArgumentNullException.ThrowIfNull() is a good example of how to use this property. It avoids having to pass in parameter names by providing a default value:
void MyMethod(object value)
{
ArgumentNullException.ThrowIfNull(value);
}
Learn more about CallerArgumentExpressionAttribute
Get C# documentation: https://docs.microsoft.com/en-us/dotnet/csharp/?ocid=AID3042760
Please leave a message below to tell us your suggestions or ideas, thank you!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。