C# 10的新特性

微軟技術棧發表於2022-02-18

我們很高興地宣佈 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 指令中使用別名,該別名也會影響您的整個專案:

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

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

有關詳細資訊,請參閱 全域性 using 指令。

隱式usings

隱式usings功能會自動為您正在構建的專案型別新增通用的全域性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文章。

Combining using功能

檔案頂部的傳統 using 指令、全域性 using 指令和隱式 using 可以很好地協同工作。 隱式using允許您在專案檔案中包含適合您正在構建的專案型別的 .NET 名稱空間。 全域性 using 指令允許您包含其他名稱空間,以使它們在整個專案中可用。 程式碼檔案頂部的 using 指令允許您包含專案中僅少數檔案使用的名稱空間。

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

如果您需要刪除通過隱式 usings 包含的名稱空間,您可以在專案檔案中指定它們:

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

您還可以新增名稱空間,就像它們是全域性 using 指令一樣,您可以將 Using 項新增到專案檔案中,例如:

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

檔案範圍的名稱空間

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

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<string, int>。 一般來說,編譯器將使用可用的 Func 或 Action 委託(如果存在合適的委託)。 否則,它將合成一個委託型別(例如,當您有 ref 引數或有大量引數時)。

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

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

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

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<D>:

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

方法組的自然型別

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

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

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

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 的引數列表必須用括號括起來:

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 表示式。

01 無引數結構建構函式和欄位初始值設定項

在 C# 10 之前,每個結構都有一個隱式的公共無引數建構函式,該建構函式將結構的欄位設定為預設值。 在結構上建立無引數建構函式是錯誤的。

從 C# 10 開始,您可以包含自己的無引數結構建構函式。 如果您不提供,則將提供隱式無引數建構函式以將所有欄位設定為預設值。 您在結構中建立的無引數建構函式必須是公共的並且不能是部分的:

public struct Address
{
    public Address()
    {
        City = "<unknown>";
    }
    public string City { get; init; }
}
 
您可以如上所述在無引數建構函式中初始化欄位,也可以通過欄位或屬性初始化程式初始化它們:
 
public struct Address
{
    public string City { get; init; } = "<unknown>";
}
 

通過預設建立或作為陣列分配的一部分建立的結構會忽略顯式無引數建構函式,並始終將結構成員設定為其預設值。 有關結構中無引數建構函式的更多資訊,請參閱結構型別。

02 Record structs

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

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

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

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

記錄結構可以是位置的,主建構函式隱式宣告公共成員:

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

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

如果您宣告一個與主要建構函式引數同名的屬性或欄位,則不會合成任何自動屬性並使用您的。

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

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

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

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

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

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

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

04 結構和匿名型別的表示式

C# 10 支援所有結構的 with 表示式,包括記錄結構,以及匿名型別:

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

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

在本文中瞭解有關 with 的更多資訊

內插字串改進

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

01 內插字串處理程式

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

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

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

您可以在本文和有關建立自定義處理程式的本教程中瞭解有關內插字串處理程式的更多資訊。

02 常量內插字串

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

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

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

其他改進

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

在解構中混合宣告和變數

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

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 記錄中新增一個地址,我們可以通過以下兩種方式進行模式匹配:

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 屬性一樣,此屬性應用於可選引數。 在這種情況下,一個字串:

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

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

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

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

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

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

瞭解有關 CallerArgumentExpressionAttribute 的更多資訊
獲得C#文件:https://docs.microsoft.com/zh...

歡迎在下方留言,告訴我們您的建議或想法,謝謝!

相關文章