...

C# 10的新特性

2022-02-15

前言


們很高興地宣布 C# 10 作爲 .NET 6 和 Visual Studio 2022的一部分已經(jīng)發(fā)布了。在這(zhè)篇文章中,我們將(jiāng)介紹 C# 10 的許多新功能(néng),這(zhè)些功能(néng)使你的代碼更漂亮、更具表現力、更快。閱讀 Visual Studio 2022 公告和.NET 6 公告以了解更多信息,包括如何安裝。


Visual Studio 2022 公告:https://aka.ms/vs2022gablog


.NET 6:https://aka.ms/dotnet6-GA


全局和隐式 usings


using 指令簡化了您使用命名空間的方式。C# 10 包括一個新的全局 using 指令和隐式 usings,以減少您需要在每個文件頂部指定的 usings 數量。


全局 using 指令


如果關鍵字 global 出現在 using 指令之前,則 using 适用于整個項目:

global using System;

你可以在全局 using 指令中使用 using 的任何功能(néng)。例如,添加靜态導入類型并使該類型的成(chéng)員和嵌套類型在整個項目中可用。如果您在using 指令中使用别名,該别名也會(huì)影響您的整個項目:

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

您可以將(jiāng)全局使用放在任何 .cs 文件中,包括 Program.cs 或專門命名的文件,如 globalusings.cs。全局 usings 的範圍是當前編譯,一般對(duì)應當前項目。

有關詳細信息,請參閱全局 using 指令。

  • 全局 using 指令https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/using-directive#global-modifier

隐式 usings

隐式 usings 功能(néng)會(huì)自動爲您正在構建的項目類型添加通用的全局 using 指令。要啓用隐式 usings,請在 .csproj 文件中設置 ImplicitUsings 屬性:

<PropertyGroup>
    <!-- Other properties like OutputType and TargetFramework -->
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

在新的 .NET 6 模闆中啓用了隐式 usings 。在此博客文章中閱讀有關 .NET 6 模闆更改的更多信息。

一些特定全局 using 指令集取決于您正在構建的應用程序的類型。例如,控制台應用程序或類庫的隐式 usings 不同于 ASP.NET 應用程序的隐式 usings。

有關詳細信息,請參閱此隐式usings文章。

  • 博客文章

    https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#net-sdk-c-project-templates-modernized

  • 隐式usings

    https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives

Combining using 功能(néng)

文件頂部的傳統 using 指令、全局 using 指令和隐式 using 可以很好(hǎo)地協同工作。隐式 using 允許您在項目文件中包含适合您正在構建的項目類型的 .NET 命名空間。全局 using 指令允許您包含其他命名空間,以使它們在整個項目中可用。代碼文件頂部的 using 指令允許您包含項目中僅少數文件使用的命名空間。

無論它們是如何定義的,額外的 using 指令都(dōu)會(huì)增加名稱解析中出現歧義的可能(néng)性。如果遇到這(zhè)種(zhǒng)情況,請考慮添加别名或減少要導入的命名空間的數量。例如,您可以將(jiāng)全局 using 指令替換爲文件子集頂部的顯式 using 指令。

如果您需要删除通過(guò)隐式 usings 包含的命名空間,您可以在項目文件中指定它們:

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

您還(hái)可以添加命名空間,就(jiù)像它們是全局 using 指令一樣,您可以將(jiāng) Using 項添加到項目文件中,例如:

<ItemGroup>
  <Using Include="System.IO.Pipes" />
</ItemGroup>

文件範圍的命名空間

許多文件包含單個命名空間的代碼。從 C# 10 開(kāi)始,您可以將(jiāng)命名空間作爲語句包含在内,後(hòu)跟分号且不帶花括号:

namespace MyCompany.MyNamespace;

class MyClass // Note: no indentation
{ ... } 

他簡化了代碼并删除了嵌套級别。隻允許一個文件範圍的命名空間聲明,并且它必須在聲明任何類型之前出現。

有關文件範圍命名空間的更多信息,請參閱命名空間關鍵字文章。

  • 命名空間關鍵字文章https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/namespace

對(duì) lambda 表達式和方法組的改進(jìn)

我們對(duì) lambda 的語法和類型進(jìn)行了多項改進(jìn)。我們預計這(zhè)些將(jiāng)廣泛有用,并且驅動方案之一是使 ASP.NET Minimal API 更加簡單。

  • lambda 的語法

    https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#lambda-expression-improvements

  • ASP.NET Minimal APIhttps://devblogs.microsoft.com/dotnet/announcing-asp-net-core-in-net-6/

lambda 的自然類型

Lambda 表達式現在有時(shí)具有“自然”類型。這(zhè)意味著(zhe)編譯器通常可以推斷出 lambda 表達式的類型。

到目前爲止,必須將(jiāng) lambda 表達式轉換爲委托或表達式類型。在大多數情況下,您會(huì)在 BCL 中使用重載的 Func<...> 或 Action<...> 委托類型之一:

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

但是,從 C# 10 開(kāi)始,如果 lambda 沒(méi)有這(zhè)樣的“目标類型”,我們將(jiāng)嘗試爲您計算一個:

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

你可以在你最喜歡的編輯器中將(jiāng)鼠标懸停在 var parse 上,然後(hòu)查看類型仍然是 Func<string, int>。一般來說(shuō),編譯器將(jiāng)使用可用的 Func 或 Action 委托(如果存在合适的委托)。否則,它將(jiāng)合成(chéng)一個委托類型(例如,當您有 ref 參數或有大量參數時(shí))。

并非所有 lambda 表達式都(dōu)有自然類型——有些隻是沒(méi)有足夠的類型信息。 例如,放棄參數類型將(jiāng)使編譯器無法決定使用哪種(zhǒng)委托類型:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

lambda 的自然類型意味著(zhe)它們可以分配給較弱的類型,例如 object 或 Delegate:

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

當涉及到表達式樹時(shí),我們結合了“目标”和“自然”類型。如果目标類型是LambdaExpression 或非泛型 Expression(所有表達式樹的基類型)并且 lambda 具有自然委托類型 D,我們將(jiāng)改爲生成(chéng) Expression<D>:

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

方法組的自然類型

法組(即沒(méi)有參數列表的方法名稱)現在有時(shí)也具有自然類型。您始終能(néng)夠將(jiāng)方法組轉換爲兼容的委托類型:

Func<intread = Console.Read;
Action<string> write = Console.Write;

現在,如果方法組隻有一個重載,它將(jiāng)具有自然類型:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

lambda 的返回類型

在前面(miàn)的示例中,lambda 表達式的返回類型是顯而易見的,并被推斷出來的。情況并非總是如此:

var choose = (bool b) => b ? 1 : "two"// ERROR: Can't infer return type

在 C# 10 中,您可以在 lambda 表達式上指定顯式返回類型,就(jiù)像在方法或本地函數上一樣。返回類型在參數之前。當你指定一個顯式的返回類型時(shí),參數必須用括号括起(qǐ)來,這(zhè)樣編譯器或其他開(kāi)發(fā)人員不會(huì)太混淆:

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

lambda 上的屬性

從 C# 10 開(kāi)始,您可以將(jiāng)屬性放在 lambda 表達式上,就(jiù)像對(duì)方法和本地函數一樣。當有屬性時(shí),lambda 的參數列表必須用括号括起(qǐ)來:

Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";

就(jiù)像本地函數一樣,如果屬性在 AttributeTargets.Method 上有效,則可以將(jiāng)屬性應用于 lambda。

Lambda 的調用方式與方法和本地函數不同,因此在調用 lambda 時(shí)屬性沒(méi)有任何影響。但是,lambdas 上的屬性對(duì)于代碼分析仍然有用,并且可以通過(guò)反射發(fā)現它們。

structs 的改進(jìn)

C# 10 爲 structs 引入了功能(néng),可在 structs (結構)和類之間提供更好(hǎo)的奇偶性。這(zhè)些新功能(néng)包括無參數構造函數、字段初始值設定項、記錄結構和 with 表達式。

01 無參數結構構造函數和字段初始值設定項

在 C# 10 之前,每個結構都(dōu)有一個隐式的公共無參數構造函數,該構造函數將(jiāng)結構的字段設置爲默認值。在結構上創建無參數構造函數是錯誤的。

從 C# 10 開(kāi)始,您可以包含自己的無參數結構構造函數。如果您不提供,則將(jiāng)提供隐式無參數構造函數以將(jiāng)所有字段設置爲默認值。您在結構中創建的無參數構造函數必須是公共的并且不能(néng)是部分的:

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

您可以如上所述在無參數構造函數中初始化字段,也可以通過(guò)字段或屬性初始化程序初始化它們:

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

通過(guò)默認創建或作爲數組分配的一部分創建的結構會(huì)忽略顯式無參數構造函數,并始終將(jiāng)結構成(chéng)員設置爲其默認值。有關結構中無參數構造函數的更多信息,請參閱結構類型。

02 Record structs

從 C# 10 開(kāi)始,現在可以使用 record struct 定義 record。這(zhè)些類似于 C# 9 中引入的record 類:

public record struct Person
{
    public string FirstName { getinit; }
    public string LastName { getinit; }
}

您可以繼續使用 record 定義記錄類,也可以使用 record 類來清楚地說(shuō)明。

結構已經(jīng)具有值相等——當你比較它們時(shí),它是按值。記錄結構添加 IEquatable<T> 支持和 == 運算符。記錄結構提供 IEquatable<T> 的自定義實現以避免反射的性能(néng)問題,并且它們包括記錄功能(néng),如 ToString() 覆蓋。

記錄結構可以是位置的,主構造函數隐式聲明公共成(chéng)員:

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

主構造函數的參數成(chéng)爲記錄結構的公共自動實現屬性。與 record 類不同,隐式創建的屬性是讀/寫的。這(zhè)使得將(jiāng)元組轉換爲命名類型變得更加容易。將(jiāng)返回類型從 (string FirstName, string LastName) 之類的元組更改爲 Person 的命名類型可以清理您的代碼并保證成(chéng)員名稱一緻。聲明位置記錄結構很容易并保持可變語義。

如果您聲明一個與主要構造函數參數同名的屬性或字段,則不會(huì)合成(chéng)任何自動屬性并使用您的。

要創建不可變的記錄結構,請將(jiāng) readonly 添加到結構(就(jiù)像您可以添加到任何結構一樣)或將(jiāng) readonly 應用于單個屬性。對(duì)象初始化器是可以設置隻讀屬性的構造階段的一部分。這(zhè)隻是使用不可變記錄結構的一種(zhǒng)方法:

var person = new Person { FirstName = "Mads", LastName = "Torgersen"};

public readonly record struct Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

在本文中了解有關記錄結構的更多信息。

  • 記錄結構

https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record

03 Record類中 ToString () 上的密封修飾符

記錄類也得到了改進(jìn)。從 C# 10 開(kāi)始,ToString() 方法可以包含 seal 修飾符,這(zhè)會(huì)阻止編譯器爲任何派生記錄合成(chéng) ToString 實現。

在本文中的記錄中了解有關 ToString () 的更多信息。

  • 有關 ToString () 的更多信息

    https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display

04 結構和匿名類型的表達式

C# 10 支持所有結構的 with 表達式,包括記錄結構,以及匿名類型:

var person2 = person with { LastName = "Kristensen" };

這(zhè)將(jiāng)返回一個具有新值的新實例。您可以更新任意數量的值。您未設置的值將(jiāng)保留與初始實例相同的值。
在本文中了解有關 with 的更多信息

  • 了解有關 with 的更多信息

ttps://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display


内插字符串改進(jìn)

當我們在 C# 中添加内插字符串時(shí),我們總覺得在性能(néng)和表現力方面(miàn),使用該語法可以做更多事(shì)情。

01 内插字符串處理程序

今天,編譯器將(jiāng)内插字符串轉換爲對(duì) string.Format 的調用。這(zhè)會(huì)導緻很多分配——參數的裝箱、參數數組的分配,當然還(hái)有結果字符串本身。此外,它在實際插值的含義上沒(méi)有任何回旋餘地。

在 C# 10 中,我們添加了一個庫模式,允許 API “接管”對(duì)内插字符串參數表達式的處理。例如,考慮 StringBuilder.Append:

var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");

到目前爲止,這(zhè)將(jiāng)使用新分配和計算的字符串調用 Append(string? value) 重載,將(jiāng)其附加到 StringBuilder 的一個塊中。但是,Append 現在有一個新的重載 Append(refStringBuilder.AppendInterpolatedStringHandler handler),當使用内插字符串作爲參數時(shí),它優先于字符串重載。

通常,當您看到 SomethingInterpolatedStringHandler 形式的參數類型時(shí),API 作者在幕後(hòu)做了一些工作,以更恰當地處理插值字符串以滿足其目的。在我們的 Append 示例中,字符串 “Hello”、args[0] 和“,how are you?” 將(jiāng)單獨附加到 StringBuilder 中,這(zhè)樣效率更高且結果相同。

有時(shí)您隻想在特定條件下完成(chéng)構建字符串的工作。一個例子是 Debug.Assert:

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

在大多數情況下,條件爲真,第二個參數未使用。但是,每次調用都(dōu)會(huì)計算所有參數,從而不必要地減慢執行速度。Debug.Assert 現在有一個帶有自定義插值字符串構建器的重載,它确保第二個參數甚至不被評估,除非條件爲假。

最後(hòu),這(zhè)是一個在給定調用中實際更改字符串插值行爲的示例:String.Create() 允許您指定 IFormatProvider 用于格式化插值字符串參數本身的洞中的表達式:

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

你可以在本文和有關創建自定義處理程序的本教程中了解有關内插字符串處理程序的更多信息。

  • 創建自定義處理程序
    https://docs.microsoft.com/dotnet/csharp/languagereference/tokens/interpolated#compilation-of-interpolated-strings

  • 内插字符串處理程序的更多信息

https://docs.microsoft.com/dotnet/csharp/whats-new/tutorials/interpolated-string-handler

02 常量内插字符串

如果内插字符串的所有洞都(dōu)是常量字符串,那麼(me)生成(chéng)的字符串現在也是常量。這(zhè)使您可以在更多地方使用字符串插值語法,例如屬性:

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

請注意,必須用常量字符串填充洞。其他類型,如數字或日期值,不能(néng)使用,因爲它們對(duì)文化敏感,并且不能(néng)在編譯時(shí)計算。

其他改進(jìn)

C# 10 對(duì)整個語言進(jìn)行了許多較小的改進(jìn)。其中一些隻是使 C# 以您期望的方式工作。

在解構中混合聲明和變量

在 C# 10 之前,解構要求所有變量都(dōu)是新的,或者所有變量都(dōu)必須事(shì)先聲明。在 C# 10 中,您可以混合:

int x2;
int y2;
(x2, y2) = (01);       // Works in C# 9
(var x, var y) = (01); // Works in C# 9
(x2, var y3) = (01);   // Works in C# 10 onwards 

在有關解構的文章中了解更多信息。

改進(jìn)的明确分配

如果您使用尚未明确分配的值,C# 會(huì)産生錯誤。C# 10 可以更好(hǎo)地理解您的代碼并且産生更少的虛假錯誤。這(zhè)些相同的改進(jìn)還(hái)意味著(zhe)您將(jiāng)看到更少的針對(duì)空引用的虛假錯誤和警告。

在 C# 10 中的新增功能(néng)文章中了解有關 C# 确定賦值的更多信息。

  • C# 10 中的新增功能(néng)文章https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#improved-definite-assignment

擴展的屬性模式

C# 10 添加了擴展屬性模式,以便更輕松地訪問模式中的嵌套屬性值。例如,如果我們在上面(miàn)的 Person 記錄中添加一個地址,我們可以通過(guò)以下兩(liǎng)種(zhǒng)方式進(jìn)行模式匹配:

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");

擴展屬性模式簡化了代碼并使其更易于閱讀,尤其是在匹配多個屬性時(shí)。

在模式匹配文章中了解有關擴展屬性模式的更多信息。

  • 模式匹配文章

    https://docs.microsoft.com/dotnet/csharp/languagereference/operators/patterns#property-pattern

調用者表達式屬性

CallerArgumentExpressionAttribute 提供有關方法調用上下文的信息。與其他 CompilerServices 屬性一樣,此屬性應用于可選參數。在這(zhè)種(zhǒng)情況下,一個字符串:

void CheckExpression(bool condition, 
    [CallerArgumentExpression("condition"
)] string? message
 = null )
{
    Console.WriteLine($"Condition: {message}");
}

傳遞給 CallerArgumentExpression 的參數名稱是不同參數的名稱。作爲參數傳遞給該參數的表達式將(jiāng)包含在字符串中。例如,

var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);

// Output:
// Condition: true
// Condition: b
// Condition: a > 5

ArgumentNullException.ThrowIfNull() 是如何使用此屬性的一個很好(hǎo)的示例。它通過(guò)默認提供的值來避免必須傳入參數名稱:

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


來源:DotNet