Start Debugging

修正: C# 15 の union 型に対して網羅的な switch で CS8509 または CS0161 が出る場合

.Value ではなく union の値そのものに対して switch し、switch 式を使うか、switch 文には case null を追加します。switch 式は警告で済む null の網羅が、switch 文では必須になります。

.Value プロパティではなく union の値そのものに対して switch し、switch 式を優先してください。値を返すメソッドで switch 文が必要な場合は、case null: の分岐を追加するか、switch の後に throw を置きます。switch 文では、default の union が持つ null の Value まで網羅して初めて、コンパイラーが union に対する switch を完全と見なすためです。以下の挙動はすべて .NET 11 RC1 SDK (11.0.100-rc.1.26425.128、C# 15、LangVersion の上書きは不要) で確認したものです。

エラーの状況

union を宣言し、すべてのケース型に対応したのに、コンパイラーは網羅していないと言ってきます。

warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '_' is not covered.
error CS0161: 'Pets.Describe(Pet)': not all code paths return a value
error CS0165: Use of unassigned local variable 's'
warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered.
error CS8780: A variable may not be declared within a 'not' or an 'or' pattern or a union matching involving matching against either the instance, or its underlying value.

これら 5 つは、同じ誤解から生じる別々の症状です。C# 15 における union の網羅性は union マッチング の性質であり、union マッチングは特定の条件下でのみ働きます。その条件から外れると、通常の object に対するパターンマッチングに戻り、2 つの型パターンでは決して網羅的になりません。

コンパイラーが switch を網羅的と見なさない理由

C# 15 の union 仕様は 1 行で述べています。union 型はそのケース型によって “exhausted” (尽くされた) と見なされるため、switch 式は union のすべてのケース型を処理していれば網羅的です。それ以外はすべて細則から導かれます。

  1. 入力は union の値でなければなりません。 union マッチングが行われるのは、“when the input value of a pattern is of a union type or of a nullable of a union type” の場合だけです。pet.Value (型は object?) や、object にボックス化された union に対して switch すると、コンパイラーはケースの一覧を得られず、_ を要求します。
  2. switch 文は switch 式より厳格です。 未処理の null がある switch 式は、CS8655 の警告付きでコンパイルされます。一方、確実な代入や戻りパスの解析に使われる switch 文は、null も網羅されている場合にのみ完全と見なされます。そのため switch の終端に到達可能なままとなり、CS0161 または CS0165 が出ます。
  3. union の Value は常に null になり得ます。 public union Pet(Cat, Dog) は、public object? Value { get; } を持つ構造体に展開されます。default(Pet) は null を保持し、new Pet((Cat)null!) も同様です。仕様は整合性 (well-formedness)の項でこれを挙げており、Value は “null or a value of a case type” です。
  4. 型パラメーターは展開されません。 union Result<T>(T, Exception) のケース型 T には、デザインネーション付きのパターンマッチングは使えません。T v が union インスタンスと中身のどちらを検査すべきか、コンパイラーが証明できないためです。これが CS8780 です。

最小の再現コード

// .NET 11 RC1 SDK 11.0.100-rc.1.26425.128, C# 15, <Nullable>enable</Nullable>
public record Cat(string Name);
public record Dog(string Name);
public union Pet(Cat, Dog);
public union MaybePet(Cat?, Dog);
public union Result<T>(T, Exception);

static class Pets
{
    // OK: no diagnostics
    static string A(Pet p) => p switch { Cat c => c.Name, Dog d => d.Name };

    // CS8509: pattern '_' is not covered
    static string B(Pet p) => p.Value switch { Cat c => c.Name, Dog d => d.Name };

    // CS0161: not all code paths return a value
    static string Describe(Pet p)
    {
        switch (p)
        {
            case Cat c: return c.Name;
            case Dog d: return d.Name;
        }
    }

    // CS8655: pattern 'null' is not covered (Cat? is a nullable case type)
    static string D(MaybePet p) => p switch { Cat c => c.Name, Dog d => d.Name };

    // CS8509: a union boxed into object is just an object
    static string E(object o) => o switch { Cat c => c.Name, Dog d => d.Name };

    // CS8655: Nullable<Pet> can be null
    static string F(Pet? p) => p switch { Cat c => c.Name, Dog d => d.Name };

    // CS8780 on 'TV v'
    static string I<TV>(Result<TV> r) => r switch { TV v => v!.ToString()!, Exception e => e.Message };
}

メソッド A が基準です。union の値をそのまま switch 式に渡し、すべてのケース型に分岐があり、コンパイラーは何も言いません。他のメソッドはすべて、上の 4 つのルールのいずれかに違反しています。

修正の詳細

順番に確認してください。最初の項目で、実際の報告の大半は解決します。

1. .Value ではなく union に対してマッチングする

Value は object? として宣言されています。参照した時点で、union 型とそのケース一覧は失われます。union に対するパターンマッチングは、すでに中身を展開してくれます。p is Cat c は p.Value に対する検査としてコンパイルされるので、自分で .Value を持ち出す理由はありません。

// .NET 11 RC1, C# 15
// Before: CS8509
static string Name(Pet p) => p.Value switch { Cat c => c.Name, Dog d => d.Name };

// After: exhaustive, no default arm
static string Name(Pet p) => p switch { Cat c => c.Name, Dog d => d.Name };

object、IUnion、またはジェネリックな T パラメーターを経由する union にも同じことが当てはまります。仕様の解決済みの質問によれば、型パラメーターは union 型に制約されていても、決して union 型にはなりません。RC1 では、static string L<TU>(TU u) where TU : struct, IUnion => u switch { Cat c => ..., Dog d => ... } は網羅性の判定にすら到達せず、CS8121 “An expression of type ‘TU’ cannot be handled by a pattern of type ‘Cat’” で失敗します。シグネチャには具体的な union 型を使ってください。

部分的な例外が 1 つあります。RC1 では、Value に対するプロパティパターンは union の知識を引き継ぎます。r switch { { Value: TV v } => ..., { Value: Exception e } => ... } は CS8509 ではなく CS8655 だけを出しました。ただし仕様では “Should direct Value property matching follow Union rules?” が未解決の問題として残っているため、これを前提にしないでください。

2. switch 式を優先し、switch 文には case null を追加する

これが CS0161 / CS0165 のケースで、最も意外なものです。同じ分岐が式では動くからです。RC1 で 3 つのバリエーションを確認しました。

// .NET 11 RC1, C# 15
public union Pet(Cat, Dog);
public closed class Shape;
public sealed class Sq : Shape;
public sealed class Ci : Shape;

// error CS0161
static string S1(Pet p) { switch (p) { case Cat c: return c.Name; case Dog d: return d.Name; } }

// compiles
static string S2(Pet p) { switch (p) { case Cat c: return c.Name; case Dog d: return d.Name; case null: return "none"; } }

// compiles: bool is exhaustive for statements
static int S3(bool b) { switch (b) { case true: return 1; case false: return 0; } }

// error CS0161: closed hierarchies behave like unions here
static int S4(Shape s) { switch (s) { case Sq: return 1; case Ci: return 0; } }

S3 は、コンパイラーが switch 文でも網羅性を判定していることを示しています。コンパイラーが行わないのは、網羅されていない null を無視することです。switch 式は、欠けている null を null 許容の警告に格下げします (すべてのケース型が非 null の union であれば、何も出ません)。switch 文は、到達可能性の判定に完全な網羅性の答えを使い、その答えには null が含まれます。#nullable disable を有効にしても変わりません。RC1 のコンパイラーは、union と closed クラスの両方で CS0161 を報告します。

きれいな修正が 3 つあります。優先度の高い順に示します。

// .NET 11 RC1, C# 15
using System.Diagnostics;

// a) Use an expression. Most switch statements that only return can be one.
static string Describe(Pet p) => p switch { Cat c => c.Name, Dog d => d.Name };

// b) Cover null explicitly. Use this when a default union is a legitimate state.
static string Describe2(Pet p)
{
    switch (p)
    {
        case Cat c: return c.Name;
        case Dog d: return d.Name;
        case null: return "no pet";
    }
}

// c) Keep the statement and declare the end unreachable.
static string Describe3(Pet p)
{
    switch (p)
    {
        case Cat c: return c.Name;
        case Dog d: return d.Name;
    }
    throw new UnreachableException();
}

修正として default: を使うのは避けてください。コンパイルは通りますが、union に次に追加するケース型まで飲み込んでしまい、本来残しておきたい唯一の診断が失われます。

CS0165 の変種は、同じ問題が代入の形で現れたものです。string s; switch (p) { case Cat c: s = ...; break; case Dog d: s = ...; break; } return s; は、s が未代入のまま switch の終端に到達できるため失敗します。同じ 3 つの修正が当てはまります。

3. 型が null になり得ると言っている箇所では null を処理する

CS8655 は、コンパイラーが正しいケースです。次の 2 つの状況で出ます。

null の分岐を追加してください。union では、null は null のインスタンスと、Value が null の union の両方にマッチします。

// .NET 11 RC1, C# 15
static string D(MaybePet p) => p switch
{
    Cat c => c.Name,
    Dog d => d.Name,
    null => "none",
};

null 許容のケース型が意図しないものだった場合は、代わりに union 宣言から ? を取り除いてください。

4. 型パラメーターのケース型ではデザインネーションを外す

union Result<T>(T, Exception) では、制約にかかわらず T v の分岐が CS8780 で失敗します。制約なし、where T : notnull、where T : class、where T : struct を試しましたが、RC1 では 4 つすべてで CS8780 が報告されます。うまくいくのは、変数なしの型パターンを使う方法か、ジェネリックなケースを var で最後に受ける方法です。

// .NET 11 RC1, C# 15
public union Result<T>(T, Exception);

// Exhaustive and clean with notnull: prints "v:5" for new Result<int>(5)
static string W4<TV>(Result<TV> r) where TV : notnull =>
    r switch { TV => "v:" + r.Value, Exception => "e" };

// Match the concrete case first, let var take the rest
static string W3<TV>(Result<TV> r) =>
    r switch { Exception e => e.Message, var other => other.Value!.ToString()! };

notnull 制約がない場合、TV => の形もコンパイルは通りますが、TV が null 許容型になり得るため CS8655 が加わります。なお、var は union を展開しません。other は中身ではなく Result<TV> であり、だからこそそこから .Value を読み取っています。

実行時の側面: default の union は例外を投げる

コンパイラーを黙らせることは、すべての値を処理することと同じではありません。ケース型がすべて非 null の union は、default に対して switch しても警告が出ません。

// .NET 11 RC1, C# 15
public union Result2(int, Exception);

static string Show(Result2 r) => r switch { int v => v.ToString(), Exception e => e.Message };

Show(default); // System.Runtime.CompilerServices.SwitchExpressionException at runtime

仕様の解決済みの質問 “Default nullable state of Value property” は、default(U).Value が null であるのに対し、null 許容解析はケース型しか見ていないことを認めています。実際には、default の union は、代入されなかったフィールド、配列の要素、ジェネリックコードでの default の戻り値、union が設定されなかったデシリアライズ済みのペイロードとして現れます。これらの経路が存在するなら、コンパイラーが求めていなくても null の分岐を追加するか、境界で検証してください。同じ考え方は、コンストラクターで設定されない非 null 許容プロパティにも当てはまります。アノテーションは意図を表すものであり、実行時の保証ではありません。

落とし穴と似たエラー

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る