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

2022-11-03 18:01:00

一、記錄(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 特性一覽及評價

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