New features of C#10

New features of C#10

We are pleased to announce that C# 10 has been released as part of. NET 6 and Visual Studio 2022.

最后更新 2/12/2022 10:14 AM
微软中国MSDN
预计阅读 18 分钟
分类
.NET
标签
.NET C# Visual Studio .NET 6 C# 10

(It takes 15 minutes to read this article)

We are pleased to announce that C#10 has been released as part of. NET 6 and Visual Studio 2022. In this article, we will introduce many of the new features of C#10 that make your code more beautiful, more expressive, and faster.

Read the Visual Studio 2022 bulletin and the. NET 6 bulletin to learn more, 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 uses to reduce the number of uses you need to specify at the top of each file.

global using instruction

If the keyword global appears before the using directive, using applies to the entire project:

global using System;

You can use any function of using in the global using directive. For example, add a statically imported type and make the type's members and nested types available throughout the project. If you use an alias in a using directive, the alias will also affect your entire project:

global using static System.Console;
global using Env = System.Environment;

You can place global usage in any.cs file, including Program.cs or a specially named file such as globalusings.cs. The scope of global uses is the current compilation and generally corresponds to the current project.

For more information, see global using directives.

Implicit usings

The implicit usage feature automatically adds common global usage directives to 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 usages are enabled in the new. NET 6 template. Read more about the changes to. NET 6 templates in this blog post.

Some specific sets of global using instructions depend on the type of application you are building. For example, the implicit usages of a console application or class library are different from the implicit usages of an ASP.NET application. For more information, see this implicit uses article.

Combining using features

Traditional using instructions, global using instructions, and implicit using instructions at the top of the file work well together. Implicit using allows you to include a. NET namespace in your project file that is appropriate for the type of project you are building. The global using directive allows you to include additional namespaces so that they are available throughout the project. The using directive at the top of your code file allows you to include namespaces used by only a few files in your project.

Regardless of how they are defined, additional using instructions increase the possibility of ambiguity in name resolution. If you encounter this situation, 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 a subset of files.

If you need to remove namespaces included through implicit usages, you can specify them in your project file:

<ItemGroup>
  <Using Remove="System.Threading.Tasks" />
</ItemGroup>

You can also add namespaces just 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-wide 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 about file-range namespaces, see the Namespace Keywords article.

Improvements to lambda expressions and method groups

We have made a number of improvements to the syntax and typing of lambda. We expect these to be widely useful, and one of the driving options 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 lambda expression.

So far, lambda expressions must be converted to delegate or expression types. 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, starting from C#10, if lambda does not have such a "target type", we will try to calculate one for you:

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

You can hover over var parse in your favorite editor and check that the type is still Func<string, int>. In general, the compiler will use the available Func or Action delegates (if a suitable delegate exists). Otherwise, it will synthesize 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 will 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 types of lambdas mean 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>

When it comes to expression trees, we combine "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 the 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>>

Natural type of method group

Method groups (that is, method names without parameter lists) now 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 a 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

Return type of lambda

In the previous 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 as you would on a method or local function. The return type comes before the parameter. When you specify an explicit return type, the parameters must be enclosed in parentheses so that the compiler or other developers are not too confusing:

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

Properties on lambda

Starting with C#10, you can put attributes on lambda expressions just like you would for methods and local functions. When there are attributes, the parameter list of lambda 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 lambda if they are valid on AttributeTargets.Method.

Lambda is called differently from methods and local functions, so attributes have no effect when calling lambda. However, attributes on lambdas are still useful for code analysis, and they can be discovered through reflection.

Improvements to structs

C#10 introduces functionality for structures to provide better parity between structures (structures) and classes. These new features include parameterless constructors, field initializers, record structures, and with expressions.

01 No parameter structure constructor and field initializers

Prior to C#10, every structure had an implicit public parameterless constructor that set the structure's fields to default values. It is wrong to create a parameterless constructor structurally.

Starting with C#10, you can include your own parameterless structural constructors. If you don't, an implicit parameterless constructor will be provided to set all fields to default values. The parameterless constructors you create in the structure must be public and cannot be partial:

public struct Address
{
    public Address()
    {
        City = "<unknown>";
    }
    public string City { get; init; }
}

You can initialize fields in a parameterless constructor as described above, or you can initialize them through a field or property initializer:

public struct Address
{
    public string City { get; init; } = "<unknown>";
}

Structures created by default or as part of an array allocation ignore explicit parameterless constructors and always set structure members to their default values. For more information about parameterless constructors in structs, see Structure Types.

02 Record structs

Starting from C#10, you can now use record struct to define record. 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 classes to define record classes, or you can use the record class to make it clear.

Structures already have values equal-when you compare them, it is by value. Add Iequatable support and the == operator to the record structure. Record structures provide custom implementations of IQuatable to avoid performance issues with reflection, and they include logging features such as ToString() overrides.

The record structure can be positional, and the main constructor implicitly declares public members:

public record struct Person(string FirstName, string LastName);

The parameters of the main constructor become public automatically implemented properties of the record structure. Unlike the record class, implicitly created attributes are read/written. This makes it easier to convert tuples to named types. Changing the return type from a tuple like (string FirstName, string LastName) to the named type of Person cleans up your code and ensures that member names are consistent. Declaring location record structures 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 your is used.

To create an immutable record structure, add readonly to the structure (just as you can add to any structure) or apply readonly to individual attributes. The object initializer is part of the construction phase where read-only properties can be set. This is just one way to use the 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 Sealing modifier on ToString () in Record class

Records classes have also been improved. Starting with C#10, the ToString() method can contain a seal modifier, which prevents the compiler from synthesizing ToString implementations for any derived records.

Learn more about ToString () in the notes in this article.

04 Structures and anonymous types of expressions

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 do not set will remain the same as the original instance.

Learn more about with in this article

Interpolated string improvements

When we added interpolated strings to C#, we always felt that using this syntax could do more in terms of performance and expressiveness.

01 Interpolated string handler

Today, compilers convert interpolated strings into calls to string.Format. This leads to a lot of assignments--the boxing of parameters, the allocation of parameter arrays, and of course the result string itself. In addition, it has no room for maneuver 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?");

So far, this will use the newly allocated and calculated string to call Append(string? value) overload, appending it to a block in StringBuilder. However, Append now has a new overloaded Append(refStringBuilder. Append Interpolated StringHandler handler), which takes precedence over string overloading when using interpolated strings as arguments. Generally, when you see parameter types in the form of SomethingInterpolatedStringHandler, the API author does some work behind the scenes to more appropriately handle interpolated strings for its purpose. In our Append example, the strings "Hello", args[0], and ", how are you?" Attach it separately to StringBuilder for greater efficiency and the same results.

Sometimes you just want to complete the task 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, each call evaluates all parameters, unnecessarily slowing down execution. Debug.Assert now has an overload with a custom interpolation string builder that ensures that the second parameter is not even evaluated unless the condition is false.

Finally, this is an example of actually changing the string interpolation behavior in a given call: String.Create() allows you to specify the expression in the hole that IFormProvider 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 strings

If all the holes in the interpolated string are constant strings, the resulting string is now constant. This allows you to use string interpolation syntax in more places, such as properties:

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

Note that the hole must be filled with a constant string. Other types, such as numbers or date values, cannot be used because they are culturally sensitive and cannot be calculated at compile time.

other improvements

C#10 makes many minor improvements to the entire language. Some of them just make C#work the way you expect it.

Mixing declarations and variables in deconstruction

Prior to C#10, deconstruction required that all variables be new, or that all variables must be declared in advance. 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 the article on deconstruction.

Clear allocation of improvements

If you use values that have not been explicitly assigned, C#will generate errors. C#10 can better understand your code and generate fewer false errors. These same improvements also mean that you will see fewer false errors and warnings about null references.

Learn more about C#determining assignments in the What's New in C#10 article.

Extended attribute schema

C#10 adds extended attribute patterns to make it easier to access nested attribute values in the patterns. For example, if we add an address to the Person record above, we can perform pattern matching 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");

Extended attribute patterns simplify code and make it easier to read, especially when matching multiple attributes.

Learn more about extended attribute patterns in the Pattern Matching article.

Caller expression properties

The CallerArgumentExpressionAttribute provides information about the method invocation context. 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 to the parameter as an argument will be included in the string. example,

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 attribute. It avoids having to pass in parameter names by providing values by default:

void MyMethod(object value)
{
    ArgumentNullException.ThrowIfNull(value);
}

Please leave a message below and tell us your suggestions or ideas. Thank you!

Keep Exploring

延伸阅读

更多文章