你所不知道的 C# 10新特性

語言: CN / TW / HK

我們很高興地宣佈 C# 10 作為 .NET 6 和 Visual Studio 2022 的一部分已經發布了。在這篇文章中,我們將介紹 C# 10 的許多新功能,這些功能使您的代碼更漂亮、更具表現力和更快。

閲讀 Visual Studio 2022 公告.NET 6 公告 以瞭解更多信息,包括如何安裝。

全局和隱式usings

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

全局using指令

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

global using System;

您可以在全局 using 指令中使用 using 的任何功能。 例如,添加靜態導入類型並使該類型的成員和嵌套類型在整個項目中可用。 如果您在 using 指令中使用別名,該別名也會影響您的整個項目:

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

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

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

隱式usings

隱式usings功能會自動為您正在構建的項目類型添加通用的全局using指令。 要啟用隱式usings,請在 .csproj 文件中設置 ImplicitUsings 屬性:

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

在新的 .NET 6 模板中啟用了隱式usings。 在此博客文章中閲讀有關 .NET 6 模板更改的更多信息。

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

有關詳細信息,請參閲此隱式usings文章。

Combining using功能

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

無論它們是如何定義的,額外的 using 指令都會增加名稱解析中出現歧義的可能性。 如果遇到這種情況,請考慮添加別名或減少要導入的命名空間的數量。 例如,您可以將全局 using 指令替換為文件子集頂部的顯式 using 指令。

如果您需要刪除通過隱式 usings 包含的命名空間,您可以在項目文件中指定它們:

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

您還可以添加命名空間,就像它們是全局 using 指令一樣,您可以將 Using 項添加到項目文件中,例如:

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

文件範圍的命名空間

許多文件包含單個命名空間的代碼。 從 C# 10 開始,您可以將命名空間作為語句包含在內,後跟分號且不帶花括號:

```csharp namespace MyCompany.MyNamespace;

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

他簡化了代碼並刪除了嵌套級別。 只允許一個文件範圍的命名空間聲明,並且它必須在聲明任何類型之前出現。 有關文件範圍命名空間的更多信息,請參閲命名空間關鍵字文章。

對 lambda 表達式和方法組的改進

我們對 lambda 的語法 和類型進行了多項改進。 我們預計這些將廣泛有用,並且驅動方案之一是使 ASP.NET Minimal API 更加簡單。

lambda 的自然類型

Lambda 表達式現在有時具有“自然”類型。 這意味着編譯器通常可以推斷出 lambda 表達式的類型。

到目前為止,必須將 lambda 表達式轉換為委託或表達式類型。 在大多數情況下,您會在 BCL 中使用重載的 Func<...> 或 Action<...> 委託類型之一:

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

但是,從 C# 10 開始,如果 lambda 沒有這樣的“目標類型”,我們將嘗試為您計算一個:

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

您可以在您最喜歡的編輯器中將鼠標懸停在 var parse 上,然後查看類型仍然是 Func。 一般來説,編譯器將使用可用的 Func 或 Action 委託(如果存在合適的委託)。 否則,它將合成一個委託類型(例如,當您有 ref 參數或有大量參數時)。

並非所有 lambda 表達式都有自然類型——有些只是沒有足夠的類型信息。 例如,放棄參數類型將使編譯器無法決定使用哪種委託類型:

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

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

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

當涉及到表達式樹時,我們結合了“目標”和“自然”類型。 如果目標類型是 LambdaExpression 或非泛型 Expression(所有表達式樹的基類型)並且 lambda 具有自然委託類型 D,我們將改為生成 Expression

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

方法組的自然類型

方法組(即沒有參數列表的方法名稱)現在有時也具有自然類型。 您始終能夠將方法組轉換為兼容的委託類型:

csharp Func<int> read = Console.Read; Action<string> write = Console.Write;

現在,如果方法組只有一個重載,它將具有自然類型:

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

lambda 的返回類型

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

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

在 C# 10 中,您可以在 lambda 表達式上指定顯式返回類型,就像在方法或本地函數上一樣。 返回類型在參數之前。 當你指定一個顯式的返回類型時,參數必須用括號括起來,這樣編譯器或其他開發人員不會太混淆:

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

lambda 上的屬性

從 C# 10 開始,您可以將屬性放在 lambda 表達式上,就像對方法和本地函數一樣。 當有屬性時,lambda 的參數列表必須用括號括起來:

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

就像本地函數一樣,如果屬性在 AttributeTargets.Method 上有效,則可以將屬性應用於 lambda。

Lambda 的調用方式與方法和本地函數不同,因此在調用 lambda 時屬性沒有任何影響。 但是,lambdas 上的屬性對於代碼分析仍然有用,並且可以通過反射發現它們。

structs的改進

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

無參數結構構造函數和字段初始值設定項 在 C# 10 之前,每個結構都有一個隱式的公共無參數構造函數,該構造函數將結構的字段設置為默認值。 在結構上創建無參數構造函數是錯誤的。

從 C# 10 開始,您可以包含自己的無參數結構構造函數。 如果您不提供,則將提供隱式無參數構造函數以將所有字段設置為默認值。 您在結構中創建的無參數構造函數必須是公共的並且不能是部分的:

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

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

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

通過默認創建或作為數組分配的一部分創建的結構會忽略顯式無參數構造函數,並始終將結構成員設置為其默認值。 有關結構中無參數構造函數的更多信息,請參閲結構類型。

Record structs 從 C# 10 開始,現在可以使用record struct 定義 record。 這些類似於 C# 9 中引入的record 類:

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

您可以繼續使用record定義記錄類,也可以使用record類來清楚地説明。

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

記錄結構可以是位置的,主構造函數隱式聲明公共成員:

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

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

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

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

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

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

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

Record類中 ToString() 上的密封修飾符 記錄類也得到了改進。 從 C# 10 開始,ToString() 方法可以包含 seal 修飾符,這會阻止編譯器為任何派生記錄合成 ToString 實現。

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

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

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

這將返回一個具有新值的新實例。 您可以更新任意數量的值。 您未設置的值將保留與初始實例相同的值。

在本文中瞭解有關 with 的更多信息

內插字符串改進

當我們在 C# 中添加內插字符串時,我們總覺得在性能和表現力方面,使用該語法可以做更多事情。

內插字符串處理程序 今天,編譯器將內插字符串轉換為對 string.Format 的調用。 這會導致很多分配——參數的裝箱、參數數組的分配,當然還有結果字符串本身。 此外,它在實際插值的含義上沒有任何迴旋餘地。

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

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

到目前為止,這將使用新分配和計算的字符串調用 Append(string? value) 重載,將其附加到 StringBuilder 的一個塊中。 但是,Append 現在有一個新的重載 Append(ref StringBuilder.AppendInterpolatedStringHandler handler),當使用內插字符串作為參數時,它優先於字符串重載。

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

有時您只想在特定條件下完成構建字符串的工作。 一個例子是 Debug.Assert:

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

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

最後,這是一個在給定調用中實際更改字符串插值行為的示例: String.Create() 允許您指定 IFormatProvider 用於格式化插值字符串參數本身的洞中的表達式: String.Create(CultureInfo.InvariantCulture, $"The result is {result}");

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

常量內插字符串

如果內插字符串的所有洞都是常量字符串,那麼生成的字符串現在也是常量。 這使您可以在更多地方使用字符串插值語法,例如屬性:

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

請注意,必須用常量字符串填充洞。 其他類型,如數字或日期值,不能使用,因為它們對文化敏感,並且不能在編譯時計算。

其他改進

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

在解構中混合聲明和變量

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

csharp 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

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

改進的明確分配

如果您使用尚未明確分配的值,C# 會產生錯誤。 C# 10 可以更好地理解您的代碼並且產生更少的虛假錯誤。 這些相同的改進還意味着您將看到更少的針對空引用的虛假錯誤和警告。

在 C# 10 中的新增功能文章中瞭解有關 C# 確定賦值的更多信息。

擴展的屬性模式

C# 10 添加了擴展屬性模式,以便更輕鬆地訪問模式中的嵌套屬性值。 例如,如果我們在上面的 Person 記錄中添加一個地址,我們可以通過以下兩種方式進行模式匹配:

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

擴展屬性模式簡化了代碼並使其更易於閲讀,尤其是在匹配多個屬性時。

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

調用者表達式屬性

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

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

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

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

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

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

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

瞭解有關 CallerArgumentExpressionAttribute 的更多信息

結束

安裝 .NET 6 或 Visual Studio 2022,享受 C# 10,並告訴我們您的想法!