Stories

Detail Return Return

C# 的模式匹配概述 - Stories Detail

模式匹配是一種通過測試表達式來確定其是否具有特定特徵的技術。C# 的模式匹配提供了更簡潔的語法來測試表達式並根據表達式是否匹配採取相應行動。“is 表達式” 支持模式匹配以測試表達式,並根據該表達式的結果有條件地聲明一個新的變量。“switch 表達式” 允許您根據表達式的第一個匹配模式執行操作。這兩種表達式都支持豐富的模式詞彙。

本文概述了可以運用模式匹配的場景。這些技術能夠提升您代碼的可讀性和正確性。

null 值檢查

模式匹配中最常見的場景之一是確保值不為 null。您可以使用以下示例來測試並將可 null 值類型轉換為其底層類型,同時使用 null 來進行測試:

int? kn = 24;
if ( kn is int zhs )
    {
        Console . WriteLine ( $"允許 null 的 int‘kn’ 具有值 {zhs}" );
    }
else
    {
        Console . WriteLine ( $"允許 null 的 int‘kn’不具有值" );
    }

上述代碼是一種用於測試變量類型並將其賦值給新變量(zhs)的聲明模式。語言規則使得這種技術比其他許多方法更加安全。變量 zhs 僅在 if 語句的 true 分支中可訪問和賦值。如果您在 else 語句中或在 if 語句塊之後嘗試訪問 zhs,編譯器會發出錯誤提示。其次,由於您未使用 == 運算符,因此這種模式在類型重載了 == 運算符的情況下也能正常工作。這使其成為檢查 null 引用值的理想方式,並添加了 “not” 模式:

string? 信息 = DQ信息或默認 ( );

if ( 信息 is not null )
    {
        Console . WriteLine ( 信息 );
    }

前面的示例使用了一個常量模式來將變量與 null 進行比較。“not” 是一個邏輯模式,當否定模式不匹配時,它會匹配。

類型測試

模式匹配的另一種常見用途是測試一個變量是否符合特定類型。例如,以下代碼會測試一個變量是否非 null,並實現 System . Collections . Generic . IList < T > 接口。如果滿足條件,它會使用該列表的 ICollection < T > . Count 屬性來找到中間索引。聲明模式不會匹配 null 值,無論變量在編譯時的類型如何。下面的代碼除了防止類型不實現 IList 的情況外,還防止了變量為 null 的情況。

static T ZD < T > ( IEnumerable < T > XuLie )
    {
        if ( XuLie is IList < T > list )
            {
                return list [ list . Count / 2 ];
            }
        else if ( XuLie is null )
            {
                throw new ArgumentNullException ( nameof ( XuLie ) , "序列不能為 null。" );
            }
        else
            {
                int z半 = XuLie . Count ( ) / 2 - 1;
                if ( z半 < 0 ) z半 = 0;
                return XuLie . Skip ( z半 ) . First ( );
            }
    }

在 switch 表達式中,可以對變量應用相同的測試來判斷其是否屬於多種不同的類型。您可以利用這些信息,根據特定的運行時類型創建更優的算法。

比較離散值

您還可以測試變量以查找特定值的匹配項。以下代碼展示了一個示例,其中將值與枚舉中聲明的所有可能值進行比較:

狀態 PO執行操作 ( Operation 操作 ) =>
    操作 switch
        {
            Operation . SystemTest => RunDiagnostics ( ),
            Operation . Start => StartSystem ( ),
            Operation . Stop => StopSystem ( ),
            Operation . Reset => ResetToReady ( ),
            _ => throw new ArgumentException ( "無效的操作枚舉值" , nameof ( 操作 ) ),
        };

前面的示例展示了基於枚舉值進行的方法調用方式。最後一個 “case” 是棄用模式,它能匹配所有值。它處理的是那些值與定義的枚舉值不匹配的任何錯誤情況。如果您省略了那個 “switch” 分支,編譯器會警告您,您的模式表達式沒有處理所有可能的輸入值。在運行時,如果要檢查的對象與任何 “switch” 分支都不匹配,那麼 “switch” 表達式就會拋出異常。您可以使用數字常量而非一組枚舉值。您還可以將此類似的技術用於表示命令的常量字符串值:

狀態 PO執行操作 ( string 操作 ) =>
    操作 switch
        {
            "SystemTest" => RunDiagnostics ( ),
            "Start" => StartSystem ( ),
            "Stop" => StopSystem ( ),
            "Reset" => ResetToReady ( ),
            _ => throw new ArgumentException ( "無效的操作字符串" , nameof ( 操作 ) ),
        };

前面的示例展示了相同的算法,但使用的是字符串值而非枚舉值。如果您的應用程序響應的是文本命令而非常規數據格式,那麼您就會採用這種場景。從 C# 11 開始,您還可以使用 Span < char > 或 ReadOnlySpan < char > 來測試常量字符串值,如以下示例所示:

狀態 PO執行操作 ( ReadOnlySpan < Char > 操作 ) =>
    操作 switch
        {
            "SystemTest" => RunDiagnostics ( ),
            "Start" => StartSystem ( ),
            "Stop" => StopSystem ( ),
            "Reset" => ResetToReady ( ),
            _ => throw new ArgumentException ( "無效的操作字符串" , nameof ( 操作 ) ),
        };

在所有這些示例中,丟棄模式確保您處理了所有的輸入。編譯器會幫助您,確保處理了所有可能的輸入值。

關係模式

您可以使用關係模式來測試某個值與常量之間的比較情況。例如,以下代碼根據華氏温度返回水的狀態:

string FF水 ( int 華氏度 ) =>
    華氏度 switch
        {
            < 32 => "冰",
            32 => "冰 / 水轉變",
            ( > 32 ) and ( < 212 ) => "水",
            212 => "水 / 水蒸氣轉變",
            > 212 => "水蒸氣",
        }

上述代碼還展示了用於檢查兩個關係模式是否匹配的聯合和邏輯模式。您還可以使用一個或多個選擇模式來檢查任一模式是否匹配。這兩個關係模式被括號包圍,您可以將任何模式都置於括號中以增強可讀性。這兩個明確的切換分支(32°F 和 212°F)分別處理熔點和沸點的情況。如果沒有這兩個分支,編譯器會警告您,您的邏輯沒有涵蓋所有可能的輸入情況。

上述代碼還展示了編譯器為模式匹配表達式提供的另一個重要特性:如果您沒有處理所有的輸入值,編譯器會向您發出警告。如果一個 switch 語句分支的模式被之前的模式所覆蓋,編譯器也會發出警告。這使您能夠自由地對 switch 表達式進行重構和重新排列。另一種編寫相同表達式的方式可以是:

string FF水2 ( int 華氏度 ) =>
    華氏度 switch
        {
            < 32 => "冰",
            32 => "冰 / 水轉變",
            < 212 => "水",
            212 => "水 / 水蒸氣轉變",
            _ => "水蒸氣",
        }

在上述示例以及任何其他重構或重新排序的操作中,關鍵在於:編譯器會驗證您的代碼能夠處理所有可能的輸入情況。

多個輸入

到目前為止所涉及的所有模式都只檢查了一個輸入。您可以編寫能夠檢查對象多個屬性的模式。請看下面的 “DD(訂單)” 記錄:
public record DD ( int 項目數 , decimal 成本 );
上述位置記錄類型在明確的位置聲明瞭兩個成員。首先出現的是 “項目數”,然後是訂單的 “成本”。

以下代碼會根據訂單中的商品數量和價格來計算折扣後的總價:

static void Main(string[] args)
    {
        decimal dec折扣;
        DD d1 = new ( 16 , 2400 );
        dec折扣 = FF計算折扣 ( d1 );
        Console . WriteLine ( $"{d1} 應享受折扣:{dec折扣},折後金額:{d1 . 成本 - d1 . 成本 * dec折扣}" );
    }

public static Decimal FF計算折扣 ( DD 訂單 ) => 訂單 switch
    {
        { 項目數: > 10, 成本: > 1000 } => 0.1m,
        { 項目數: > 5 , 成本: > 500 } => 0.05m,
        { 成本: > 250 } => 0.02m,
        null => throw new ArgumentNullException ( nameof ( 訂單 ) , "無法對無訂單情況進行折扣計算" ),
        var XMs => 0m,
    };

前兩個條件分支分別檢查 “順序” 對象的兩個屬性。第三個條件分支僅檢查成本。接下來的條件分支會檢查為 null 的情況,最後的條件分支則會匹配任何其他值。如果 “順序” 類型定義了合適的 “分解” 方法,您就可以省略模式中的屬性名稱,並使用分解來檢查屬性:

public static Decimal FF計算折扣 ( DD 訂單 ) => 訂單 switch
    {
        { > 10 , > 1000 } => 0.1m,
        { > 5 , > 500 } => 0.05m,
        { 成本: > 250 } => 0.02m,
        null => throw new ArgumentNullException ( nameof ( 訂單 ) , "無法對無訂單情況進行折扣計算" ),
        var XMs => 0m,
    };

上述代碼展示了這種位置模式,即對屬性進行分解以形成表達式。

您還可以將屬性與 {} 進行匹配,這樣就能匹配任何非 null 值。請看下面的聲明示例,它用於存儲帶有可選註釋的測量數據:

public record class Zhi觀測值 ( int 值 , string 單位 , string 名稱 )
    {
        public string? 註釋 { get; set; }
    }

您可以使用以下模式匹配表達式來檢驗給定的觀察結果是否具有非 null 註釋:

if ( Zhi觀測值 . 註釋 is { } )
    {
        Console . WriteLine ( $"觀測值註釋: {觀測值 . 註釋}" );
    }

列表模式

您可以使用列表模式來檢查列表或數組中的元素。列表模式提供了一種將模式應用於序列中任何元素的方法。此外,您可以應用 “丟棄模式”( _ )來匹配任何元素,或者應用切片模式來匹配零個或多個元素。
當數據沒有遵循固定的結構時,列表模式便成為一種非常有用的工具。您可以利用模式匹配來檢驗數據的形狀和數值,而無需將其轉換為一組對象。

請看以下從一個文本文件中截取的銀行交易內容:

2020 年  4 月  1 日,存款,初始存款,    2250.00
2020 年  4 月 15 日,存款,退款,    125.65
2020 年  4 月 18 日,存款,工資,    825.65
2020 年  4 月 22 日,取款,借記,食品雜貨,    255.73
2020 年  5 月  1 日,取款,#1102,房租,公寓,    2100.00
2020 年  5 月  2 日,利息,    0.65
2020 年  5 月  7 日,取款,借記,電影,    12.57
2020 年  4 月 15 日,費用,    5.55

這是一種 CSV 格式,但有些行包含的列數多於其他行。對於處理來説情況更糟的是,WITHDRAWAL 類型中的一個列包含用户生成的文本,並且該文本中可能包含逗號。一個包含丟棄模式、常量模式和變量模式的列表模式用於捕獲值,以處理這種格式的數據:

decimal LiXi = 0m;
foreach ( string [ ] 買賣 in DQ記錄 ( ) )
    {
        LiXi += 買賣 switch
            {
                [ _ , "存款" ,  _ , var Cun ] => decimal . Parse ( 金額 ),
                [ _ , "取款" , .. , var Qu ] => -decimal . Parse ( 金額 ),
                [ _ , "利息" , var Li ] => decimal . Parse ( 金額 ),
                [ _ , "費用" , var Fei ] => -decimal . Parse ( Fei ),
                _ => throw new InvalidOperationException ( $"記錄 {string . Join ( "," , 買賣 )} 不符合預期的格式!" ),
        };

    Console . WriteLine ( $"記錄:{string . Join ( "," , 買賣 )} , 新利息:{LiXi:C}" );
    }

上述示例中使用了一個字符串數組,其中每個元素代表一行中的一個字段。switch 表達式依據第二個字段來確定交易的類型以及剩餘列的數量。每一行都能確保數據格式正確。丟棄模式( _ )會跳過第一個字段(即交易日期)。第二個字段匹配交易類型。剩餘元素匹配跳轉到金額字段。最後的匹配使用 var 模式來捕獲金額的字符串表示形式。該表達式計算要從餘額中增加或扣除的金額。

列表模式使您能夠根據一系列數據元素的形狀進行匹配。您使用 “丟棄” 和 “切片” 模式來匹配元素的位置。您還使用其他模式來匹配單個元素的特定特徵。

本文介紹了在 C# 中使用模式匹配時可以編寫的各種代碼類型。接下來的幾篇文章將展示更多在不同場景中運用模式的示例,並且還會詳細介紹可用的全部模式詞彙表。

丟棄

“丟棄變量” 是應用程序代碼中特意未使用的佔位符變量。丟棄變量與未賦值的變量相同;它們沒有值。丟棄變量向編譯器和其他閲讀您代碼的人員傳達了這樣的意圖:您有意忽略表達式的結果。您可能希望忽略表達式的結果、元組表達式的一個或多個成員、方法的輸出參數,或者模式匹配表達式的目標。

棄用聲明能清晰地表明代碼的意圖。棄用聲明意味着我們的代碼永遠不會使用該變量。它們能提高代碼的可讀性和可維護性。

您可以通過將變量的名稱設為下劃線( _ )來表明該變量為 “丟棄變量”。例如,以下方法調用會返回一個元組,其中第一個和第二個值為 “丟棄值”。SJD面積 是一個先前已聲明的變量,其值被設置為從 HQ城市信息 方法返回的第三個組件:
( _ , _ , SJD面積 ) = CHSH . HQ城市信息 ( 城市名 );
您可以使用 “丟棄” 關鍵字來指定 lambda 表達式中未使用的輸入參數。

當 “_” 是一個有效的丟棄標識符時,如果嘗試獲取其值或在賦值操作中使用它,就會引發編譯錯誤 CS0103,“當前上下文中不存在名為 '_' 的標識符”。此錯誤的原因在於 “_” 未被賦予值,甚至可能也沒有被分配存儲位置。如果它是一個實際的變量,那麼就不能像前面的例子那樣丟棄多個值。

元組和對象拆解

在處理元組時,棄值非常有用,當您的應用程序代碼使用某些元組元素但忽略其他元素時。例如,以下的 FF查詢隔年人物信息 方法返回一個包含人物名稱、日齡、早年年份、該年份的身高、晚年以及該年份的身高的元組。該示例展示了這兩個年份之間的身高變化情況。從這個元組中可獲取的數據中,我們不關心小妹妹現在的身高,而在設計時我們已知人物名稱和這兩個日期。因此,我們只對元組中存儲的兩個身高值感興趣,並可以將其餘值當作棄值處理。

static void Main ( string [ ] args )
    {
        var (_, Ling日, _, nian1, _, nian2) = FF查詢隔年人物信息 ( "黃麗華" , 2005 , 2018 );
        Console . WriteLine ( $"黃麗華同學 {Ling日 . TotalDays}(日齡,約 {Ling日 . Days / 365} 歲)在 2005 年到 2018 年身高的變化:{nian2 - nian1}mm。" );
    }

static ( string , TimeSpan , int , int , int , int ) FF查詢隔年人物信息 ( string 姓名 , int 早年 , int 晚年 )
    {
        int SHG早年 = 0 , SHG晚年 = 0;
        TimeSpan Ling日 = new TimeSpan ( );

        if ( 姓名 == "黃麗華" )
            {
                Ling日 = DateTime . Now . Subtract ( new DateTime ( 1996 , 12 , 8 ) );
                if ( 早年 == 2005 )
                    {
                        SHG早年 = 1356;
                    }
                if ( 晚年 == 2018 )
                    {
                        SHG晚年 = 1677;
                    }
            return ( 姓名, Ling日, 早年, SHG早年, 晚年, SHG晚年 );
        }
            return ( "", TimeSpan . Zero, 0, 0, 0, 0 );

類 Deconstruct、結構或接口的方法還允許檢索和解構對象中的特定數據集。如果想只使用析構值的一個子集,可使用棄元。以下示例將對象解構為四個 Ren 字符串(姓和名、城市和省),但放棄名和省。

static void Main ( string [ ] args )
    {
        var r = new Ren ( "劉", "曉晴", "晴晴", "淄博市", "山東省");

        // 解構 Ren 對象
        var ( fName , _ , city , _ ) = r1;
        Console . WriteLine ( $"你好,{city} 的 {fName}!" );
    }

public class Ren
    {
        public string Xing { get; set; }
        public string Ming { get; set; }
        public string NiCh { get; set; }
        public string ChSh { get; set; }
        public string Sheng { get; set; }

        public Ren ( string 姓 , string 名 , string 暱稱 , string 所在城市 , string 所在省份 )
            {
                Xing = 姓;
                Ming = 名;
                NiCh = 暱稱;
                ChSh = 所在城市;
                Sheng = 所在省份;
            }

        public void Deconstruct ( out string 姓 , out string 名 )
            {
                姓 = Xing;
                名 = Ming;
            }

    public void Deconstruct ( out string 姓 , out string 名 , out string 暱稱 )
            {
                姓 = Xing;
                名 = Ming;
                暱稱 = NiCh;
            }

    public void Deconstruct ( out string 姓 , out string 名 , out string 城市 , out string 省 )
        {
            姓 = Xing;
            名 = Ming;
            城市 = ChSh;
            省 = Sheng;
        }
    }

利用 switch 的模式匹配

棄元模式可通過 switch 表達式用於模式匹配。每個表達式(包括 null)始終匹配丟棄模式。

下面的示例定義一個 ProvidesFormatInfo 方法,該方法使用 switch 表達式來確定對象是否提供 IFormatProvider 實現並測試對象是否為 null。它還使用佔位符模式來處理任何其他類型的非 null 對象。

object? [ ] objects = [ CultureInfo . CurrentCulture,
                        CultureInfo . CurrentCulture . DateTimeFormat,
                        CultureInfo . CurrentCulture . NumberFormat,
                        new ArgumentException ( ),
                        null ];
foreach ( var obj in objects )
    ProvidesFormatInfo ( obj );

static void ProvidesFormatInfo( object? obj ) =>
    Console.WriteLine( obj switch
    {
        IFormatProvider fmt => $"{fmt . GetType ( )} object",
        null => "A null object reference: Its use could result in a NullReferenceException",
        _ => "Some object type without format information"
    } );
// The example displays the following output:
//    System.Globalization.CultureInfo object
//    System.Globalization.DateTimeFormatInfo object
//    System.Globalization.NumberFormatInfo object
//    Some object type without format information
//    A null object reference: Its use could result in a NullReferenceException

對具有 out 參數的方法的調用

調用 Deconstruct 該方法以解構用户定義的類型(類、結構或接口的實例)時,可以放棄各個 out 參數的值。 但是,在調用任何帶有out參數的方法時,也可以放棄out參數的值。

以下示例調用 DateTime . TryParse ( String , out DateTime ) 方法以確定日期的字符串形式在當前文化中是否有效。因為此示例僅涉及驗證日期字符串,而不是分析它以提取日期,因此該方法中的 out 參數是一個棄元。

string [ ] dateStrings = [ "05/01/2018 14:57:32.8" , "2018-05-01 14:57:32.8" ,
                           "2018-05-01T14:57:32.8375298-04:00" , "5/01/2018" ,
                           "5/01/2018 14:57:32.80 -07:00" ,
                           "1 May 2018 2:57:32.8 PM" , "16-05-2018 1:00:32 PM" ,
                           "Fri, 15 May 2018 20:10:57 GMT" ];
foreach ( string dateString in dateStrings )
    {
        if ( DateTime . TryParse ( dateString , out _ ) )
            Console . WriteLine ( $"‘{dateString}’:有效" );
        else
            Console . WriteLine ( $"‘{dateString}’:無效" );
    }
//       ‘05/01/2018 14:57:32.8’:有效
//       ‘2018-05-01 14:57:32.8’:有效
//       ‘2018-05-01T14:57:32.8375298-04:00’:有效
//       ‘5/01/2018’:有效
//       ‘5/01/2018 14:57:32.80 -07:00’:有效
//       ‘1 May 2018 2:57:32.8 PM’:有效
//       ‘16-05-2018 1:00:32 PM’:無效
//       ‘Fri, 15 May 2018 20:10:57 GMT’:無效

獨立棄元

可使用獨立棄元來指示要忽略的任何變量。一個典型的用途是使用賦值來確保參數不為 null。下面的代碼使用棄元來強制賦值。賦值的右側使用 Null 合併操作符,用於在參數為 System . ArgumentNullException 時引發 null。代碼不需要賦值的結果,因此會被丟棄。表達式強制執行 null 值檢查。丟棄操作闡明瞭你的意圖:分配結果不需要或不會被使用。

public static void Method ( string arg )
    {
        _ = arg ?? throw new ArgumentNullException ( paramName: nameof ( arg ) , message: "參數不能為 null" );

        // 做點什麼……
    }

以下示例使用獨立佔位符來忽略異步操作返回的 Task 對象。分配任務的效果等同於抑制操作即將完成時所引發的異常。它使你的意圖清晰:你想要丟棄 Task,並忽略從該異步操作中產生的任何錯誤。

private static async Task ExecuteAsyncMethods ( )
    {
        Console . WriteLine ( "關於啓動任務……" );
        _ = Task . Run ( ( ) =>
            {
                var CS迭代 = 0;
                for ( int ctr = 0 ; ctr < int . MaxValue ; ctr++ )
                    CS迭代++;
                    Console . WriteLine ( "結束循環操作……" );
                    throw new InvalidOperationException ( );
            } );
        await Task . Delay ( 5000 );
        Console . WriteLine ( "五秒後退出" );
    }
//       關於啓動任務……
//       結束循環操作……
//       五秒後退出

如果不將任務分配給棄元,則以下代碼會生成編譯器警告:

private static async Task ExecuteAsyncMethods ( )
    {
        Console . WriteLine ( "關於啓動任務……" );
        // CS4014:由於此調用並非 await,因此在調用完成之前,當前方法的執行將繼續
        // 考慮將 “await” 操作符應用於該調用的結果
        Task.Run ( ( ) =>
            {
                var CS迭代 = 0;
                for ( int ctr = 0 ; ctr < int . MaxValue ; ctr++ )
                    CS迭代++;
                    Console . WriteLine ( "結束循環操作……" );
                    throw new InvalidOperationException ( );
            });
        await Task . Delay ( 5000 );
        Console . WriteLine ( "五秒後退出" );

備註:如果使用調試器運行上述兩個示例之一,調試器將在引發異常時停止程序。如果沒有附加調試器,則在這兩種情況下都以無提示方式忽略異常。

也是有效的標識符。在受支持的上下文之外使用時, 不會被視為放棄,而是被視為有效的變量。如果名為 的標識符已在範圍內,則使用 作為獨立佔位符可能導致:

  • 將預期的佔位符的值賦給範圍內 _ 變量,會導致該變量的值被意外修改。例如:

    private static void ShowValue ( int _ )
      {
          byte [ ] arr = [ 0 , 0 , 1 , 2 ];
          _ = BitConverter . ToInt32 ( arr , 0 );
          Console . WriteLine ( _ );
      }
    //       33619968
  • 由於違反類型安全而產生的編譯錯誤。例如:

    private static bool RoundTrips ( int _ )
      {
          string value = _ . ToString();
          int newValue = 0;
          _ = Int32 . TryParse ( value , out newValue );
          return _ == newValue;
      }
    //      error CS0029:無法將類型 “bool” 隱式轉換為 “int”

    析構元組和其他類型

    元組提供一種從方法調用中檢索多個值的輕量級方法。但是,一旦檢索到元組,就必須處理它的各個元素。按元素逐個操作比較麻煩,如下例所示。FF查詢病人信息 方法返回一個三元組,並通過單獨的操作將其每個元素分配給一個變量。

    public class Example
      {
          public static void Main ( )
              {
                  var result = FF查詢病人信息 ( "寧採臣" );
    
                  var XingMing = result . Item1;
                  var SHGao = result . Item2;
                  var TZhong = result . Item3;
    
                  // 處理數據
             }
    
      private static ( string , int , double ) FF查詢病人信息 ( string 姓名 )
          {
              if ( 姓名 == "寧採臣" )
                  return ( 姓名 , 168 , 64.5 );
    
              return ( "" , 0 , 0 );
          }
      }

    從對象檢索多個字段值和屬性值可能同樣麻煩:必須按成員逐個將字段值或屬性值賦給一個變量。

可從元組中檢索多個元素,或者在單個析構操作中從對象檢索多個字段值、屬性值和計算值。若要析構元組,請將其元素分配給各個變量。析構對象時,將選定值分配給各個變量。

元組

C# 提供內置的元組析構支持,可在單個操作中解包一個元組中的所有項。用於析構元組的常規語法與用於定義元組的語法相似:將要向其分配元素的變量放在賦值語句左側的括號中。例如,以下語句將四元組的元素分配給 4 個單獨的變量:
var ( 姓名 , 地址 , 城市 , 郵編 ) = contact . FF獲取地址信息 ( )
有三種方法可用於析構元組:

  • 可以在括號內顯式聲明每個字段的類型。以下示例使用此方法來析構由 FF獲取病人信息 方法返回的三元組。
    ( string 姓名 , int 年齡 , string 病史 ) = FF獲取病人信息 ( "寧採臣" );
  • 可使用 var 關鍵字,以便 C# 推斷每個變量的類型。將 var 關鍵字放在括號外。以下示例在析構由 FF獲取病人信息 方法返回的三元組時使用類型推理。
    var ( 姓名 , 年齡 , 病史 ) = FF獲取病人信息 ( "寧採臣" );
    還可在括號內將 var 關鍵字單獨與任一或全部變量聲明結合使用。
    ( string 姓名 , var 年齡 , var 病史 ) = FF獲取病人信息 ( "寧採臣" );
    前面的示例很繁瑣,不建議這樣做。
  • 最後,可以將元組解構為已聲明的變量。

    public static void Main ( )
      {
          string 姓名 = "寧採臣";
          int 身高 = 1752;
          double 體重 = 62.8;
    
          ( 姓名 , 身高 , 體重 ) = FF查詢病人信息 ( "聶小倩" );
    
          // 做點什麼吧……
      }
  • 可以在析構中混合變量聲明和賦值。

    public static void Main ( )
      {
          string 姓名 = "寧採臣";
          int 身高 = 1752;
    
          ( 姓名 , 身高 , double 體重 ) = FF查詢病人信息 ( "聶小倩" );
    
          // 做點什麼吧……
      }

即使元組中的每個字段都具有相同的類型,也不能在括號外使用特定類型。這樣做會生成編譯器錯誤 CS8136:“析構 ‘var (...)’ 形式不允許對 ‘var’ 使用特定類型。”

必須將元組的每個元素分配給一個變量。如果省略任何元素,編譯器將生成錯誤 CS8132:“無法將 ‘x’ 元素的元組析構為 ‘y’ 變量”。

使用棄元的元組元素

析構元組時,通常只需要關注某些元素的值。可利用 C# 對棄元的支持,棄元是一種僅能寫入的變量,且其值將被忽略。在賦值中使用下劃線字符 “_” 聲明一個棄元。您可以丟棄任意數量的值,一個丟棄標記 _ 表示所有已丟棄的值。

以下示例演示了對元組使用棄元時的用法。例如,以下的 FF查詢隔年人物信息 方法返回一個包含人物名稱、日齡、早年年份、該年份的身高、晚年以及該年份的身高的元組。該示例展示了這兩個年份之間的身高變化情況。從這個元組中可獲取的數據中,我們不關心小妹妹現在的身高,而在設計時我們已知人物名稱和這兩個日期。因此,我們只對元組中存儲的兩個身高值感興趣,並可以將其餘值當作棄值處理。

static void Main ( string [ ] args )
    {
        var (_, Ling日, _, nian1, _, nian2) = FF查詢隔年人物信息 ( "黃麗華" , 2005 , 2018 );
        Console . WriteLine ( $"黃麗華同學 {Ling日 . TotalDays}(日齡,約 {Ling日 . Days / 365} 歲)在 2005 年到 2018 年身高的變化:{nian2 - nian1}mm。" );
    }

static ( string , TimeSpan , int , int , int , int ) FF查詢隔年人物信息 ( string 姓名 , int 早年 , int 晚年 )
    {
        int SHG早年 = 0 , SHG晚年 = 0;
        TimeSpan Ling日 = new TimeSpan ( );

        if ( 姓名 == "黃麗華" )
            {
                Ling日 = DateTime . Now . Subtract ( new DateTime ( 1996 , 12 , 8 ) );
                if ( 早年 == 2005 )
                    {
                        SHG早年 = 1356;
                    }
                if ( 晚年 == 2018 )
                    {
                        SHG晚年 = 1677;
                    }
            return ( 姓名, Ling日, 早年, SHG早年, 晚年, SHG晚年 );
        }
            return ( "", TimeSpan . Zero, 0, 0, 0, 0 );

用户定義類型

C# 提供對解構元組類型、record 和 DictionaryEntry 類型的內置支持。但是,用户作為 class、struct 或 interface 的創建者,可通過實現一個或多個 Deconstruct 方法來析構該類型的實例。該方法返回 “void”。方法簽名中的 out 參數表示要解構的每個值。例如,Deconstruct 類的以下 Ren 方法返回姓、名和性別:
public void Deconstruct ( out string 姓 , out string 名 , out bool 性別 )
然後,可使用與下列代碼類似的賦值來析構名為 r 的 Ren 類的實例:
var ( 姓 , 名 , 性別 ) = r;
以下示例重載 Deconstruct 方法以返回 Ren 對象的各種屬性組合。單個重載返回:

  • 姓和名
  • 姓名和性別
  • 姓名、性別和城市名和省/市/自治區名。
static void Main ( string [ ] args )
    {
        var p = new Ren ( "劉", "曉晴", false, "淄博市", "山東省");

        // Deconstruct the person object.
        var ( 姓 , 名 , 性別 , 城市 , 省 ) = p;
        Console . WriteLine ( $"你好,{省}{城市} 的 {姓}{名}({性別})!" );
    }

public class Ren
    {
        public string Xing { get; set; }
        public string Ming { get; set; }
        public bool XingBie { get; set; }
        public string ChSh { get; set; }
        public string Sheng { get; set; }

        public Ren ( string 姓 , string 名 , bool 性別 , string 所在城市 , string 所在省份 )
            {
                Xing = 姓;
                Ming = 名;
                XingBie = 性別;
                ChSh = 所在城市;
                Sheng = 所在省份;
            }

        public void Deconstruct ( out string 姓 , out string 名 )
            {
                姓 = Xing;
                名 = Ming;
            }

        public void Deconstruct ( out string 姓 , out string 名 , out string 性別 )
            {
                姓 = Xing;
                名 = Ming;
                if ( XingBie == true )
                    性別 = "男";
                    性別 = "女";
            }

        public void Deconstruct ( out string 姓 , out string 名 , out string 城市 , out string 省 )
            {
                姓 = Xing;
                名 = Ming;
                城市 = ChSh;
                省 = Sheng;
            }

        public void Deconstruct ( out string 姓 , out string 名 , out string 性別 , out string 城市 , out string 省 )
            {
                姓 = Xing;
                名 = Ming;
                if ( XingBie == true )
                    性別 = "男";
                    性別 = "女";
                城市 = ChSh;
                省 = Sheng;
            }
    }

具有相同數量參數的多個 Deconstruct 方法是不明確的。在定義 Deconstruct 方法時,必須小心使用不同數量的參數或 “元數”。在重載解析過程中,不能區分具有相同數量參數的 Deconstruct 方法。

使用棄元的用户定義類型

就像使用元組一樣,可使用棄元來忽略 Deconstruct 方法返回的選定項。 名為 “_” 的變量表示棄元。單個解構操作可以包含多個棄元。

以下示例將 Ren 對象解構為四個字符串(名字、姓氏、城市和省(市、自治區)),但丟棄了姓氏和省。

var ( _ , 名 , 城市 , _ ) = r;
Console . WriteLine ( $"你好,來自 {城市} 的 {名}!" );
//      你好,來自 武漢 的 小蓮!

擴展 Deconstruct 方法

如果沒有創建 class、struct 或 interface,仍可通過實現一個或多個 Deconstruct 擴展方法來析構該類型的對象,以返回所需值。

以下示例為 System . Reflection . PropertyInfo 類定義了兩個擴展的 Deconstruct 方法。第一個返回一組值,指示屬性的特徵。第二個方法指示屬性的可訪問性。布爾值指示屬性是否具有單獨的 get 和 set 訪問器或不同的可訪問性。如果只有一個訪問器,或者 get 和 set 訪問器具有相同的可訪問性,則 access 變量指示整個屬性的可訪問性。否則,get 和 set 訪問器的可訪問性由 getAccess 和 setAccess 變量指示。

public static class Lei擴展的反射
    {
    public static void Deconstruct ( this PropertyInfo 屬性信息 , out bool Ber靜態 , out bool Ber只讀 , out bool Ber索引 , out Type 屬性類型 )
        {
            var HQ = 屬性信息 . GetMethod;
            Ber只讀 = !屬性信息 . CanWrite;
            Ber靜態 = HQ . IsStatic;
            Ber索引 = 屬性信息 . GetIndexParameters ( ) . Length > 0;
            屬性類型 = 屬性信息 . PropertyType;
        }

    public static void Deconstruct ( this PropertyInfo 屬性信息 , out bool Ber擁有get和set , out bool Ber同樣的訪問級別 , out string? Z訪問級別 , out string? get級別 , out string? set級別 )
        {
            Ber擁有get和set = Ber同樣的訪問級別 = false;
            string? getAccessTemp = null;
            string? setAccessTemp = null;

            MethodInfo? getter = null;
            if ( 屬性信息 . CanRead )
                getter = 屬性信息 . GetMethod;

            MethodInfo? setter = null;
            if ( 屬性信息 . CanWrite )
                setter = 屬性信息 . SetMethod;

            if ( setter != null && getter != null )
                Ber擁有get和set = true;

            if ( getter != null )
                {
                    if ( getter . IsPublic )
                        getAccessTemp = "public(公共)";
                    else if ( getter . IsPrivate )
                        getAccessTemp = "private(私有)";
                    else if ( getter . IsAssembly )
                        getAccessTemp = "internal(內部)";
                    else if ( getter . IsFamily )
                        getAccessTemp = "protected(受保護)";
                    else if ( getter . IsFamilyOrAssembly )
                        getAccessTemp = "protected internal(內部受保護)";
        }

        if ( setter != null )
            {
                if ( setter . IsPublic )
                    setAccessTemp = "public(公共)";
                else if ( setter . IsPrivate )
                    setAccessTemp = "private(私有)";
                else if ( setter . IsAssembly )
                    setAccessTemp = "internal(內部)";
                else if ( setter . IsFamily )
                    setAccessTemp = "protected(受保護)";
                else if ( setter . IsFamilyOrAssembly )
                    setAccessTemp = "protected internal(內部受保護)";
        }

    // get 和 set 的可訪問性是否相同?
    if ( setAccessTemp == getAccessTemp )
        {
            Ber同樣的訪問級別 = true;
            Z訪問級別 = getAccessTemp;
            get級別 = set級別 = String . Empty;
        }
    else
        {
            Z訪問級別 = null;
            get級別 = getAccessTemp;
            set級別 = setAccessTemp;
        }
    }
}

namespace C__的模式匹配_2
{
    internal class Program
    {
        static void Main ( string [ ] args )
            {
            Type LX = typeof ( DateTime );
            PropertyInfo? SHX = LX . GetProperty ( "Now" );
            var ( isStatic , isRO , isIndexed , propType ) = SHX;
            Console . WriteLine ( $"\n{LX . FullName} . {SHX . Name} 屬性:" );
            Console . WriteLine ( $"   屬性類型:{propType . Name}" );
            Console . WriteLine ( $"   靜態:    {isStatic}" );
            Console . WriteLine ( $"   只讀:    {isRO}" );
            Console . WriteLine ( $"   索引:    {isIndexed}" );

            Type listType = typeof ( List < > );
            SHX = listType . GetProperty ( "Item" , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . Static );
            var ( hasGetAndSet , sameAccess , accessibility , getAccessibility , setAccessibility ) = SHX;
            Console . Write ( $"\n{listType . FullName} . {SHX . Name}屬性的可訪問性:" );

            if ( !hasGetAndSet | sameAccess )
                {
                Console . WriteLine ( accessibility );
                }
            else
                {
                Console . WriteLine ( $"\n   get 訪問權限:{getAccessibility}" );
                Console . WriteLine ( $"   set 訪問權限:{setAccessibility}" );
                }
            }

系統類型的擴展方法

為了方便起見,某些系統類型提供 Deconstruct 方法。 例如,System . Collections . Generic . KeyValuePair < TKey , TValue > 類型提供此功能。循環訪問 System . Collections . Generic . Dictionary < TKey , TValue > 時,每個元素都是 KeyValuePair < TKey , TValue >,並且可以析構。請考慮以下示例:

Dictionary < string , int > 快照提交圖 = new ( StringComparer . OrdinalIgnoreCase )
    {
        ["https://github.com/dotnet/docs"] = 16_465,
        ["https://github.com/dotnet/runtime"] = 114_223,
        ["https://github.com/dotnet/installer"] = 22_436,
        ["https://github.com/dotnet/roslyn"] = 79_484,
        ["https://github.com/dotnet/aspnetcore"] = 48_386
    };

foreach ( var ( CK , TJ ) in 快照提交圖 )
    {
        Console . WriteLine ( $"截至 2021 年 11 月 10 日,該 {CK} 倉庫共有 {TJ:N0} 次提交。" );
    }

record 類型

使用兩個或多個位置參數聲明 record 類型時,編譯器將為 Deconstruct 聲明中的每個位置參數創建一個帶有 out 參數的 record 方法。

Add a new Comments

Some HTML is okay.