
2026/04/05 11:53
**C# 15 のユニオン型(Union Types)** C# 15 では *union types*(別名「discriminated unions」や「tagged unions」)が導入され、変数が複数の異なる名前付き代替値のいずれかを保持できるようになりつつ、型安全性も維持できます。 --- ### 1. ユニオン型の定義 ```csharp public union Result<TSuccess, TError> { // 成功の場合 public readonly record struct Ok(TSuccess Value); // エラーの場合 public readonly record struct Err(TError Reason); } ``` - `Result<int, string>` は `Ok`(整数)または `Err`(文字列)のいずれかを保持できます。 - 各代替値は *record struct* であるため、イミュータブルで軽量です。 --- ### 2. ユニオンの使用 ```csharp Result<int, string> GetNumber(bool success) { return success ? new Result<int, string>.Ok(42) : new Result<int, string>.Err("failed"); } var result = GetNumber(true); result.Match( ok => Console.WriteLine($"Success: {ok.Value}"), err => Console.Error.WriteLine($"Error: {err.Reason}") ); ``` - `Match` はパターンマッチングのヘルパーで、各代替値に対してデリゲートを受け取ります。 - C# の `switch` 式でも同様に扱えます: ```csharp var message = result switch { Result<int, string>.Ok(ok) => $"Success: {ok.Value}", Result<int, string>.Err(err) => $"Error: {err.Reason}" }; ``` --- ### 3. メリット | 機能 | 重要性 | |------|--------| | **型安全** | コンパイル時にすべての代替値が処理されることを保証します。 | | **イミュータブル** | record struct により不注意な変更を防止します。 | | **パターンマッチング** | 各ケースを簡潔かつ表現力豊かに扱えます。 | | **外部ライブラリ不要** | 言語内蔵であり、`OneOf` などのサードパーティパッケージは必要ありません。 | --- ### 4. よく使われるパターン - **Result / Either** – 成功 vs エラー(`Ok/Err`、`Right/Left`)。 - **Option / Maybe** – 値あるいはなし(`Some/Nothing`)。 - **状態機械** – 状態を個別の代替値として扱う。 ```csharp public union State { public readonly record struct Idle; public readonly record struct Running(int Count); public readonly record struct Stopped(string Reason); } ``` --- ### 5. 相互運用性 ユニオンを、別々の型を期待する API に渡す際はパターンマッチングで内部値を抽出できます: ```csharp if (result is Result<int, string>.Ok(ok)) { ProcessValue(ok.Value); // ok は Result<int,string>.Ok 型です } ``` --- ### 6. 留意点 - **シリアライゼーション** – カスタム処理が必要になる場合があります。ユニオンは自動的にシリアライズ可能ではありません。 - **パフォーマンス** – 各代替値は独立した struct であるため、高スループット環境では過度な箱詰めを避けるべきです。 - **ツールサポート** – IDE はユニオン型を理解しますが、古いツールでは対応していない可能性があります。 --- ### 7. クイックリファレンス ```csharp // 宣言 public union MyUnion { public readonly record struct A(int X); public readonly record struct B(string Y); } // インスタンス生成 var a = new MyUnion.A(10); var b = new MyUnion.B("hello"); // パターンマッチング myValue.Match( a => HandleA(a.X), b => HandleB(b.Y) ); // Switch 式 string result = myValue switch { MyUnion.A(a) => $"A: {a.X}", MyUnion.B(b) => $"B: {b.Y}" }; ``` --- **結論** C# 15 のユニオン型は、外部ライブラリのボイラープレートを排除しつつ、エラー処理や状態表現などで「複数形態の値」をクリーンにかつ型安全に扱うための強力な手段です。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
**C# 15 は
キーワードを使用して union 型を導入し、最初に .NET 11 Preview 2 で利用可能になりました。ユニオンは関連のないケース型の閉じた集合を宣言します;コンパイラは除外 (union) やデフォルトブランチを必要とせずに網羅的なパターンマッチングを強制します。例えば:_public union Pet(Cat, Dog, Bird);この型は各ケースクラスから暗黙の変換が可能で、
プロパティ経由で値にアクセスできます。非 null のユニオンインスタンスに対する switch 式は網羅的です;新しいケースを追加すると欠落しているアームについてコンパイル時警告が発生します。ユニオンのデフォルト構造体値には nullValueが設定されるため、switch でValueアームを含めてその状態を処理できます。null => …
ユニオンは追加メンバーも持つことができ、例としてなどがあります。既存のライブラリはpublic union OneOrMore<T>(T, IEnumerable<T>) { public IEnumerable<T> AsEnumerable() => … }を付与し、必要なコンストラクタと[System.Runtime.CompilerServices.Union]プロパティを提供することでユニオンとして扱うことができます。Value
パフォーマンス向上のために、ユニオンは、HasValue、または値型のボクシングを避ける非ボクシングパターンを公開する場合があります。「閉じた階層」(sealed クラス)や「閉じた列挙」などの関連提案も、閉じた集合に対して同様の網羅的マッチングを実現しようとします。TryGetValue
プレビューでユニオン型を試すには、.NET 11 Preview SDK をインストールし、ターゲットをに設定し、プロジェクトファイルにnet11.0を追加します。また、早期プレビューを使用する場合は<LangVersion>preview</LangVersion>とUnionAttributeを宣言してください。IDE サポートは今後の Visual Studio Insiders ビルドで提供される予定です;ユーザーフィードバックが将来のリリース形成に役立ちます。**IUnion
本文
C# / .NET プリンシパル コンテンツ デベロッパー
C# で「ユニオン型」が頻繁に要望されており、ついに実装されました。 .NET 11 Preview 2 以降、C# 15 は
union キーワードを導入しています。このキーワードは、コンパイラが厳密に検証する「閉じた型集合」のうちの 1 つだけ を保持できる値であることを宣言します。F# の discriminated unions や他言語の類似機能をご存知なら、すぐに馴染めるでしょう。ただし C# ユニオンは C# 本来の体験を意図して設計されており、既存の型と組み合わせて使い、パターンマッチングとの親和性も高く、言語全体とシームレスに統合されます。
ユニオン型とは?
C# 15 以前では、メソッドが複数種類の値を返す必要がある場合、次のような選択肢しかありませんでした:
| 選択肢 | 説明 |
|---|---|
| 型に制約がないため任意の型が入れられる。呼び出し側は予期せぬ値に対して防御的なロジックを書く必要がある。 |
| マーカーインタフェース/抽象基底クラス | 制限された型集合を示すことはできるが、閉じた(完全)な集合とは言えない。誰でも実装・派生できるためコンパイラは完結性を保証できない。また共通の祖先が必要であり、 と のように相関しない型同士のユニオンは表現できない。 |
ユニオン型 はこれらの問題を解決します。ユニオンは 閉じた 型集合(ケースタイプ)を宣言します。ケースタイプ間で親子関係は必要なく、他の型が追加されることもありません。コンパイラはスイッチ式が exhaustive になるよう保証し、デフォルト (
_) や default ブランチなしにすべてのケースを網羅できます。さらに、従来の階層構造では表現できない設計(任意の既存型の組み合わせ)をコンパイラが検証した契約として実装します。
シンプルな宣言例
public record class Cat(string Name); public record class Dog(string Name); public record class Bird(string Name); public union Pet(Cat, Dog, Bird);
上記 1 行で
Pet は新しい型として宣言され、変数は Cat, Dog, Bird のいずれかを保持できます。コンパイラは各ケースタイプからの暗黙的な変換を提供します。
Pet pet = new Dog("Rex"); Console.WriteLine(pet.Value); // Dog { Name = Rex } Pet pet2 = new Cat("Whiskers"); Console.WriteLine(pet2.Value); // Cat { Name = Whiskers }
ケースタイプに含まれない型を代入するとコンパイルエラーになります。
ユニオン型のインスタンスが null でない とき、コンパイラはケース集合全体を把握しているため、すべてを網羅したスイッチ式は exhaustive です。
string name = pet switch { Dog d => d.Name, Cat c => c.Name, Bird b => b.Name, };
いずれかのケースが nullable(例:
int?, Bird?)の場合、すべてのスイッチ式は null ブランチを必要とします。ケースタイプを 4 種類に増やした場合、未網羅のスイッチにはコンパイル警告が発生します。これは「ビルド時に欠落ケースを検出」できるというユニオン型の主要な価値です。
パターンは
プロパティ に対して適用され、ユニオン構造体自体ではありません。この「アンラップ」は自動で行われます。例外として Value
var と _ はユニオン全体に対して適用でき、全体を取得または無視できます。
null パターンは
Value が null かどうかを判定します。デフォルト値のユニオン構造体は Value が null の状態です。
Pet pet = default; var description = pet switch { Dog d => d.Name, Cat c => c.Name, Bird b => b.Name, null => "no pet", }; // description は "no pet"
実務で使えるユニオン型のシナリオ
OneOrMore<T>
― 単一値またはコレクション
OneOrMore<T>API が「単一アイテム」か「複数アイテム」のいずれかを受け取る場合、ユニオンに本体(body)を持たせてヘルパーメンバーを追加できます。
OneOrMore<T> の宣言は AsEnumerable() メソッドを直接ユニオンの本体で定義しています。
public union OneOrMore<T>(T, IEnumerable<T>) { public IEnumerable<T> AsEnumerable() => Value switch { T single => [single], IEnumerable<T> multiple => multiple, null => [] }; }
AsEnumerable() は Value が null のケースも処理する必要があります。呼び出し側は便利な形式を渡せば、AsEnumerable() がそれを正規化します。
OneOrMore<string> tags = "dotnet"; OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" }; foreach (var tag in tags.AsEnumerable()) Console.Write($"[{tag}] "); // [dotnet] foreach (var tag in moreTags.AsEnumerable()) Console.Write($"[{tag}] "); // [csharp] [unions] [preview]
既存ライブラリ向けのカスタムユニオン
ユニオン宣言は構文上の短縮形です。コンパイラは各ケースタイプ用のコンストラクタと
object? 型の Value プロパティを持つ構造体を生成します。コンストラクタにより、任意のケースタイプからユニオン型への暗黙的変換が可能です。ユニオンインスタンスは常に内部で単一の object? 参照として保持し、値型はボックス化されます。
既存のコミュニティライブラリで「ユニオン風」型を使用している場合も、構文を変えずに恩恵を受けられます。
[System.Runtime.CompilerServices.Union] 属性が付与されたクラスまたは構造体は、以下の基本パターン(1 つ以上の public single‑parameter コンストラクタと public Value プロパティ)に従っていればユニオン型として認識されます。
性能重視で値型をケースタイプに含める場合、ライブラリは非ボックス化アクセスパターン(
HasValue プロパティや TryGetValue メソッド)の実装も可能です。これによりコンパイラはパターンマッチングをボックス化せずに行えます。
カスタムユニオン型の作成と非ボックス化アクセスパターンについては、ユニオン型言語リファレンスをご覧ください。
関連提案
ユニオン型は「閉じた型集合」を持つタイプを提供します。型階層や列挙体に対しても同様の機能を実現する提案が 2 つあります:
- Closed hierarchies ―
修飾子をクラスに付与すると、定義アセンブリ外で派生クラスを宣言できなくなります。closed - Closed enums ― 列挙体を閉じることで、宣言されたメンバー以外の値は作れません。
これら 3 つの機能により、C# は包括的な exhaustive マッチングストーリーを構築します:
| 機能 | exhaustive なマッチ対象 |
|---|---|
| ユニオン型 | 閉じた型集合 |
| Closed hierarchies | シールドクラス階層 |
| Closed enums | 固定列挙値 |
ユニオン型は現在プレビューで利用可能です。評価の際には、上記ロードマップを考慮してください。これら提案はアクティブですが、まだリリースに確約されているわけではありません。
使ってみる
ユニオン型は .NET 11 Preview 2 以降で利用可能です。
- .NET 11 Preview SDK をインストールします。
をターゲットにしたプロジェクトを作成または更新します。net11.0- プロジェクトファイルに
を設定します。<LangVersion>preview</LangVersion>
Visual Studio では次の Visual Studio Insiders ビルド(最新の C# DevKit Insiders に含まれます)で IDE サポートが利用できます。
初期プレビュー:ランタイム型を自分で宣言
.NET 11 Preview 2 では
UnionAttribute と IUnion はまだランタイムに組み込まれていません。プロジェクト内で宣言しておけば、後続のプレビューでこれらが含まれるようになります。
namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } }
これでユニオン型を宣言・使用できます。
public record class Cat(string Name); public record class Dog(string Name); public union Pet(Cat, Dog); Pet pet = new Cat("Whiskers"); Console.WriteLine(pet switch { Cat c => $"Cat: {c.Name}", Dog d => $"Dog: {d.Name}", });
完全仕様に含まれる一部機能(ユニオンメンバープロバイダーなど)はまだ実装されていません。今後のプレビューで追加予定です。
フィードバックを共有
ユニオン型は現在プレビュー段階です。直接的に最終設計に影響しますので、ぜひプロジェクトで試し、エッジケースや課題点を教えてください。
著者
C# / .NET プリンシパル コンテンツ デベロッパー
Bill Wagner は https://docs.microsoft.com/dotnet/csharp のドキュメントを書いています。彼のチームは docs.microsoft.com 上の .NET コンテンツ全般を担当しています。また、C# 標準化委員会にも所属しています。