本文へスキップ
C# に Union 型がやってきた — Result 型で安全・簡潔にエラーを扱うのアイキャッチ画像
C# Tips

C# に Union 型がやってきた — Result 型で安全・簡潔にエラーを扱う

公開: 更新: 約8分で読めます

F# や TypeScript を触ったことがある開発者なら、一度は「C# にも 判別共用体(union 型)があれば」と思ったはずです。1つの値が「Windows か Linux か MacOS のいずれか」といった、複数の型のどれか1つを取ることを、型安全に表現する仕組みです。長らく C# には無く、多くの現場が object やクラス階層、あるいは独自の Result 型で代用してきました。

その union 型が、この秋リリース予定の .NET 11 のプレビューで、ついに C# の言語機能として搭載されました。本記事では、Andrew Lock 氏の解説記事と公式プロポーザルをもとに、新しい union キーワードの使い方、コンパイラが裏で何を生成するのか、そして現時点の制約までを整理します。数値や仕様はプレビュー時点のもので、正式版までに変わり得る点は先に断っておきます。

Union 型(判別共用体)とは何か

union 型は、関数型プログラミングの世界で日常的に使われてきた道具です。代表例が、成功か失敗かを表す Result<TSuccess, TError> や、値が有るか無いかを表す Option<T> です。値が取り得る状態を型として列挙し、そのどれであるかを網羅的にパターンマッチで処理できるため、状態の取りこぼしをコンパイル時に防げます。

C# はこれまで、この表現を object への格納やクラス継承、あるいはライブラリ製の判別共用体で代用してきました。いずれも安全性や記述量に難があり、F#・TypeScript・Rust といった言語に一日の長がありました。今回の C# への搭載は、その差を言語標準で埋めるものです。海外でも記事公開と同時に反響が広がりました。

実際、記事の著者である Andrew Lock 氏自身も、実装・設計判断・自作方法までを解説した記事を X で告知しています。

union キーワードの使い方

使い方はきわめて簡潔です。まず取り得るケースの型を用意し、union キーワードで1行にまとめて宣言します。ケースには record でもクラスでも構造体でも使えます。

public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

// 3つのケースを持つ判別共用体を宣言する
public union SupportedOS(Windows, Linux, MacOS);

値を作るときは、ケースの型からそのまま代入できます。コンパイラが暗黙変換を用意するため、いちいちラップするコンストラクタ呼び出しを書かずに済みます。

SupportedOS os = new MacOS("Tahoe", 25); // 暗黙変換で代入できる

そして union 型の真価は、switch 式での網羅的な分岐にあります。すべてのケースを列挙すれば、_(discard)による受け皿を書かなくてもコンパイルが通ります。逆にケースを1つ書き忘れれば、コンパイラが警告で知らせてくれます。

string Describe(SupportedOS os) => os switch
{
    Windows w => $"Windows {w.Version}",
    Linux l   => $"{l.Distro} {l.Version}",
    MacOS m   => $"MacOS {m.Name} ({m.Version})",
};

この仕組みを使えば、冒頭で触れた関数型の定番も素直に書けます。たとえば値の有無を表す Option は、わずか数行で定義できます。

public record class None;
public union Option<T>(None, T);

Result 型で安全・簡潔にエラーを扱う

Result 型で失敗を値として扱う概念図。ParseUser が Result を返し、成功(Success/User)と失敗(Failure/Error)に分岐し、呼び出し側の switch が両方を必ず処理して正常処理とエラー処理へつなぐ流れ
Result 型で「失敗」を値として扱う概念図。関数が Result を返し、呼び出し側は switch で成功・失敗の両方を必ず処理する

union 型がとくに効くのが、エラーハンドリングです。従来の C# では、失敗を表す手段が例外・null・戻り値のフラグなどに分かれ、呼び出し側が処理を書き忘れても実行するまで気づけませんでした。Result 型は、この「失敗」を型として明示し、値そのものとして扱えるようにします。

たとえば JSON を解析する関数を、成功なら User、失敗なら Error を返す Result として定義します。失敗を例外で飛ばすのではなく、戻り値としてそのまま返すのがポイントです。

public record User(int Id, string Name);
public record Error(string Message);
public union Result<T>(T, Error);

Result<User> ParseUser(string json)
{
    if (string.IsNullOrWhiteSpace(json))
        return new Error("空の入力です");   // 失敗も“値”として返す
    // …解析に成功したら…
    return new User(1, "Aoi");
}

呼び出し側は switch 式で受け取り、成功と失敗の両方を書きます。どちらかを書き忘れればコンパイラが警告するため、処理漏れを実行前に検出できます。例外の catch 忘れや null チェック漏れといった、動かしてはじめて露呈するバグが構造的に減ります。

string message = ParseUser(input) switch
{
    User user => $"ようこそ、{user.Name} さん",
    Error err => $"読み込みに失敗しました: {err.Message}",
};

失敗を戻り値として素直に扱えるため、try-catch でフローが分断されず、処理の流れが一直線に読めます。X で言及されていた鉄道指向プログラミング(Railway Oriented Programming)も、この Result 型を土台にすれば自然に表現できます。短いコードのまま、成功・失敗の取り扱いを型と網羅チェックに任せられるのが、union 型による Result の実利です。

コンパイラは裏で何を生成するのか

union 宣言はマジックではありません。コンパイラは、IUnion インターフェイスを実装し [Union] 属性を付けた構造体を生成します。中身は、格納値を保持する object? Value プロパティと、各ケースごとのコンストラクタだけです。

[Union]
public struct SupportedOS : IUnion
{
    public object? Value { get; }
    public SupportedOS(Windows value) => Value = value;
    public SupportedOS(Linux value)   => Value = value;
    public SupportedOS(MacOS value)   => Value = value;
}

暗黙変換と switch 式でのパターンマッチを駆動しているのは、この [Union] 属性です。属性が無ければ、どちらの糖衣も効きません。裏を返せば、この形に沿えば自前の判別共用体も作れるということです。

ただし既定の実装には注意点があります。ケースの値を object? に入れる設計上、構造体(値型)のケースはボクシングされてヒープに載ります。ホットパスでこれが問題になる場合に備え、コンパイラは TryGetValue メソッドを持つ独自実装を認識し、Value プロパティの代わりに使ってボクシングを避ける経路も用意しています。性能が要る箇所では、int と bool を1つの構造体に詰めるような非ボクシング実装を手書きする余地が残されています。

X で広がった反応

日本の開発者コミュニティでも、プレビュー公開の直後から反応が上がりました。鉄道指向プログラミング(Railway Oriented Programming)のライブラリ ResultBox を公開している高丘知央氏は、さっそく union 型に対応した ResultBoxUnion を試作したと報告しています。待望の機能が、既存ライブラリの設計にどう効くかを即座に検証する動きです。

プレビューを追う .NET 系の情報アカウントでも、Preview 3 での IDE 対応の前進や、union 型を使うにはまだポリフィルが要る点などが取り上げられており、正式版に向けて機能が段階的に固まっていく様子がうかがえます。

まだプレビュー — 制約と使いどころ

実務投入の前に、現状の制約を押さえておく必要があります。union 型は .NET 11 のプレビュー段階の機能で、正式版までに構文や挙動が変わる可能性があります。試すには、プロジェクトの <LangVersion>preview</LangVersion> を有効にします。初期のプレビューや .NET 11 より前をターゲットにする場合は、UnionAttribute と IUnion を自前で用意するポリフィルが要る点にも注意が要ります。

さらに、関連する言語機能のうち、closed enums(列挙型の網羅チェック)や closed hierarchies(sealed なクラス階層の網羅チェック)、union メンバーを別の型で定義する仕組みは、この時点ではまだ揃っていません。IDE 対応も、Visual Studio Preview と VS Code の C# DevKit Insiders が先行し、JetBrains Rider は対応待ちという状況です。今すぐ本番採用するというより、設計の選択肢が1つ増えることを見据えて評価しておくのが現実的です。

まとめ

C# の union 型は、値が「複数の型のどれか1つ」であることを言語標準で型安全に表現し、網羅的なパターンマッチで扱えるようにする機能です。宣言は union キーワード1行、実体は [Union] 属性付きの構造体で、暗黙変換と switch 式の糖衣が付きます。既定ではボクシングが伴うものの、非ボクシングの手書き実装も許容されています。プレビュー段階ゆえ変更の余地はありますが、Result や Option といった堅牢なエラーハンドリングを C# で自然に書ける未来が、ようやく現実になりつつあります。

エンハンスド株式会社では、最新の C# / .NET を活かした業務システムの設計・実装と、レガシーな .NET 資産のモダナイゼーションを支援しています。新しい言語機能を「使える形」でコードベースに取り込む設計判断まで含めてご相談いただけます。レガシーシステム モダナイゼーションのページもあわせてご覧ください。

参照元

この記事をシェア

コピーしました

関連記事