C# 9.0 添加和增強的功能【基礎篇】

一、記錄(record)

C# 9.0 引入了記錄類型。 可使用 record 關鍵字定義一個引用類型,以最簡的方式創建不可變類型。這種類型是執行緒安全的,不需要進行執行緒同步,非常適合併行計算的數據共享。它減少了更新對象會引起各種bug的風險,更為安全。System.DateTime 和 string 也是不可變類型非常經典的代表。

與類不同的是,它是基於值相等而不是唯一的標識符–對象的引用。

通過使用位置參數或標準屬性語法,可以創建具有不可變屬性的記錄類型,整個對象都是不可變的,且行為像一個值。

優點:

  1)在構造不可變的數據結構時,它的語法簡單易用

  2)record 為引用類型,不用像值類型在傳遞時需要記憶體分配,還可整體拷貝

  3)構造函數和結構函數為一體的、簡化的位置記錄;

  4)有力的相等性支援 —— 重寫了 Equals(object), IEquatable, 和 GetHashCode() 這些基本方法。

record 類型可以定義為可變的,也可以是不可變的。

// 沒有 set 訪問器,創建後不可更改,叫不可變類型
public record Person
{
    // 要支援用對象初始化器進行初始化,則在屬性中使用 init 關鍵字
    // 或者以構造函數的方式
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}
// 可變類型的 record
// 因為有 set 訪問器,所以它支援用對象初始化器進行初始化
public record Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

記錄(record)和類一樣,在面向對象方面,支援繼承,多態等所有特性。除過前面提到的 record 專有的特性,其他語法寫法跟類也是一樣。同其他類型一樣,record 的基類依然是 object。

  注意:1)記錄只能從記錄繼承,不能從類繼承,也不能被任何類繼承; 2)record 不能定義為 static 的,但是可以有 static 成員。

從本質上來講,record 仍然是一個類,但是關鍵字 record 賦予這個類額外的幾個像值的行為:

  1)基於值相等性的比較方法,如 Equals,==,!=,EqualityContract 等; 2)重寫 GetHashCode(); 3)拷貝和克隆成員; 4)PrintMembers 和 ToString() 方法。

應用場景:

  1)用於 web api 返回的數據,通常作為一種一次性的傳輸型數據,不需要是可變的,因此適合使用 record;2)作為不可變數據類型 record 對於並行計算和多執行緒之間的數據共享非常適合,安全可靠;3)record 本身的不可變性和 ToString 的數據內容的輸出,不需要人工編寫很多程式碼,就適合進行日誌處理;4)其他涉及到有大量基於值類型比較和複製的場景,也是 record 的常用的使用場景。

with 表達式

  當使用不可變的數據時,一個常見的模式是從現存的值創建新值來呈現一個新狀態。

  例如,如果 Person 打算改變他的姓氏(last name),我們就需要通過拷貝原來數據,並賦予一個不同的 last name 值來呈現一個新 Person。這種技術被稱為非破壞性改變。作為描繪隨時間變化的 person,record 呈現了一個特定時間的 person 的狀態。為了幫助進行這種類型的編程,針對 records 就提出了 with 表達式,用於拷貝原有對象,並對特定屬性進行修改

// 修改特定屬性後複製給新的 record
var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };
// 只是進行拷貝,不需要修改屬性,那麼無須指定任何屬性修改
Person clone = person with { };

  with 表達式使用初始化語法來說明新對象在哪裡與原有對象不同。with 表達式實際上是拷貝原來對象的整個狀態值到新對象,然後根據對象初始化器來改變指定值。這意味著屬性必須有 init 或者 set 訪問器,才能用 with 表達式進行更改

  注意:1)with 表達式左邊操作數必須為 record 類型; 2)record 的引用類型成員,在拷貝的時候,只是將所指實例的引用進行了拷貝。

  record 參考:C# 9.0新特性詳解系列之五:記錄(record)和with表達式

二、僅限 Init 的資源庫

從 C# 9.0 開始,可為屬性和索引器創建 init 訪問器,而不是 set 訪問器。 調用方可使用屬性初始化表達式語法在創建表達式中設置這些值,但構造完成後,這些屬性將變為只讀。

僅限 init 的資源庫提供了一個窗口用來更改狀態。 構造階段結束時,該窗口關閉。 在完成所有初始化(包括屬性初始化表達式和 with 表達式)之後,構造階段實際上就結束了。

屬性初始值設定項可明確哪個值正在設置哪個屬性。 缺點是這些屬性必須是可設置的。

可在編寫的任何類型中聲明僅限 init 的資源庫。 例如,以下結構定義了天氣觀察結構:

// 以下結構定義了天氣觀察結構
public struct WeatherObservation
{
    public DateTime RecordedAt { get; init; }
    public decimal TemperatureInCelsius { get; init; }
    public decimal PressureInMillibars { get; init; }
    public override string ToString() =>
        $"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
        $"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
}
// 調用方可使用屬性初始化表達式語法來設置值,同時仍保留不變性
var now = new WeatherObservation 
{ 
    RecordedAt = DateTime.Now, 
    TemperatureInCelsius = 20, 
    PressureInMillibars = 998.0m 
};
//初始化後嘗試更改觀察值會導致編譯器錯誤
// Error! CS8852.
now.TemperatureInCelsius = 18;

對於從派生類設置基類屬性,僅限 init 的資源庫很有用。這些設置器可在 with 表達式中使用。 可為定義的任何 classstruct 或 record 聲明僅限 init 的資源庫。

三、頂級語句

頂級語句,就是從應用程式中刪除了不必要的流程。例如最基本的「HelloWorld!」:

using System;
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
// 只有一行程式碼執行所有操作,藉助頂級語句
// 可使用 using 指令和執行操作的一行替換所有樣本
using System;
Console.WriteLine("Hello World!");
// 如果需要單行程式,可刪除 using 指令,並使用完全限定的類型名稱
System.Console.WriteLine("Hello World!");

應用程式中只有一個文件可使用頂級語句。

  如果編譯器在多個源文件中找到頂級語句,則是錯誤的。

  如果將頂級語句與聲明的程式入口點方法(通常為 Main 方法)結合使用,也會出現錯誤。

從某種意義上講,可認為一個文件包含通常位於 Program 類的 Main 方法中的語句。

頂級語句可提供類似腳本的試驗體驗,這與 Jupyter 筆記型電腦提供的很類似。 頂級語句非常適合小型控制台程式和實用程式。Azure Functions 是頂級語句的理想用例。

(Jupyter Notebook 的本質是一個 Web 應用程式,便於創建和共享程式文檔,支援實時程式碼,數學方程,可視化和 markdown。 用途包括:數據清理和轉換,數值模擬,統計建模,機器學習等等)

(Azure Functions 是一種無伺服器解決方案,可以使用戶減少程式碼編寫、減少需要維護的基礎結構並節省成本。 無需擔心部署和維護伺服器,雲基礎結構提供保持應用程式運行所需的所有最新資源。你只需專註於對你最重要的程式碼,Azure Functions 處理其餘程式碼。)

四、模式匹配增強功能

C# 9.0 版本進行模式匹配方面的改進如下:

  1)類型模式,匹配一個與特定類型匹配的對象;
  2)帶圓括弧的模式強制或強調模式組合的優先順序;(圓括弧模式允許編程人員在任何模式兩邊加上括弧)
  3)聯合 and 模式要求兩個模式都匹配;
  4)析取 or 模式要求任一模式匹配;
  5)否定 not 模式要求模式不匹配;
  6)關係模式要求輸入小於、大於、小於等於或大於等於給定常數。

// 類型模式,一個類型模式需要聲明一個標識符
void M(object o1, object o2)
{
    var t = (o1, o2);
    if (t is (int, string)) {} // 判斷 o1、o2 是 int、string 類型
    switch (o1) {
        case int: break; // 判斷 o1 是 int
        case System.String: break; // 判斷 o1 是 string
    }
}
// 關係模式,關係運算符<,<=等對應的模式
DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},
// 邏輯模式,用邏輯操作符and,or 和not將模式進行組合
DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},

後面模式中的任何一種都可在允許使用模式的任何上下文中使用:is 模式表達式、switch 表達式、嵌套模式以及 switch 語句的 case 標籤的模式。

模式組合器

  模式 組合器 允許匹配兩個不同模式 and(還可以通過重複使用)來擴展到任意數量的模式,方法是通過 and、or,或者使用的是模式的 求反 not 。

bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

五、模組初始值設定(ModuleInitializer)

為什麼要支援 模組或者程式集初始化工作:

  1)在庫載入的時候,能以最小的開銷、無需用戶顯式調用任何介面,使客戶做一些期望的和一次性的初始化;

  2)當前靜態構造函數方法的一個最大的問題是,運行時會對帶有靜態構造函數的類型做一些額外的檢查,這是因為要決定靜態構造函數是否需要被運行所必須的一步,但是這個又有著顯著的開銷影響;

  3)使源程式碼生成器在不需要用戶顯式調用一些東西的情況下能運行一些全局的初始化邏輯。

詳細內容

C# 9.0 將模組初始化器設計為一個 Attribute,用這個 Attribute 來修飾進行模組初始化邏輯的方法,就實現了模組初始化功能。這個 Attribute 被命名為 ModuleInitializerAttribute,具體定義如下:

using System;
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class ModuleInitializerAttribute : Attribute { }
}

如果要使用模組初始化器,你只要將 ModuleInitializerAttribute 用在符合下面要求的方法上就可以了。
  1)必須使靜態的、無參的、返回值為void的函數。
  2)不能是泛型或者包含在泛型類型里
  3)必須是可從其所在模組里訪問的。也就是說,方法的有效訪問符必須是 internal 或者 public,不能是局部方法。

using System.Runtime.CompilerServices;
class MyClass
{
    [ModuleInitializer]
    internal static void Initializer()
    {  ...  }
}

實踐

模組初始化器與靜態構造函數之間有著一定的關聯影響。因為模組初始化器是一個靜態方法,因而其被調用執行前,必然會引起其所處類型的靜態構造函數的執行。請參考下列示例:

static class ModuleInit
{
    static ModuleInit()
    {
        //先執行
        Console.WriteLine("ModuleInit靜態構造函數 cctor");
    }
    [ModuleInitializer]
    internal static void Initializer()
    {
        //在靜態構造函數執行後才執行
        Console.WriteLine("模組初始化器");
    }
}

在一個模組中指定多個模組初始化器的時候,他們之間的順序也是一個值得注意的問題。以上這些問題的存在,就要求我們注意以下幾點:
  1)在指定了模組初始化器的類型中,不要在靜態構造函數中,寫與模組初始化器中程式碼有著順序依賴程式碼,最好的就是不要使用靜態構造函數。
  2)多個模組初始化器之間的程式碼,也不要有任何依賴關係,保持各個初始化器程式碼的獨立性。

日常開發中,我們通常需要在模組初始化的時候,做一些前置性的準備工作,以前常採用靜態構造函數這種不具有全局性方法,局限性很大,現在,這些都得到了完美解決。

  參考:C# 9.0新特性詳解系列之三:模組初始化器

六、可以為 null 的引用類型規範

此功能添加了,兩種新類型的可為 null 的類型 (可以為 null 的引用類型和可以為 null 的現有值類型) 為 null 的泛型類型,並引入了靜態流分析以實現 null 安全。

可以為 null 的引用類型和可以為 null 的類型參數的語法 T? 與可以為 null 的值類型的短格式相同,但沒有相應的長格式。(DateTime 格式、TimeSpan 格式)

出於規範的目的,當前 nullable_type 被重命名為 nullable_value_type ,並新增了可空引用類型命名 nullable_reference_type nullable_type_parameter 可空類型參數。

non_nullable_reference_type nullable_reference_type 必須是不可 null 引用類型 (類、介面、委託或數組) 。

non_nullable_non_value_type_parameterIn nullable_type_parameter 必須是不被約束為值類型的類型參數。

可以為 null 的引用類型和可以為 null 的類型參數不能出現在以下位置:

1 作為基類或介面 2 作為的接收方 member_access
3 作為 type 中的 object_creation_expression 4 作為 delegate_type 中的 delegate_creation_expression
5 作為 type 中的 is_expression , catch_clause 或 type_pattern 6 作為 interface 完全限定的介面成員名稱中的

null 合併運算符

  E1 ?? E2     // 若 E1 為 null 則取 E2 的值

七、目標類型的 new 表達式

當類型已知時,則構造函數的類型標註不必須。

// 允許欄位初始化,而不顯示類型
Dictionary<string, List<int>> field = new() { { "item1", new() { 1, 2, 3 } } };
// 如果可從用法推斷,則允許省略類型
XmlReader.Create(reader, new() { IgnoreWhitespace = true });
// 實例化對象,而不會對類型進行拼寫檢查
private readonly static object s_syncObj = new();
(int a, int b) t = new(1, 2); // "new" 是不必要的
Action a = new(() => {}); // "new" 是不必要的
(int a, int b) t = new(); // 可以,類似 (0, 0)
Action a = new(); // 沒有發現構造函數

八、擴展分部方法

什麼是分布類、分部方法?

  拆分一個類、一個結構、一個介面或一個方法的定義到兩個或更多的文件中是可能的。 每個源文件包含類型或方法定義的一部分,編譯應用程式時將把所有部分組合起來

  分部方法要求,所在的類型有 partial 標識,同時分部方法也有 partial 進行標識。CLR其實是不知道所謂的分部方法的,都是編譯器在做。通過使用分部方法,可以將一個類型中的操作分散在多個文件中,方便開發

分部類的作用:

  1)處理大型項目時,使一個類分布於多個獨立文件中可以讓多位程式設計師同時對該類進行處理;

  2)當使用自動生成的源文件時,你可以添加程式碼而不需要重新創建源文件。 Visual Studio 在創建 Windows 窗體、Web 服務包裝器程式碼等時會使用這種方法。 你可以創建使用這些類的程式碼,這樣就不需要修改由 Visual Studio 生成的文件。

  3)若要拆分類定義,必須使用 partial 關鍵字修飾符。

partial 關鍵字指示可在命名空間中定義該類、結構或介面的其他部分。 所有部分都必須使用 partial 關鍵字。 在編譯時,各個部分都必須可用來形成最終的類型。 各個部分必須具有相同的可訪問性,如 publicprivate 等。

如果將任意部分聲明為抽象的,則整個類型都被視為抽象的;如果將任意部分聲明為密封的,則整個類型都被視為密封的;如果任意部分聲明基類型,則整個類型都將繼承該類。

指定基類的所有部分必須一致,但忽略基類的部分仍繼承該基類型。 各個部分可以指定不同的基介面,最終類型將實現所有分部聲明所列出的全部介面。 在某一分部定義中聲明的任何類、結構或介面成員可供所有其他部分使用。 最終類型是所有部分在編譯時的組合

註: partial 修飾符不可用於委託或枚舉聲明中。

[SerializableAttribute]
partial class Moon { }
[ObsoleteAttribute]
partial class Moon { }
// 上邊部分的兩次聲明,等同於以下聲明
[SerializableAttribute]
[ObsoleteAttribute]
class Moon { }

可以合併的內容包括:(XML 注釋)(介面)(泛型類型參數屬性)(class 特性)(成員)。

partial class Earth : Planet, IRotate { }
partial class Earth : IRevolve { }
// 與下面聲明等效
class Earth : Planet, IRotate, IRevolve { }

處理分部類定義時需遵循下面的幾個規則:

  1. 要作為同一類型的各個部分的所有分部類型定義都必須使用 partial 進行修飾。
  2. partial 修飾符只能出現在緊靠關鍵字 classstruct 或 interface 前面的位置。
  3. 分部類型定義中允許使用嵌套的分部類型。
partial class ClassWithNestedClass 
{ 
    partial class NestedClass { } 
} 
partial class ClassWithNestedClass 
{
    partial class NestedClass { } 
}

  4. 要成為同一類型的各個部分的所有分部類型定義都必須在同一程式集和同一模組(.exe 或 .dll 文件)中進行定義。 分部定義不能跨越多個模組。

  5. 類名和泛型類型參數在所有的分部類型定義中都必須匹配。 泛型類型可以是分部的。 每個分部聲明都必須以相同的順序使用相同的參數名。

  6. 下面用於分部類型定義中的關鍵字是可選的,但是如果某關鍵字出現在一個分部類型定義中,則該關鍵字不能與在同一類型的其他分部定義中指定的關鍵字衝突:(public、private、protect、internal、abstract、sealed、基類、new修飾符(嵌套部分)、泛型約束)。

// 分部結構和介面示例
partial interface ITest
{
    void Interface_Test();
}
partial interface ITest
{
    void Interface_Test2();
}
partial struct S1
{
    void Struct_Test() { }
}
partial struct S1
{
    void Struct_Test2() { }
}

分部類或結構可以包含分部方法。 類的一個部分包含方法的簽名。 可以在同一部分或另一個部分中定義可選實現。 如果未提供該實現,則會在編譯時刪除方法以及對方法的所有調用。

分部方法使類的某個部分的實施者能夠定義方法(類似於事件)。 分部類中的任何程式碼都可以隨意地使用分部方法,即使未提供實現也是如此。 調用但不實現該方法不會導致編譯時錯誤或運行時錯誤

在自定義生成的程式碼時,分部方法特別有用。 這些方法允許保留方法名稱和簽名,因此生成的程式碼可以調用方法,而開發人員可以決定是否實現方法。 與分部類非常類似,分部方法使程式碼生成器創建的程式碼和開發人員創建的程式碼能夠協同工作,而不會產生運行時開銷

 分部方法聲明由兩個部分組成:定義和實現。 它們可以位於分部類的不同部分中,也可以位於同一部分中。 如果不存在實現聲明,則編譯器會優化定義聲明和對方法的所有調用。

// 定義在 file1.cs
partial void OnNameChanged();

// 實現在 file2.cs
partial void OnNameChanged()
{
  // method body
}

九、靜態匿名函數

為了避免不必要的記憶體分配, C# 9.0 中引入 static 匿名函數。

如果想在 lambda 表達式里捕獲封閉方法的局部變數或者參數,那麼就會存在兩種堆分配,一種是委託上的分配,另一種是閉包上的分配,如果 lambda 表達式僅僅捕獲一個封閉方法的實例狀態,那麼僅會有委託分配,如果 lambda 表達式什麼都不捕獲或者僅捕獲一個靜態狀態,那麼就沒有任何分配。示例如下:

//  lambda 中需要獲取 y,所以就有了意想不到的堆分配
int y = 1;
MyMethod(x => x + y);
// 為了避免這種不必要和浪費記憶體的分配,可以在 lambda 上使用 static 關鍵詞或變數上標註 const
const int y = 1;
MyMethod(static x => x + y);
// 註:static 匿名函數不能訪問封閉方法的局部變數和參數和 this 指針,但可以引用它的 靜態方法 和 常量

如何使用靜態匿名方法:

// 通過兩步標記,來避免多餘的記憶體分配
public class Demo
{
    // 1/2 formattedText 上標記 const
    private const string formattedText = "{0} It was developed by Microsoft's Anders Hejlsberg in the year 2000.";
    void DisplayText(Func<string, string> func)
    {
        Console.WriteLine(func("C# is a popular programming language."));
    }
    public void Display()
    {
        // 2/2 lambda 上標記 static
        DisplayText(static text => string.Format(formattedText, text));
        Console.Read();
    }
}
class Program
{
    static void Main(string[] args)
    {
        new Demo().Display();
        Console.Read();
    }
}
// 若沒有今天加靜態標識,則:
// formattedText 變數會被 DisplayText 方法中的 func 所捕獲,這也就意味著它會產生你意料之外的記憶體分配

現在就可以使用 static + const 組合來提升應用程式性能了,同時也可以有效的阻止在 lambda 中誤用封閉方法中的局部變數和參數引發的不必要開銷。

 參考:如何在 C#9 中使用 static 匿名函數

十、目標類型(Target-Typed)的條件表達式

對於條件表達式: c ? e1 : e2

  當 e1 和 e2 沒有通用類型,或它們的通用類型為 e1 或者 e2,但另一個表達式沒有到該類型的隱式轉換

我們定義了一個新的隱式條件表達式轉換,該轉換允許從條件表達式到任何類型(T)的隱式轉換(從 e1 到 T 的轉換,以及從 e2 到 T 的轉換)。如果條件表達式在 e1 和 e2 之間,既沒有通用類型,也不符合條件表達式轉換,則是錯誤的。

_ = (A)(b ? c : d);

其中 c 的類型為 C,d 的類型為 D ,並且存在從 C 到 D 的隱式用戶定義的轉換, 以及從 D 到 A 的隱式用戶定義的轉換,以及從 C 到 A 的隱式用戶定義的轉換。

  • 如果在 C# 9.0 之前編譯此程式碼,則在 b 為 true 時,我們會將 c 從 D 轉換 A 為。
  • 如果使用條件表達式轉換,則當 b 為 true 時,將直接從轉換 c 為 A ,從而減少了一次操作

因此,我們將 條件表達式轉換 視為轉換中的最後一個手段,以保留現有行為。

十一、協變返回類型(Covariant returns type)

我們經常會遇到實現基類型的抽象方法時,返回值是固定的一個抽象類類型,示例:

public abstract class A
{
    public abstract A? GetNewOne(A? val); // 固定類型 A?
}
public sealed class B : A
{
    public override A? GetNewOne(A? val) => val as B; // 實現抽象方法時,仍要返回固定類型 A?
}

可以看到,這裡返回的結果要麼是 B 這個派生類類型的,也可以是 null,但總之跟 A 除了一個繼承關係也就沒啥別的關係了。

當我們想調用 B 類中的 GetNewOne 方法的時候能夠立即得到 B 類的實例或 null 的話,C# 9 就直接允許我們把返回值類型改成 B?,以後就不必每次調用的時候還強制轉換一下了。

這個協變返回類型就是這裡重寫方法的返回類型 B? 了。

public override B? GetNewOne(A? val) => val as B;

十二、迭代器擴展(擴展 GetEnumerator 方法來支援 foreach 循環)

允許 foreach 循環,識別擴展了方法 GetEnumerator 的類型。

也就是說,對於不支援 foreach 的類型,只要我們為這個類型實現 GetEnumerator 的擴展方法,那麼這個類型就可以用 foreach 循環了。

如果我要實現一個功能,來獲取這個 int 類型數據的所有比特位為 1 的這個偏移量的話,就只能寫一個比較丑的方法,然後去調用它了,示例:

public static class Utils
{
    public static IEnumerator<int> GetEnumerator(this int @this)
    {
        for (int i = 0, v = @this; i < 32; i++, v >>= 1)
        {
            if ((v & 1) != 0)
            {
                yield return i;
            }
        }
    }
}
// 於是我們就可以對 int 類型的值應用 foreach
foreach (int offset in 17)
{
    // ...
}

十三、lambda 棄元參數

允許棄元( _ )用作 lambda 表達式和匿名方法的參數。寫法:

(_, _) => 0 , (int _, int _) => 0 // lambda 表達式
delegate(int _, int _) { return 0; } // 匿名方法

當且僅當參數同時有兩個及以上的都不用的話,棄元才生效。如果 Lambda 只需要一個參數的時候,即使你寫 _,它也是一個正常的變數。

textBoxHello.TextChanged += (_, _) =>
{
    // ...
};

在上面這個情景下的時候,Lambda 棄元會比較有用:再給【一個控制項賦值一個事件處理方法,且該方法直接用 Lambda 表達式賦值】的時候。

十四、本地函數的屬性(Attributes on local functions)

本地函數是 C# 7 新增的一個概念,在當前 C# 9.0 允許本地函數聲明屬性。 包括本地函數上的參數和類型參數。

在 C# 7 里,本地函數是一個高級版的委託變數,它允許捕獲變數,也允許傳入 Lambda 的時候正常傳遞,這就是一個委託變數嘛!所以,既然是一個普通的變數,當然就不能標註特性了。

如果程式碼確實比較短,想讓程式碼使用類似 C/C++ 的內聯關鍵字 inline ,加個特性就行了,C# 9.0 允許我們加特性到本地函數上(如下程式碼示例):

(調用函數需要 CPU 執行參數壓棧、暫存器保存與恢復、跳轉指令等操作,開銷比較大,高頻繁的調用函數對性能有影響,在 C/C++ 語言里產生了 Macro 宏,由於宏不是函數不會產生上述開銷,是一種比較好的優化,但宏不是強類型編程,於是 VC++ 產生了 inline 內聯函數,inline 優化就是將內聯函數展開,就沒有了函數調用的 CPU 開銷,性能上等同於宏,而且是強類型編程)

public void Method()
{
    var rng = new Random();
    Console.WriteLine(g());
    Console.WriteLine(g());
    Console.WriteLine(g());
    // Local function.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    int g() => rng.Next(1, 100);
}

現在允許對本地函數使用修飾符 extern 這使本地函數成為外部方法的外部函數。

與外部方法類似,外部本地函數的本地函數體必須是分號。 只允許對外部本地函數的局部函數體使用分號。外部本地函數也必須是 static 。

另:本地函數和 Lambda 表達式的語法非常相似,但本地函數更能節省時間和空間上的開銷。參見:C# 中的本地函數

十五、本機大小的整數

本機大小的整數包含(有符號) nint 和(無符號,大於 0) nuint 兩種類型,都是整數類型。它們由基礎類型 System.IntPtr 和 System.UIntPtr 表示。編譯器將這些類型的其他轉換和操作作為本機整數公開。

在運行時獲取本機大小的整數大小,可以使用 sizeof()。 但是,必須在不安全的上下文中編譯程式碼。例如:

Console.WriteLine($"size of nint = {sizeof(nint)}");
Console.WriteLine($"size of nuint = {sizeof(nuint)}");
// output when run in a 64-bit process
//size of nint = 8
//size of nuint = 8
// output when run in a 32-bit process
//size of nint = 4
//size of nuint = 4

// 也可以通過靜態 IntPtr.Size 和 UIntPtr.Size 屬性獲得等效的值。

 本機大小的整數定義 MaxValue 或 MinValue 的屬性。 這些值不能表示為編譯時編譯時,因為它們取決於目標電腦上整數的本機大小

若要在運行時獲取本機大小的整數的最小值和最大值,請將 MinValue 和 MaxValue 用作 nint 和 nuint 關鍵字的靜態屬性,如以下示例中所示:

Console.WriteLine($"nint.MinValue = {nint.MinValue}");
Console.WriteLine($"nint.MaxValue = {nint.MaxValue}");
Console.WriteLine($"nuint.MinValue = {nuint.MinValue}");
Console.WriteLine($"nuint.MaxValue = {nuint.MaxValue}");
//  output when run in a 64-bit process----
//nint.MinValue = -9223372036854775808
//nint.MaxValue = 9223372036854775807
//nuint.MinValue = 0
//nuint.MaxValue = 18446744073709551615
//  output when run in a 32-bit process----
//nint.MinValue = -2147483648
//nint.MaxValue = 2147483647
//nuint.MinValue = 0
//nuint.MaxValue = 4294967295

這些值在運行時是只讀的。編譯器可將這些類型隱式和顯式轉換為其他數值類型。 

可在以下範圍內對 nint 使用常量值:[int.MinValue .. int.MaxValue]。

可在以下範圍內對 nuint 使用常量值:[uint.MinValue .. uint.MaxValue]。

沒有適用於本機大小整數文本的直接語法。 沒有後綴可表示文本是本機大小整數,例如 L 表示 long。 可以改為使用其他整數值的隱式或顯式強制轉換。 例如:

nint a = 42
nint a = (nint)42;

十六、函數指針(Function pointers)

C# 的函數指針由於會兼容 C 語言和 C++ 的函數,因此會有託管函數(託管方法,Managed Function)和非託管函數(非託管方法,Unmanaged Function)的概念。

  託管函數:函數由 C# 語法實現,底層也是用的 C# 提供的 CLR 來完成的。
  非託管函數:函數並不由 C# 實現,它不受 C# 語法控制,而是通過 DLL 文件交互使用。

本文只簡單介紹下託管函數的函數指針,詳情可參考大牛文章:探索 C# 9 的函數指針

託管函數的函數指針

先來說一下 C# 函數內部的函數指針(託管函數的函數指針)

unsafe
{
    int arr[] = { 3, 8, 1, 6, 5, 4, 7, 2, 9 };
    delegate* managed<int, int, int> funcPtr = &compareTwoValue; // Here.
    bubbleSort(arr, funcPtr); // Pass the function pointer.
}
static int compareTwoValue(int a, int b) => a - b;
static unsafe void bubbleSort(int* arr, delegate* managed<int, int, int> comparison)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1 - i; j++)
        {
            if (comparison(arr[j], arr[j + 1]) >= 0)
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

就是把 C 里的 int (*ptr)(int, int) 改成 delegate* managed<int, int, int>

先寫函數記號 delegate 關鍵字,然後帶一個星號。這兩個東西是函數指針聲明的頭部,是固定不變的語法規則。接著,指針符號後面寫上 managed 關鍵字,這也是 C# 9 里提供的一個新關鍵字,它在這裡的語義表示一個「託管函數」。然後使用委託的類似語法:尖括弧里寫類型參數表列,最後一個類型參數是整個函數的返回值類型。如果一個函數沒有參數,返回值為 void 就寫成 delegate* managed<void>,如果有多個參數,就把參數挨個寫上去,然後返回值上追加一個類型參數在末尾就可以了。

另外,managed 默認可以不寫,因為 C# 的函數指針默認是指向託管函數的,於是,記號就簡化成了 delegate*<int, int, int>。當然,你得注意一下,函數指針是不安全的,所以需要先寫 unsafe 才能用

十七、跳過臨時變數初始化(Skip locals initialization)

這個特性不屬於 C# 語法的特性,它是為 .NET 的 API 里添加了一個新的特性(attribute):SkipLocalsInitAttribute,它用來告訴當前程式碼塊里,所有的變數都在定義的時候能不初始化的地方都不初始化,以便優化程式碼執行的效率。

十八、

在 c # 8 中, ? 批註僅適用於顯式約束為值類型或引用類型的類型參數。 在 c # 9 中, ? 批註可應用於任何類型參數,而不考慮約束。

除非將類型形參顯式約束為值類型,否則只能在上下文中應用注釋 #nullable enable 。

如果類型參數 T 替換為引用類型,則 T? 表示該引用類型的可以為 null 的實例。

// 如果類型參數 T 替換為引用類型,則 T? 表示該引用類型的可以為 null 的實例
var s1 = new string[0].FirstOrDefault();  // string? s1
var s2 = new string?[0].FirstOrDefault(); // string? s2
// 如果 T 用值類型替換,則 T? 表示的實例 T 
var i1 = new int[0].FirstOrDefault();  // int i1
var i2 = new int?[0].FirstOrDefault(); // int? i2
// 如果 T 使用批註類型替換 U? ,則 T? 表示批註的類型 U? 而不是 U?? 
var u1 = new U[0].FirstOrDefault();  // U? u1
var u2 = new U?[0].FirstOrDefault(); // U? u2
// 如果 T 將替換為類型 U ,則 T? 表示 U? ,即使在上下文中也是如此 #nullable disable 
#nullable disable
var u3 = new U[0].FirstOrDefault();  // U? u3
// 對於返回值, T? 等效於 [MaybeNull]T ; 對於參數值, T? 等效於 [AllowNull]T 
// 在使用 c # 8 編譯的程式集中重寫或實現介面時,等效性非常重要
public abstract class A
{
    [return: MaybeNull] public abstract T F1<T>();
    public abstract void F2<T>([AllowNull] T t);
}
public class B : A
{
    public override T? F1<T>() where T : default { ... }       // matches A.F1<T>()
    public override void F2<T>(T? t) where T : default { ... } // matches A.F2<T>()
}

終極參考:C# 9.0 功能-官網

部分參考:C# 9 特性一覽及評價

註:暫時整理到這裡,歡迎指正和補充。