PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

.NET 11でついに Union types が来た:C# 15の新機能をわかりやすく解説

まずは要点

Union type って何がうれしいの?

Union type は、ざっくり言うと「この変数には A 型か B 型か C 型のどれかが入る」という型です。

たとえば現実世界でも、同じ「OS情報」という名前であっても、中身は Windows だったり Linux だったり macOS だったりしますよね。
でも、それぞれ持っている情報はバラバラです。

こういう「似ていない型をひとまとめにしたい」場面では、これまで C# では少し工夫が必要でした。

ただ、どれも少し面倒です。
object にすると型安全性が消えますし、enum 管理は地味に事故りやすい。個人的には、こういう「本来は言語が面倒を見てほしい部分」をようやく C# が拾ってくれたのはかなりうれしいと思います。

C# 15 の union キーワード

記事では、次のような例が紹介されています。

public union SupportedOS (Windows, Linux, MacOS);

これは「SupportedOS という型は、Windows / Linux / MacOS のどれかを持てます」という宣言です。

さらに、こうやって生成した Union は普通に作れます。

SupportedOS os = new SupportedOS(new MacOS("Tahoe", 25));

しかも、型によっては暗黙変換も使えるので、次のように書けます。

image_0001.png

SupportedOS os = new MacOS("Tahoe", 25);

見た目がかなり自然です。
「Union を使っている」と意識しなくても、普通の C# の型のように扱えるのがいいところだと思います。

中身を取り出す方法

Union type には Value というプロパティがあり、中に入っている値を object? として取り出せます。

Console.WriteLine(os.Value);

ただし、これは「とりあえず中身を見る」ための手段で、実際にはあまりおすすめの使い方ではなさそうです。
本命は 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})",
};

これがかなり気持ちいいです。
Windows なら Windows 用の処理、Linux なら Linux 用の処理、というふうに自然に分岐できます。

しかも、全部のケースを書き忘れるとコンパイラが警告してくれます。
これは地味だけど超重要です。
「あとでバグになる見落とし」を、コンパイラが先回りして教えてくれるからです。

これまでの C# で何がつらかったのか

Union type がなかった時代、こういう表現をするには少し苦労しました。

1. 基底クラスを作る

共通の親クラスを作ってまとめる方法です。
ただし、元の型を自由に変更できる場合に限ります。ライブラリ側の型だと、こちらで勝手に親クラスを追加できません。

2. object に入れる

最も雑ですが、最も安全性が落ちます。
中身を取り出すたびにキャストが必要で、ミスると実行時エラーになりやすいです。

3. tag で管理する

「今入っているのは Windows なのか Linux なのか」を別の値で管理する方法です。
ただし、管理項目が増えるとコードがどんどん複雑になります。

image_0005.png

こうして見ると、Union type はかなり素直な解決策です。
「いま欲しかったのはこれだよね」と言いたくなるタイプの機能だと思います。

Result<T>Option<T> も表現しやすい

記事では、Union type はかなり広く使えると説明されています。

たとえば、よくある Result<TSuccess, TError> は、成功か失敗かのどちらかを持つ構造です。

これを Union で表すと、考え方としてはかなりきれいです。

public union Result<T>(T, Exception);

同様に、値があるかないかを表す Option<T> も書けます。

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

このへんは、関数型言語に慣れている人には「やっと来たか」という感じではないでしょうか。
C# はどちらかというと実用派の言語ですが、こういう表現力が増えると、堅牢なコードを書きやすくなるのが大きいです。

使うには何が必要?

記事時点では、.NET 11 preview 2 以降の SDK が必要です。
ただし、より快適に使うなら preview 4 以降がよいとのことでした。

さらに、プロジェクトファイルで preview の言語機能を有効にします。

<LangVersion>preview</LangVersion>

なお、重要なのは「これはランタイム機能というよりコンパイラ機能だ」という点です。
そのため、.NET 11 SDK を使いながら、古いランタイムをターゲットにすることもできます。ここは少しややこしいですが、要するに「新しいコンパイラが Union を理解してくれる」から動く、という話です。

image_0006.jpg

ただし、古いランタイムや preview 2〜3 を使う場合は、補助用の型を自前で入れる必要があります。

preview 4 以降なら標準で入っています。

裏側の実装はかなりシンプル

記事の面白いところは、Union type の実装が意外と単純だと見せてくれる点です。

コンパイラが生成する型は、おおむねこんな感じです。

[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] がないと何が起きる?

記事では、あえて [Union] 属性を外した例も紹介されています。
すると、MacOSSupportedOS にそのまま代入できなくなり、switch のパターンマッチも通らなくなります。

image_0008.svg

つまり、コンパイラは [Union] を見て

という特別扱いをしているわけです。

これは「ただの便利な型」ではなく、「言語とコンパイラが協力して意味を理解している」機能だと分かります。
この手の仕組みが増えると、コードは短くなるだけでなく、読みやすさも上がるので嬉しいです。

既存のカスタム Union 実装にも追い風

記事では、OneOfSasa のような既存ライブラリにも触れています。
こうしたライブラリは以前から Union 的なことをやってきましたが、C# の言語機能として正式に扱われると、switch のサポートなどでより自然に使えるようになります。

これもかなり重要です。
新機能というと「これから何かを作る人向け」に見えがちですが、実際には既存コードや既存ライブラリの価値を押し上げることも多いんですよね。
私はこういう「新機能がエコシステム全体を少しずつ底上げする」感じが好きです。

まとめ

.NET 11 / C# 15 の Union type は、ずっと欲しかった人が多いであろう、かなり実用的な追加機能です。

特に良いのは、

という点です。

派手さはないけれど、開発体験をかなり押し上げるタイプの機能だと思います。
C# は少しずつ「現代的で表現力の高い言語」へ寄ってきていますが、Union type はその流れを象徴する一歩ではないでしょうか。


参考: .NET (OK, C#) finally gets union types🎉: Exploring the .NET 11 preview - Part 2

同じ著者の記事