(本文閱讀所需 15 分鐘)
我們很高興地宣布 C# 10 作為 .NET 6 和 Visual Studio 2022 的一部分已經發布了。在這篇文章中,我們將介紹 C# 10 的許多新功能,這些功能讓您的程式碼更漂亮、更具表現力、更快。
請閱讀 Visual Studio 2022 公告和 .NET 6 公告以了解更多資訊,包括如何安裝。
全域與隱含 using
using 指令簡化了您使用命名空間的方式。C# 10 包含一個新的全域 using 指令與隱含 using,以減少您需要在每個檔案頂端指定的 using 數量。
全域 using 指令
如果關鍵字 global 出現在 using 指令之前,則 using 適用於整個專案:
global using System;
您可以在全域 using 指令中使用 using 的任何功能。例如,加入靜態匯入類型,並讓該類型的成員與巢狀類型在整個專案中可用。如果您在 using 指令中使用別名,該別名也會影響您的整個專案:
global using static System.Console;
global using Env = System.Environment;
您可以將全域 using 放在任何 .cs 檔案中,包括 Program.cs 或專門命名的檔案,如 globalusings.cs。全域 using 的範圍是目前的編譯,一般對應到目前的專案。
如需詳細資訊,請參閱全域 using 指令。
隱含 using
隱含 using 功能會自動為您正在建置的專案類型加入通用的全域 using 指令。若要啟用隱含 using,請在 .csproj 檔案中設定 ImplicitUsings 屬性:
<PropertyGroup>
<!-- Other properties like OutputType and TargetFramework -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
在新的 .NET 6 範本中已啟用隱含 using。在此部落格文章中閱讀有關 .NET 6 範本變更的更多資訊。
一些特定全域 using 指令集取決於您正在建置的應用程式類型。例如,主控台應用程式或類別庫的隱含 using 不同於 ASP.NET 應用程式的隱含 using。 如需詳細資訊,請參閱此隱含 using 文章。
結合 using 功能
檔案頂端的傳統 using 指令、全域 using 指令與隱含 using 可以很好地協同運作。隱含 using 允許您在專案檔案中包含適合您正在建置之專案類型的 .NET 命名空間。全域 using 指令允許您包含其他命名空間,使其在整個專案中可用。程式碼檔案頂端的 using 指令允許您包含專案中僅少數檔案使用的命名空間。
無論它們是如何定義的,額外的 using 指令都會增加名稱解析中出現歧義的可能性。如果遇到這種情況,請考慮加入別名或減少要匯入的命名空間數量。例如,您可以將全域 using 指令取代為檔案子集頂端的明確 using 指令。
如果您需要刪除透過隱含 using 包含的命名空間,您可以在專案檔案中指定它們:
<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
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 時屬性沒有任何影響。但是,Lambda 上的屬性對於程式碼分析仍然有用,並且可以透過反射發現它們。
結構的改進
C# 10 為結構引入了功能,可在結構和類別之間提供更好的奇偶性。這些新功能包括無參數建構函式、欄位初始設定式、記錄結構和 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 結構
從 C# 10 開始,現在可以使用 record struct 定義 record。這些類似於 C# 9 中引入的 record 類別:
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
您可以繼續使用 record 定義記錄類別,也可以使用 record class 來清楚地說明。
結構已經具有值相等——當您比較它們時,它是按值。記錄結構加入 IEquatable
記錄結構可以是位置的,主建構函式隱含宣告公用成員:
public record struct Person(string FirstName, string LastName);
主建構函式的參數成為記錄結構的公用自動實作屬性。與 record class 不同,隱含建立的屬性是可讀寫的。這使得將元組轉換為命名類型變得更加容易。將傳回型別從 (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() 上的 sealed 修飾詞
記錄類別也得到了改進。從 C# 10 開始,ToString() 方法可以包含 sealed 修飾詞,這會阻止編譯器為任何衍生記錄合成 ToString 實作。
在本文中的記錄中了解有關 ToString() 的更多資訊。
04 結構和匿名類型的 with 運算式
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 可以更好地理解您的程式碼,且產生更少的虛假錯誤。這些相同的改進也意味著您將看到更少的針對 null 參考的虛假錯誤和警告。
在 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);
}
歡迎在下方留言,告訴我們您的建議或想法,謝謝!