世界を動かす技術を、日本語で。

.NET (OK, C#) についにユニオン型が追加されました

概要

  • C# 15 で待望の union型 が正式に導入
  • 複数型を安全に扱えるデータ構造として Result<>Option<> の実装例を紹介
  • unionキーワード とその利用方法、実際のコード例を解説
  • .NET 11 での利用準備やIDEサポート状況に言及
  • カスタムUnion型 の実装やパフォーマンス上の注意点も説明

C# 15のUnion型とは何か

  • union型 は、複数の異なる型を一つの型として安全に扱えるデータ構造
  • F#、TypeScript、Rustなど多くの関数型言語で一般的な機能
  • 例として Result<TSuccess, TError>Option<T> などがあり、成功・失敗や値の有無を明示的に表現可能
  • C# 15以前は、基底クラスの継承やobject型の利用、タグ値管理など煩雑な実装が必要だった
  • unionキーワード により、型安全かつ簡潔な記述が可能となった

C# 15でのUnion型の利用方法

  • unionキーワード で複数の型をまとめて定義可能
    • 例: public union SupportedOS(Windows, Linux, MacOS);
  • インスタンス生成は明示的にnewを使うか、暗黙的な型変換が利用可能
    • 例: SupportedOS os = new MacOS("Tahoe", 25);
  • IUnionインターフェース を自動実装し、格納値へは .Value プロパティでアクセス
  • switch式 で型ごとの分岐処理が簡単に記述可能
    • すべての型ケースを網羅しない場合、コンパイル時に警告表示
  • null許容型を含む場合は、switch式でnullケースも必須

Union型の実装例

  • Result<T> の例: public union Result<T>(T, Exception);
  • Option<T> の例:
    • public record class None;
    • public union Option<T>(None, T);
  • これにより、従来のカスタム実装よりも簡潔で型安全なコードが実現

.NET 11でUnion型を使うための準備

  • .NET 11 preview 2以上 のSDKインストールが必要
  • プロジェクトファイルに <LangVersion>preview</LangVersion> を追加
    • 例:
      • <LangVersion>preview</LangVersion>
      • <TargetFrameworks>net11.0;net8.0;net48</TargetFrameworks>
  • .NET 11未満やpreview 2/3利用時は、 UnionAttributeIUnion インターフェースの自前実装が必要
  • IDEサポートは Visual Studio PreviewVS Code C# DevKit Insiders で初期対応
    • JetBrains Rider のサポートは今後予定

Union型の内部実装について

  • [Union]属性 付きのstructとして自動生成され、IUnionインターフェースを実装
  • 各ケース型ごとにコンストラクタが生成され、値はobject?型のValueプロパティに格納
  • 暗黙的な型変換は、[Union]属性により実現
  • [Union]属性を外すと、型変換やswitch式での分岐が利用不可

カスタムUnion型実装とパフォーマンス注意点

  • 既存の OneOfSasa などのカスタムUnion型も、IUnion実装と[Union]属性付与で言語機能の恩恵を受けられる
  • 標準Union型は内部的にobjectとして値を保持するため、struct型の値は ボックス化 されヒープ領域に格納される点に注意
    • 例: public union IntOrBool(int, bool); ではintやboolもobject型として格納
  • パフォーマンス重視の場合は、独自実装やボックス化回避の工夫が必要

まとめ

  • C# 15のunion型 は、複数型を安全に扱うための強力な新機能
  • 型安全・簡潔な記述・switch式での分岐が大きなメリット
  • 実装やパフォーマンス要件に応じて、標準・カスタムの使い分けが重要

Hackerたちの意見

F#が先頭を切って、C#がゆっくり追いついてくるって感じだね。なのに、何故かC#が一番注目されてる。

Haskell、OCaml、Erlangが先頭を走って、Rust、Zig、Goが注目を集めてる。実験的な言語が新機能を開発して、他の言語がそれを真似てCスタイルの文法に持ってくるのはよくあるパターンだと思う。

F#の方がC#よりも解決しやすい問題って何があるの?F#とC#を一つのコードベースで組み合わせるのは可能?それは推奨されるの?

F#が大好き。毎日使ってる言語だし、個人的にはIDEのサポート(パフォーマンスやQoL機能など)がC#に比べて遅れてる唯一の部分だと思う。それ以外は、自分がやりたいことに関しては圧倒的に勝ってるよ。

マイクロソフトの経営陣はCLRに新しい意味を持たせることに決めたみたい。C#ランタイムってね。VBやC++/CLI、F#は既存の顧客のためだけに存在してる。F#をVS 2010の公式言語として研究プロジェクトから昇格させたのは間違いだったかのように振る舞ってきた。以来、チームは.NET顧客基盤にどう売り込むか全く分からなくなって、C#やVBのためのライブラリだけになったり、ユニットテストやWeb開発、データ分析など、何でもできるように方向転換してきた。でも、Standard ML、Miranda、Hope、OCaml、Haskellが道を切り開いてきたのに、まだ完全には達成できてないね。

C#は平均的に理解しやすくて使いやすいから、みんなの注目を集めてる。F#がもっと賢くて簡潔なのはみんな認めるところだよね。誰もそのことにこだわってない。でも、顧客の要求に応えたり、他の人たちとチームで働く上では、同じようには通用しないんだ。確かに、何か特別な努力や無関心があるわけじゃない。マイクロソフトがF#のマーケティング予算を10倍にしたとしても、採用率は多分変わらないだろうね。

ようやくC#にこれが入るのを見られて嬉しいよ。C#でユニオンを純粋に使いたいわけじゃなくて、他の言語とインターフェースする時に定義できるようにしたいんだ。

C#が大好きで、毎回のアップデートでCっぽいパフォーマンスを得られる機能が増えていくのが嬉しい。C#は本当に上手くやってるよ。もしパフォーマンスやメモリに制約がなければ、これらの機能を無視して言語の使いやすさに戻れるからね。

プロとして.NET開発者やってるけど、君が書いたことには完全に同意するよ。ただ、C#がその点で特にユニークだとは思わないな。SwiftやJava、Kotlinなど、いろんな一般的なコンパイル言語も同じ道を歩んでると思う。時間が経つにつれて、グリーンフィールドプロジェクトでC#を使う理由を見つけるのが難しくなってきてる。

今の時点で、C#はJavaを機能やパフォーマンスで追い越したと思う?

F#は何十年も前からこれを持ってたけど、C#は基本的にCスタイルの文法でF#に少しずつ近づいてる感じ。文句は言わないけど、ほとんどのチームは言語を切り替えないから、実際に使われる場所でこれらの機能が得られるのは悪くないよね。

C#でユニオン型を待ってたから、もう文法にはこだわらないよ。動くものをくれればいい。だから、努力には感謝してる。これを形にするのに少なくとも10年かかったのは知ってるし、かなり考えられてるんだろうね。チームに拍手を送りたい。

C#の大ファンだけど、これはちょっと残念だね。常に値型をボックス化しちゃうから。

ボクシングしないオプションもあるし、これはまだ初期段階の作業だから、これで終わりってわけじゃないよ。

C#に興味があった頃(今はもうないけど)、この機能や関連機能の議論にちょっと関わってたんだ。主に君の求めてることを理論的に考えてたんだけど、JITチームはサポート追加には全く興味がないってはっきり言ってたよ。C#コンパイラはある程度できるけど、実用的にするにはあまりにも多くの条件がある。JITチームが心変わりしない限り、これを見ることはないだろうね。

今日も常にボックス化されてるよ。最近チームはMVPをリリースして、後でパフォーマンスを改善してるみたい。この記事にもあるように、すでに自分で回避策を考えられるから、ここでも同じことをするかもしれないね。

こんにちは、C#の言語デザイナーの一人です。これについては、https://news.ycombinator.com/item?id=48255658 で少し話しています。多くの人が思っていることとは逆に、ボクシングは実際にはほとんどの場面で非常に良い戦略なんです。そして、実際に多くの人がここでそれを行っています。デザインはボクシングしないパターンをサポートしていて、私たちはそれに対して合理的なデフォルトを持つ実装を提供することを期待しています。また、ソースジェネレーターは、特定の顧客のニーズに応じたボクシングしないための高度に専門化された実装戦略を得るための素晴らしい方法になると思います。

やっと来たね。TypeScriptやRustが、型レベルで「これかあれ」ってモデル化できると、どれだけコードがクリーンになるかを証明した。真の試練は、ライブラリの作者たちがこれを公開APIで使い始めるか、それともただの好奇心で終わるかだね。

スタンダードMLが50年前にこれを証明したよ。

ついにC#にも、2005年の「The Daily WTF」からのこのクラシックなコード例を表現するもっと自然な方法ができるんだね。— enum Bool { True, False, FileNotFound };

FalseがFileNotFoundと等しいってこと?これはASTシンボル制約のケースなのかな?

ひどいけど、どうやってこうなったのかは想像できる…多分、コードよりも自分のことを語ってる気がする。

マイクロソフト自身の「MsoTrioState」はどうなの? enum MsoTrioState { Toggle, Mixed, True, False, CTrue };

デフォルトの言語レベルのunionキーワード実装がボクシングを強制する理由は何なんだろう?HasValue/TryGetを実装してそれを避けられるなら、ちょっとおかしな決定だよね。

こんにちは!C#の言語デザイナーです。ユニオンに関わっている一人でもあります。ボクシングは本来避けるべきものではないんですよ。実際、多くの(ほとんどの?)ケースでうまく機能するし、ボクシングを使わないアプローチが引き起こす問題(例えば、テアリングやコピーコスト)を避けることができます。ボクシングを使わないパターンは私たちが実装できることも確かですし、リリース後にそれを行う可能性も十分あります。ただ、これは簡単な分野ではありません。「ボクシングしない」実装に正解はないんです。例えば、すべての非管理データに対して別々のフィールドを持つべきですか?それとも、最大の非管理フィールドのセットからすべてのデータを整列させるのに十分なサイズのバイトの塊を持っていて、それに対してアンセーフインデックスを使うべきですか?管理データについても同じような質問があります。強い型のフィールドを持っていますか?それとも、できるだけ小さなスペースに収めるためにオブジェクトを使おうとしていますか?前者はキャストコストを避けられますが、後者はスペースを最小限に抑えることができます。アンセーフキャストを使うことも可能ですが、テアリングの状況ではメモリホールを引き起こすかもしれません。だから、最良の結果はパターンを定義すること(これはもうやりました)で、その後にジェネレーターを使って、実装戦略を正確に制御できるようにすることだと思います。これで、自分のドメインに最適な調整ができるようになります。

System.Text.Jsonがシリアライズやスキーマのエクスポートをサポートするまで、あと3年くらいかかるのは残念だね…。