共用方式為


C# 9.0 中的模式比對變更

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異是記錄在的 語言設計會議(LDM)相關紀要中的。

您可以在 規格的文章中了解將功能規格納入 C# 語言標準的流程。

我們正在考慮少數幾項 C# 9.0 模式比對的增強功能,其具有自然協同作用,能有效解決許多常見的程式設計問題:

括弧模式

括弧模式可讓程式設計人員將括弧放在任何模式周圍。 這並不適用於 C# 8.0 中的現有模式,但是新的模式組合器引進了程式設計人員可能想要覆寫的優先順序。

primary_pattern
    : parenthesized_pattern
    | // all of the existing forms
    ;
parenthesized_pattern
    : '(' pattern ')'
    ;

類型模式

我們允許類型做為模式:

primary_pattern
    : type-pattern
    | // all of the existing forms
    ;
type_pattern
    : type
    ;

這個修正將現有的 is-type-expression 轉為 is-pattern-expression,在這個模式中是一個 類型模式,不過我們不會更改編譯程式所產生的語法樹。

其中一個微妙的實作問題是,此文法模棱兩可。 像 a.b 這樣的字串,可以在類型上下文中解析為限定名稱,或在運算式上下文中解析為點狀表達式。 編譯程式已經能夠將限定名稱視為點表達式,以處理類似 e is Color.Red之類的專案。 編譯程式的語意分析會進一步擴充為能夠將常數模式(例如點表達式)系結為類型,以便將它視為系結類型模式,以支援此建構。

在此變更之後,您將能夠撰寫

void M(object o1, object o2)
{
    var t = (o1, o2);
    if (t is (int, string)) {} // test if o1 is an int and o2 is a string
    switch (o1) {
        case int: break; // test if o1 is an int
        case System.String: break; // test if o1 is a string
    }
}

關係型模式

關係模式可讓程式設計人員表示輸入值必須滿足與常數值相較之下的關係型條件約束:

    public static LifeStage LifeStageAtAge(int age) => age switch
    {
        < 0 =>  LifeStage.Prenatal,
        < 2 =>  LifeStage.Infant,
        < 4 =>  LifeStage.Toddler,
        < 6 =>  LifeStage.EarlyChild,
        < 12 => LifeStage.MiddleChild,
        < 20 => LifeStage.Adolescent,
        < 40 => LifeStage.EarlyAdult,
        < 65 => LifeStage.MiddleAdult,
        _ =>    LifeStage.LateAdult,
    };

關係型模式支持關係運算子 <<=>>=,適用於支持這些運算子的所有內建型別,這些型別在表達式中有兩個相同類型的操作數。 具體來說,我們支援 sbytebyteshortushortintuintlongulongcharfloatdoubledecimalnintnuint的所有關係型模式。

primary_pattern
    : relational_pattern
    ;
relational_pattern
    : '<' relational_expression
    | '<=' relational_expression
    | '>' relational_expression
    | '>=' relational_expression
    ;

該表達式需要被評估為一個常量值。 如果常數值 double.NaNfloat.NaN,就會發生錯誤。 如果表達式是 Null 常數,則為錯誤。

當輸入是一種類型,其中定義了適合的內建二進位關係運算符,其適用於輸入做為其左操作數,而指定常數做為其右操作數,該運算子的評估會被視為關係型模式的意義。 否則,我們會使用明確的可為 Null 或 Unboxing 轉換,將輸入轉換成表達式的類型。 如果沒有任何這類轉換存在,則為編譯時期錯誤。 如果轉換失敗,模式會被視為不相符。 如果轉換成功,則模式比對作業的結果就是評估表達式 e OP v,其中 e 是轉換的輸入,OP 是關係運算符,v 是常數表達式。

模式結合器

模式 結合子 允許使用 來比對兩個不同模式的兩者(這可透過重複使用 來擴充至任意數目的模式)、使用 的兩種不同模式之一(ditto),或使用 來 模式的 否定。

組合器的常見用法將是成語

if (e is not null) ...

這個模式比目前的習慣用語 e is object更容易閱讀,這更加清楚地表示正在檢查非空值。

andor 組合器將會對測試值範圍很有用

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

此範例說明 and 的剖析優先順序會比 or更緊密地系結。 程式設計人員可以使用 括弧模式,讓優先順序明確:

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

與所有模式一樣,這些組合器可用於需要模式的任何上下文中,包括巢狀模式、is-模式表達式switch-模式表達式,以及 switch 語句 case 標籤的模式。

pattern
    : disjunctive_pattern
    ;
disjunctive_pattern
    : disjunctive_pattern 'or' conjunctive_pattern
    | conjunctive_pattern
    ;
conjunctive_pattern
    : conjunctive_pattern 'and' negated_pattern
    | negated_pattern
    ;
negated_pattern
    : 'not' negated_pattern
    | primary_pattern
    ;
primary_pattern
    : // all of the patterns forms previously defined
    ;

將其更改為 6.2.5 語法歧義

由於引入 類型模式,泛型類型可能會出現在標記 =>之前。 因此,我們會將 => 新增至 §6.2.5 文法歧義 中列出的標記集,以允許釐清用於開始類型參數清單的 <。 請參閱 https://github.com/dotnet/roslyn/issues/47614

針對建議的變更提出問題

關係運算子的語法

andornot 某種內容關鍵詞嗎? 如果是這樣,是否存在破壞性變更(例如,與其在 宣告模式中作為指示項的用法相比)。

關聯運算子的語意(例如類型)

我們預期支援使用關係運算符在表達式中可以比較的所有基本類型。 簡單案例中的意義很清楚

bool IsValidPercentage(int x) => x is >= 0 and <= 100;

但是,當輸入不是這類基本類型時,我們會嘗試將它轉換成何種類型?

bool IsValidPercentage(object x) => x is >= 0 and <= 100;

我們建議在輸入類型已經是可比較的基本類型的情況下,應使用該類型進行比較。 不過,當輸入不是可比較的基本類型時,我們會將關係運算視為包含對關係運算右側常數類型的隱式類型測試。 如果程式設計人員想要支援多個輸入類型,則必須明確完成:

bool IsValidPercentage(object x) => x is
    >= 0 and <= 100 or    // integer tests
    >= 0F and <= 100F or  // float tests
    >= 0D and <= 100D;    // double tests

結果:在關係表達式中,確實包含對其右側常數類型的隱含型別測試。

從左向右流動的類型資訊 and

建議當您撰寫 and 結合器時,左側所學到的有關頂層類型的類型資訊可能會轉移到右邊。 例如

bool isSmallByte(object o) => o is byte and < 100;

在這裡,輸入類型 到第二個模式,會透過 and左邊的 型別縮小 需求。 我們會定義所有模式的類型縮小語意,如下所示。 模式 P縮小類型 定義如下:

  1. 如果 P 是類型模式,則 縮小類型 是類型模式的類型。
  2. 如果 P 是宣告模式,則 縮小類型 是宣告模式的類型。
  3. 如果 P 是提供明確類型的遞歸模式,則 縮小類型 為該類型。
  4. 如果 P是通過ITuple的規則進行匹配的,那麼 縮小類型 是類型 System.Runtime.CompilerServices.ITuple
  5. 如果 P 是常數模式,其中常數不是 null 常數,而且表達式沒有 常數表達式轉換輸入類型,則 窄型別 是常數的類型。
  6. 如果 P 是關係型模式,其中常數表達式沒有 常數表達式轉換輸入類型,則 縮小類型 是常數的類型。
  7. 如果 Por 模式,窄型別 是子模式 窄型別 的常見類型,如果這類常見類型存在。 基於這個目的,一般類型演算法只會考慮識別、Boxing 和隱含參考轉換,並且會考慮一系列 or 模式的所有子模式(忽略括弧模式)。
  8. 如果 Pand 模式,則 窄化型別 是右側模式的 窄化型別。 此外,左圖樣的窄型別 是右圖樣的輸入類型
  9. 否則,P縮小類型P的輸入類型。

結果:已實作上述縮小語意。

變數定義和明確指派

新增 ornot 模式,會產生一些關於模式變數和明確指派的有趣新問題。 由於變數通常最多可以宣告一次,所以在 or 模式的一端宣告的任何模式變數似乎都不會在模式相符時明確指派。 同樣地,在 not 模式中宣告的變數,在模式相符時不一定會被指派。 最簡單的解決方式是禁止在這些內容中宣告模式變數。 不過,這可能會太嚴格。 還有其他方法需要考慮。

值得考慮的一個案例是

if (e is not int i) return;
M(i); // is i definitely assigned here?

這目前無法運作,因為對於 is-pattern-expression而言,模式變數會被視為 絕對指派 只有在 為 pattern-expression 為 true 時才會被指派(「絕對指派為 true」)。

支援這項功能會比新增否定條件 if 語句的支援更簡單(從程式設計人員的觀點來看)。 即使我們新增這類支援,程式設計人員也會想知道上述代碼段為何無法運作。 另一方面,switch 中的相同情境比較沒有意義,因為在程式中不存在這樣一個點,在 false 的情況下被明確指派會有意義。 我們會在 is-pattern-expression 中允許這種情況,但在允許模式的其他情境下卻不允許嗎? 這似乎不規則。

與此相關的是 分離模式中明確指派的問題。

if (e is 0 or int i)
{
    M(i); // is i definitely assigned here?
}

當輸入值不是零時,我們只會預期會確定指派 i。 但是,由於我們不知道輸入在區塊內部是否為零,因此 i 未被明確指派。 不過如果我們允許以不同的互斥模式來宣告 i,該怎麼辦?

if ((e1, e2) is (0, int i) or (int i, 0))
{
    M(i);
}

在這裡,變數 i 確定會在區塊內賦值,並在找到為零的元素時,從元組的其他元素中取得其值。

它也建議允許變數在案例區塊的每個案例中定義(乘法):

    case (0, int x):
    case (int x, 0):
        Console.WriteLine(x);

若要進行這項工作,我們必須仔細定義允許這類多個定義的位置,並在哪些情況下將這類變數視為明確指派。

如果我們選擇推遲這項工作到後來(我建議這樣做),那麼我們可以在 C# 9 中說

  • notor底下,可能無法宣告模式變數。

然後,我們將有時間積累一些經驗,這將提供對於稍後放寬相關規定可能價值的深入瞭解。

結果:模式變數無法在 notor 模式下宣告。

診斷、包含和全面性

這些新的範式引入了許多可以診斷程序設計人員錯誤的新機會。 我們需要決定我們將診斷的錯誤種類,以及如何進行。 以下是一些範例:

case >= 0 and <= 100D:

此案例永遠無法比對(因為輸入不能同時是 intdouble)。 當我們偵測到永遠無法符合的案例時,可能會產生錯誤,但其措辭(「switch case 已由先前案例處理」和「模式已由前一個 switch 表達式的部分處理」)在新情況中可能會產生誤導。 我們可能必須修改措辭,只說模式永遠不會符合輸入。

case 1 and 2:

同樣地,這會是錯誤,因為值不能同時 12

case 1 or 2 or 3 or 1:

此案例可能相符,但結尾的 or 1 不會對模式新增任何意義。 我建議,我們應該以每當複合模式的某些連接詞或分離詞未定義模式變數或影響匹配值的集合為目標來產生錯誤。

case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;

在這裡,0 or 1 or 在第二個案例中並不新增任何內容,因為第一個案例已經處理這些值。 這也應該被視作一個錯誤。

byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };

像這樣的 switch 表達式應該視為 完整(它會處理所有可能的輸入值)。

在 C# 8.0 中,只有當 switch 表達式中的類型為 byte 的輸入具備一個最終臂且其模式可以匹配所有情況(例如 discard-patternvar-pattern),才被視為詳盡。 即使是包含所有不同 byte 值的分支的 switch 表達式,在 C# 8 中也不被視為是完整的。 為了正確處理關係型模式的詳盡性,我們也必須處理此案例。 在技術上,這會是重大變更,但沒有任何使用者可能會注意到。