[Unity] switch式によるパターンマッチの網羅性チェック
ローカル環境
Unity Editor : 6000.0.58f2
C# (.NET 9) のswitch文と課題
C#の列挙型 (enum) のswitch文
では対応するcaseがあれば実行されますが、対応するcaseがない場合は何も実行されません。
そのため、enumの列挙値が増えた際、switch文
でハンドリングされずに意図しない挙動になる恐れがあります。また、その挙動の検知は実行時にしか確認できないため、実行頻度が低いコードにおいては検知困難なケースがあります。
enum E { A, B, C }
// case E.B: にマッチするのでログが出力される
// B
Do(E.B);
// E.C に対応するcaseがないので何も出力されない
//
Do(E.C);
void Do(E e)
{
switch (e)
{
case E.A:
Console.WriteLine("A");
break;
case E.B:
Console.WriteLine("B");
break;
// E.C に対応するcase節がない
}
}
Swift (6.1.2) のswitch文
一方、iOSやmacOSなどに主に活用されるSwiftではコンパイラによる網羅性チェックが保証されているため、不足しているcaseがある場合はコンパイルエラーになります。
import Foundation
enum E {
case a
case b
case c
}
Do(e: .b);
Do(e: .c);
func Do(e: E) {
// |- error: switch must be exhaustive
// `- note: add missing case: '.c'
switch (e)
{
case .a:
print("a")
break;
case .b:
print("b")
break;
// E.c に対応するcase節がない
}
}
C# (.NET 9) のswitch式
swiftのswitch文
のようにC#でも実行前のコンパイル段階で不足しているcaseを検知して、コンパイルエラーにすることで堅牢性を向上させたいです。
いくつかアプローチはありますが、ここではswitch文
ではなくswitch式
を使うと網羅性チェックに関する警告がコンパイル段階で検出されることを活用して、caseの網羅性不足の際にコンパイルエラーを発生させます。
var e = E.B;
// [CS8509] Warning
// The 'switch' expression does not handle all possible inputs (it is not exhaustive). For example, the pattern 'E' is not covered.
var value = e switch
{
E.A => 1,
E.B => 2
// E.C に対応する処理がない
}
switch式の網羅性チェックによるコンパイルエラー
Unityにおけるコンパイルにオプションを渡せる*.rsp
ファイルを使って対象の警告 (CS8509) をコンパイルエラーにします。
これによりコンパイル段階で、IDE・Unity Editorの両方で、単純な値を返すswitch式
に関しては網羅性チェックを担保できます。
Rider | Unity |
---|---|
![]() |
![]() |
Assets/csp.rsp
Unity における .rsp(Response)ファイルの拡張子
C# コンパイラ(Roslyn)の 追加引数・設定を渡すためのファイル
主に Unity のビルドプロセスで csc.exe にオプションを注入するために使われる
# switch式で既知の値が未処理 (定義済みの列挙型の値についてのみコンパイルエラーにする)
-warnaserror+:CS8509
# switch式でnullなどの網羅漏れ
-nowarn:CS8524
処理分岐のswitch式
単純な値を返すswitch式
は実現できましたが、シンプルな処理分岐はswitch式
をそのまま使うと不便です。
Action型に格納してから実行することはできますが、実行漏れをした際にエラーにならないのでリスクがあります。
enum E { A, B, C }
Do(E.B); // B
Do(E.C); //
void Do(E e)
{
Action handle = e switch
{
E.A => () => Console.WriteLine("A"),
E.B => () => Console.WriteLine("B"),
E.C => () => { }
};
handle(); // 実行漏れのリスク
}
へルパークラスを活用して実行漏れを防ぐ
実行漏れのリスクを減らすために、switch式
だけで実行まで完了するヘルパークラス (InstantAction) を使います。
InstantActionのコンストラクタが呼び出された時点でActionが即時実行されるため、実行漏れのリスクがありません。同期、非同期の両方に対応しているため、処理内容に応じて使い分けることができます。
// 即時実行用のヘルパークラス
public sealed class InstantAction
{
private InstantAction(Action action)
{
action?.Invoke(); // コンストラクタで即時実行することで実行漏れを防ぐ
}
public static Action Noop { get; } = () => { };
public static Func<UniTask> AwaitableNoop { get; } = () => UniTask.CompletedTask;
public static Func<UniTask> AwaitableAction(Action action) => () =>
{
action.Invoke();
return UniTask.CompletedTask;
};
public static async UniTask Awaitable(Func<UniTask> asyncAction)
{
if (asyncAction != null)
await asyncAction();
}
// Implicit conversion between Action and InstantAction
public static implicit operator InstantAction(Action action) => new(action);
}
// 同期
InstantAction _ = e switch
{
E.A => () => Console.WriteLine("A"),
E.B => () => Console.WriteLine("B"),
E.C => InstantAction.Noop
}
// 非同期
await InstantAction.Awaitable(e switch
{
E.A => async () => {
await SomeAsyncOperation(); // 非同期処理の例
},
E.B => InstantAction.AwaitableAction(() => Console.WriteLine("B")),
E.C => InstantAction.AwaitableNoop
});
まとめ
*.rsp
とswitch式
を活用することで、enumの網羅性チェックをコンパイル時に保証しつつパターンマッチを実現できました。
チームで開発する場合はenumのパターンマッチを扱う際にswitch式
で記述するというコーディング規約が必要ですが、複数の開発者が特定のenumの列挙値を増やした際にコンパイルエラーになるので、不具合の早期発見に繋がります。
Discussion