
2026/05/22 21:28
C# がようやくユニオン型(Union Types)に対応しました。.NET (OK, C#) にて
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
C# 15 および .NET 11 では、Windows、Linux、MacOS など複数の潜在的に無関係なレコード型を、基底クラスやタグ付き enum に依存せずに単一の変数内で表現できるようにする画期的な機能「union types」(新しい
union キーワードを使用)が導入されました。デフォルトでは、union types は IUnion インターフェースを実装し、単一の object? Value プロパティを持っています。switch 式はすべての case を網羅している場合、明示的な discard case を必要とせずに union の各ケースを自動的に分解(deconstruct)できます。この利便性には代償があり、デフォルトの実装では値型が object? 中に boxing され、ホットパスでヒープ割当を引き起こす可能性があります。開発者は、この問題を回避するために HasValue および TryGetValue(out T) メンバーによるカスタムの非 boxing 実装を提供するか、または [Union] 属性を使用してコンパイラによる再書き換えを適用することで(例:new MacOS(...) のようなレコードを union wrapper に巻き込む)、暗黙的変換を回避できます。union を使用するには、.NET 11 プレビュー SDK をインストールし、.csproj で <LangVersion>preview</LangVersion> を設定するか、2026 年 5 月 19 日までの時点では [Union] 属性の適用または言語バージョンの設定により earlier runtimes(例:.NET 8)をターゲットにします。IDE でのサポートは現在 Visual Studio Preview および VS Code C# DevKit Insiders で利用可能で、JetBrains Rider のサポートは待機中です。エコシステムが進化するにつれて、将来のアップデートでは閉じた enum、閉じた階層、union メンバープロバイダーを導入し、型安全性と網羅性チェックをさらに強化することで、Windows、Linux、MacOS 上で互換性のある開発環境を利用したより柔軟なクロスプラットフォームアプリケーションの実現を可能にします。本文
#.NET 11(C# 15)で登場!ついにユニオン型を獲得しました
2026 年 5 月 19 日公開|所要時間:約 10 分
本シリーズ「#.NET 11 プレビュー版の探求」の第 2 部です。
- 第 1 部 - Blazor で Web Workers を使用してバックグラウンドタスクを実行する
- 第 2 部 - .NET(つまり C#)ついにユニオン型を獲得しました 🎉(本記事)
ユニオン型とは何か?
ユニオン型は、関数型プログラミングにおける基本的なデータ構造です。F#、TypeScript、Rust などの言語で広く利用されており、「一つの型として二つの異なるものを表現する」ための機能です。
代表的なパターン:Result<T>
Result<T>最も一般的な実装例の一つである
Result<T> は、以下の 2 つの状態のみを持ちます:
- Success(成功) - 操作の結果を含む
- Error(エラー) - エラーメッセージなどの情報を含む
このパターンは「結果パターン(result pattern)」とも呼ばれ、呼び出し側が明示的に両方のケースを処理することを義務付けます。
柔軟な型の組み合わせ
ユニオン型は
Result に限定されず、任意の組み合わせられた型セットを表現するために使用できます。例えば、「複数の潜在的に関連性のない型のいずれか」を表す場合に最適です。
C# 15 におけるユニオン型と union
キーワード
unionC# 15(正式には C# 15)では、
union キーワードによる直接サポートが導入されました。これにより、複数の異なる型のいずれかであるデータを格納できる型を定義できます。
多様なタイプの例
例えば、OS の情報を持つレコードを定義する場合:
public record Windows(string Version); public record Linux(string Distro, string Version); public record MacOS(string Name, int Version);
これらを単一の型で扱えるようになります。
新しい構文と IUnion
インターフェース
IUnion// 👇 型として `union` を使用 public union SupportedOS(Windows, Linux, MacOS); // 👆 ユニオンに含まれる型のリスト
生成された
SupportedOS は、以下のインターフェースを実装します:
public interface IUnion { object? Value { get; } }
インスタンスの作成方法
- 明示的なコンストラクタ呼び出し
SupportedOS os = new SupportedOS(new MacOS("Tahoe", 25)); - インプリシット変換(裏側でコンストラクタが呼ばれる)
SupportedOS os = new MacOS("Tahoe", 25); // ✅ これも可
値へのアクセス:switch
式
switch.Value を経由して object? として取得することもできますが、標準的な処理は switch 式 です。
string GetDescription(SupportedOS os) => os switch { Windows windows => $"Windows {windows.Version}", Linux linux => $"{linux.Distro} {linux.Version}", MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})", }; // 注:discard `_` は必要ありません
コンパイラーは自動で網羅性をチェックし、未処理のケースがある場合は警告が出ます。また、nullable なケース(例:
MacOS?)には null チェックも含まれます。
Result<T>
も同じように実装可能
Result<T>public union Result<T>(T, Exception);
または
Option<T> 型を定義することも可能です:
public record class None; public union Option<T>(None, T);
.NET 11 におけるユニオン型の実用化
ユニオン型を使用するには以下の準備が必要です。
1. ソフトウェア要件
- .NET 11 プレビュー版 2 以上の SDK のインストール
- 推奨: プレビュー版 4+ を使用するとより安定した体験ができます
2. プロジェクト設定(.csproj
)
.csproj言語機能を有効にするために、以下を追加します:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <!-- 👇 これを追加します --> <LangVersion>preview</LangVersion> <TargetFrameworks>net11.0;net8.0;net48</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
3. 古いランタイムをターゲットする場合の対応
[Union] 属性や IUnion インターフェースが標準に含まれていない環境(プレビュー版 2/3、または古いランタイム)では、以下のコードを手動で追加する必要があります:
#if !NET11_0_OR_GREATER namespace System.Runtime.CompilerServices; [AttributeUsage(Class | Struct, AllowMultiple = false, Inherited = false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } #endif
IDE サポート状況
- Visual Studio Preview / VS Code (C# DevKit Insiders): 初期サポートあり
- JetBrains Rider: 未対応(待機中)
ユニオン型がどのように実装されているか
.NET 11 では、コンパイラーが自動的に簡素な構造体を生成します。
ビルドインの動作
宣言:
using System.Runtime.CompilerServices; [Union] public struct SupportedOS : IUnion { public object? Value { get; } // 各ケースタイプに対するコンストラクター public SupportedOS(Windows value) => this.Value = (object) value; public SupportedOS(Linux value) => this.Value = (object) value; public SupportedOS(MacOS value) => this.Value = (object) value; }
生成される型は以下の特性を持ちます:
属性で装飾された[Union]struct- 単一の readonly プロパティ
object? Value - すべてのケースタイプに対するコンストラクター
インプリシット変換の裏側
new MacOS(...) と書くと、コンパイラーは内部で new SupportedOS(new MacOS(...)) に置き換えます。この挙動は [Union] 属性によって制御されています。
属性を忘れた場合のエラー確認
もし
[Union] 属性を指定し忘れると、インプリシット変換や switch 式が機能しません:
// エラーが発生します SupportedOS os = new MacOS("Tahoe", 25); // error CS0029: 型'MacOS'から'SupportedOS'へのインプリシット変換できません var description = os switch { Windows windows => "...", // error CS8121: パターンで処理できません // ... };
カスタムユニオン実装でのボックス化の回避
ビルトインの実装は
object? Value を使用するため、値が自動的にボックス化されます。これは int や bool などの単純な値を扱う場合にパフォーマンス面で望ましくありません。
ボックス化の問題
以下のコードでは、値がヒープ上に配置されてしまいます:
[Union] public struct IntOrBool : IUnion { public object? Value { get; } // Struct の引数は常にボックス化され、ヒープ上に割り当てられます public IntOrBool(int value) => this.Value = (object) value; public IntOrBool(bool value) => this.Value = (object) value; }
回避方法:TryGetValue
パターン
TryGetValueパフォーマンスを重視する場合、「非ボックス化」実装が推奨されます。これには以下の追加メンバーが必要です:
:値が存在するか(null でないか)を判定bool HasValue { get; }
:型に応じて値を取り出すbool TryGetValue(out T value)
カスタム非ボックス化実装の例
[Union] public struct IntOrBool : IUnion { private readonly bool _isBool; private readonly int _value; public IntOrBool(int value) { _isBool = false; _value = value; } public IntOrBool(bool value) { _isBool = true; _value = value ? 1 : 0; // 真偽値を 1/0 に変換して整数で管理 } public bool HasValue => true; // ボックス化せずに int 値を取得 public bool TryGetValue(out int value) { value = _value; return !_isBool; } // ボックス化せずに bool 値を取得 public bool TryGetValue(out bool value) { value = _isBool && _value is 1; return _isBool; } // 👇 IUnion の要件を満たすが、通常は使用されません(ボックス化されるため) public object Value => _isBool ? (_value is 1) : _value; }
スイッチ式での使用
TryGetValue を実装すると、コンパイラーが自動でそれらを使用します:
IntOrBool unmatchedValue = new IntOrBool(23); string str; // 👇 ボックス化された Value プロパティの使用 대신 TryGetValue を呼び出します if (unmatchedValue.TryGetValue(out int _)) { str = "integer"; } else if (unmatchedValue.TryGetValue(out bool _)) { str = "bool"; }
その他、まだ来る予定の機能とは?
言語仕様にはまだ追加される予定の機能がいくつかあります:
- ユニオンメンバープロバイダー
ユニオン型自体とは異なる型でメンバを定義する方法 - 閉じた列挙型(Closed Enums)
Switch 式に「キャッチオールケース」(
) を含めなくてもよい列挙型_ => - 閉じた階層(Closed Hierarchies)
クラスに
モディファイアを追加し、外部からの派生を禁止する機能closed
これらが .NET 11 に導入されるかは未定ですが、今後対応されたら必ずカバーします!
まとめ
本記事では以下について解説しました。
- #.NET 11 プレビュー版 2 でユニオン型が正式にサポートされました。
キーワードとunion
属性による実装方法。[Union]- Switch 式(パターン Matching) を用いたデコンストラクションの実行方法。
- インターフェース
とプロパティIUnion
の仕組み。.Value - パフォーマンス向上のための**非ボックス化実装(TryGetValue)**の作成方法。
これにより、C# の網羅性(Exhaustiveness)が飛躍的に向上し、関数型プログラミングの手法がより安全に利用できるようになります。