共用体の型 (C# リファレンス)

共用体の型は、複数のケース型のいずれかを指定できる値を表します。 共用体は、各ケースの種類からの暗黙的な変換、完全なパターン マッチング、および強化された null 許容追跡を提供します。 共用体の型を宣言するには、 union キーワードを使用します。

public union Pet(Cat, Dog, Bird);

この宣言では、Pet、Cat、Dogの 3 つのケースの種類を持つBird共用体が作成されます。 Pet変数には、任意のケース型の値を割り当てることができます。 コンパイラは、 switch 式がすべてのケースの種類をカバーすることを保証します。

C# 言語リファレンスには、C# 言語の最新リリース バージョンが記載されています。 また、今後の言語リリースのパブリック プレビューの機能に関する初期ドキュメントも含まれています。

このドキュメントでは、言語の最後の 3 つのバージョンまたは現在のパブリック プレビューで最初に導入された機能を特定します。

ヒント

C# で機能が初めて導入された時期を確認するには、 C# 言語バージョン履歴に関する記事を参照してください。

値が固定型のセットのいずれかである必要があり、すべての可能性が処理されるようにコンパイラに強制する必要がある場合は、共用体を宣言します。 一般的なシナリオは、次のとおりです。

  • 結果またはエラーが返されます。メソッドは成功値またはエラー値を返し、呼び出し元は両方を処理する必要があります。 union Result(Success, Error)のような共用体は、結果のセットを明示的にします。
  • メッセージまたはコマンドのディスパッチ: システムは、閉じたメッセージの種類のセットを処理します。 共用体を使用すると、新しいメッセージの種類では、まだ処理されないすべての switch でコンパイル時の警告が生成されます。
  • マーカー インターフェイスまたは抽象基底クラスの置き換え: パターン マッチングの型をグループ化するためだけにインターフェイスまたは抽象クラスを使用する場合、共用体は継承や共有メンバーを必要とせずに完全なチェックを提供します。

共用体は、重要な点で他の型宣言とは異なります。

  • classやstructとは異なり、共用体では新しいデータ メンバーは定義されません。 代わりに、既存の型を閉じた代替セットに構成します。
  • interfaceとは異なり、共用体は閉じられます。宣言でケース型の完全なリストを定義し、コンパイラはそのリストを使用して完全なチェックを行います。
  • recordとは異なり、共用体は等価性、複製、または分解動作を追加しません。 共用体では、"どのフィールドがありますか" ではなく、"どのケースですか" に焦点を当てています。

共用体の型は、UnionAttribute名前空間のIUnion型とSystem.Runtime.CompilerServices型に依存します。 .NET 11 ランタイムには、これらの型が含まれています。

共用体宣言

共用体宣言では、名前とケースの種類の一覧を指定します。

public union Pet(Cat, Dog, Bird);

ケース型 には、クラス、構造体、インターフェイス、型パラメーター、null 許容型、その他の共用体など、 objectに変換する任意の型を指定できます。 次の例は、さまざまなケースの種類の可能性を示しています。

public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);

ケース型が値型 ( int など) の場合、共用体宣言は値をボックス化し、単一の object? 参照として格納します。 カスタム共用体の種類 では、別のストレージ戦略を使用できます。

共用体宣言には、構造体と同様に、いくつかの制限に従って、追加のメンバーを含む本文を含めることができます。 共用体宣言には、インスタンス フィールド、自動プロパティ、またはフィールドに似たイベントを含めることはできません。 また、1 つのパラメーターでパブリック コンストラクターを宣言することもできません。これは、コンパイラがそれらのコンストラクターを共用体作成メンバーとして生成するためです。 宣言するすべてのコンストラクターは、生成されたコンストラクターに直接または間接的にデリゲートするために、 this(...) 初期化子を使用する必要があります。 次の Length 共用体は、パターン マッチングを使用してすべてのケースの種類を処理する TotalMeters プロパティと、2 つの長さを組み合わせた Add メソッドを追加します。

public record class Meters(double Value);
public record class Feet(double Value);

public union Length(Meters, Feet)
{
    public double TotalMeters => this switch
    {
        Meters m => m.Value,
        Feet f => f.Value * 0.3048,
        _ => throw new InvalidOperationException("The Length has no value."),
    };

    public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}

共用体の変換

各ケース型から共用体型への暗黙的な 共用体変換 が存在します。

static void BasicConversion()
{
    Pet pet = new Dog("Rex", "Labrador");
    Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }

    Pet pet2 = new Cat("Whiskers");
    Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}

共用体変換は、対応する生成されたコンストラクターを呼び出すことによって機能します。 同じ型に対してユーザー定義の暗黙的な変換演算子が存在する場合、ユーザー定義演算子は共用体変換よりも優先されます。 複数のケース型がソース値に同じように適用される場合、共用体変換はあいまいであり、コンパイラはエラーを報告します。 変換の優先順位の詳細については、 機能の仕様を参照してください。

null 許容共用体構造体 (T?) への共用体変換は、 T が共用体型の場合にも機能します。

static void NullableUnionExample()
{
    Pet? maybePet = new Dog("Buddy", "Beagle");
    Pet? noPet = null;

    Console.WriteLine(Describe(maybePet)); // output: Buddy
    Console.WriteLine(Describe(noPet));    // output: no pet

    static string Describe(Pet? pet) => pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
        null => "no pet",
    };
}

和集合パターン マッチング

共用体式で型または宣言パターンを使用する場合、パターンは共用体インスタンスではなく共用体の内容をテストします。 たとえば、 Dog パターンでは、 Value が Dogであるかどうかをテストします。

static void PatternMatching()
{
    Pet pet = new Dog("Rex", "Labrador");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

パターンの形式によって、テストする値が決まります。

Pattern 共用体式に対する動作
型または宣言パターン ( Dog や Dog dog 指定した型に対して内容をテストします。
型を持つプロパティまたは位置パターン (例: Dog { Breed: "Labrador" } 型に対して内容をテストし、そのケース値に入れ子になったパターンを適用します。
型のないプロパティまたは位置パターン (例: { Name: "Rex" } 共用体インスタンスのプロパティまたは Deconstruct メソッドを使用します。
リストのパターン 共用体インスタンスのカウントメンバーとインデックス作成メンバーを使用します。コンテンツのメンバーは使用しません。
var またはパターン _ 破棄する ラップを解除せずに、受信共用体インスタンスをキャプチャまたは照合します。
定数またはリレーショナル パターン 内容をテストします。 null パターンは、null 共用体参照または null 許容ラッパーも処理します。

たとえば、含まれているCatまたはDogにデリゲートする読み取り専用のName プロパティを使用して、Pet共用体を定義します。 プロパティの switch 式の型パターンは、内容をテストします。

public union Pet(Cat, Dog)
{
    public string Name => this switch
    {
        Cat cat => cat.Name,
        Dog dog => dog.Name,
        null => "",
    };
}

型指定されたパターン Dog { Breed: "Labrador" } 内容をテストし、共用体に含まれていないプロパティである犬の Breedを読み取ります。 型指定されていないパターン { Name: "Rex" } は、代わりに共用体の Name プロパティを読み取るので、Rex という名前の猫にも一致します。

Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" });           // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" });           // output: True

not パターンは、入れ子になったパターンを否定します。 pet is not nullでは、入れ子になったnull パターンによって内容がチェックされるため、not nullは便利な値を持つテストです。 これに対し、空の型指定されていないプロパティ パターン { }は、共用体インスタンスをチェックします。 Valueが null の場合でも、常に null 非許容構造体共用体と一致します。

Pet empty = default;
Console.WriteLine(empty is { });       // output: True
Console.WriteLine(empty is not null);  // output: False
if (empty is var captured)
{
    Console.WriteLine(captured.Value is null); // output: True
}

論理パターンでは、これらの区別が保持されます。 var pet and not nullでは、var petは受信共用体をキャプチャし、null 以外の内容があるかどうかをテストnot null。 not null and var petでは、var パターンは引き続き受信共用体をキャプチャします。notは、右側のパターンに渡される値を変更しません。 これに対し、 Dog and { Breed: "Labrador" }では、型パターンは一致した Dog をプロパティ パターンに渡します。

注

型パターンは、少なくとも 1 つのケース型と互換性がある必要があります。 この記事の Pet 宣言の場合、 pet is Pet はコンパイラ エラー CS8121 を生成します。いずれのケースの型も Petに一致することはできません。 これは、共用体インスタンスのランタイム テストではありません。 内容を確認するには、 pet is not null または pet.Value is not nullを使用します。共用体自体をキャプチャするには、 var パターンを使用します。

簡単に言えば、型と宣言のパターンによって内容がテストされます。 型を持つ再帰パターンでも内容がテストされます。型のない再帰パターンでは、共用体独自のメンバーが使用されます。 これらの規則は、 is 式と switch 式で同じです。

Null 照合

構造体共用体の場合、 null パターンは、 Value が null であるかどうかをチェックします。

static void NullHandling()
{
    Pet pet = default;
    var description = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
        null => "no pet",
    };
    Console.WriteLine(description); // output: no pet
    Console.WriteLine(pet.Value is null); // output: True
}

クラス ベースの共用体の場合、共用体参照自体が null であるか、そのnullプロパティが null の場合、Valueは成功します。

Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }

Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }

null 許容共用体構造体型 (Pet?) の場合、 null は、null 許容ラッパーに値がない場合、または基になる共用体の Value が null の場合に成功します。

和集合の網羅性

switch式は、各ケースの種類を処理するときに、共用体のケース型を網羅しています。 null 値許容分析で null 以外の内容も考慮される場合は、破棄 (_) や var arm は必要ありません。

static void PatternMatching()
{
    Pet pet = new Dog("Rex", "Labrador");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

null の内容を処理するには、varまたは破棄 (_) アームよりもnullアームを使用します。 null パターンでは、一致する arm を使用せずに新しいケースの種類を追加するときに、意図が示され、警告が保持されます。 varまたは破棄アームは新しいケースも受け入れるため、コンパイラはその不足しているケースについて警告できません。

Valueの追跡された null 状態が "null" の場合、null arm を省略するとコンパイラも警告します。 たとえば、 NullablePet には null 許容のケース型があるため、パラメーターの内容は null になる可能性があります。

public union NullablePet(Cat?, Dog, Bird);

static string DescribeNullableCase(NullablePet pet) => pet switch
{
    Cat cat => cat.Name,
    Dog dog => dog.Name,
    Bird bird => bird.Name,
    null => "no pet",
};

このスイッチから null アームを取り外した場合、警告 CS8655 が生成されます。

Null 許容分析では、生成された共用体に実行時に null 以外の内容が含まれるという保証はありません。 既定の共用体には、既定で初期化されたフィールドまたは配列要素である場合を含め、null が含まれます。 すべてのケース型が null 非許容の場合、 Value の既定の追跡状態は "null ではない" ため、コンパイラは省略された null arm について必ずしも警告しません。 既定のコンテンツがスイッチに到達できる場合は常に、 null を正しく処理します。

static void NullHandling()
{
    Pet pet = default;
    var description = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
        null => "no pet",
    };
    Console.WriteLine(description); // output: no pet
    Console.WriteLine(pet.Value is null); // output: True
}

この Pet 例では、前のコードで追跡対象の null 状態が変更されない限り、 null arm を削除しても null アーム不足の警告は生成されませんが、スイッチは既定値を受け取ったときに SwitchExpressionException をスローします。 以前の is null テストでは、追跡対象の状態を変更し、警告を発生させることができます。 この動作は明確な代入とは別です。コンパイラは、割り当てられていないローカル変数の読み取りを拒否します。

NULL 値の許容

コンパイラは、次の規則を使用して、共用体の Value プロパティの null 状態を追跡します。

  • 共用体の Value プロパティの既定の null 状態は、任意のケース型の既定の null 状態が "null の可能性があります" の場合は "null" になります。それ以外の場合、既定の null 状態は "not null" です。
  • ケース型から (コンストラクターまたは共用体の変換を使用して) 共用体値を作成すると、 Value は受信値の null 状態を取得します。
  • ボックス化されていないアクセス パターンの HasValue または TryGetValue(...) メンバーが共用体の内容に対してクエリを実行すると、 Value の null 状態は true ブランチで "not null" になります。

カスタム共用体の型

コンパイラは、 union 宣言を struct 宣言に変換します。 構造体は [System.Runtime.CompilerServices.Union] 属性でマークされ、 IUnion インターフェイスを実装します。 各ケース型のパブリック コンストラクターと Value プロパティが含まれています。 この言語は、これらのコンストラクターを呼び出す暗黙的な共用体変換を提供します。 その生成されたフォームは意見が出されています。 これは常に構造体であり、常に値型のケースをボックス化し、常にコンテンツを object?として格納します。

既存の型を調整する場合、クラスベースの共用体を作成する場合、またはカスタム ストレージ戦略を使用する場合、または相互運用サポートが必要な場合は、異なる動作が必要になる場合があります。 共用体の種類は手動で作成できます。

[Union]に従う場合、属性を持つクラスまたは構造体は共用体型です。 基本的な共用体パターンには、次のものが必要です。

  • 型の [Union] 属性。
  • 1 つ以上のパブリック コンストラクター。それぞれが単一の値渡しまたは in パラメーターを持ちます。 各コンストラクターのパラメーター型は 、ケース型を定義します。
  • Value アクセサーobject? (またはobject) 型のパブリック get プロパティ。

上記の共用体メンバーはすべてパブリックである必要があります。 コンパイラはこれらのメンバーを使用して、共用体変換、パターン マッチング、および網羅性チェックを実装します。 また、 非ボックス化アクセス パターンを 実装したり、 クラスベースの共用体型を作成したりすることもできます。 カスタム共用体の種類では、さらにメンバーを追加できます。

コンパイラは、カスタム共用体の型が次の動作規則を満たすことを前提としています。

  • サウンド: Value は常に、 null またはケースの種類の 1 つの値 (既定の構造体値を含む) を返します。 カスタム共用体は、既定で生成される共用体の null 内容とは異なり、その既定値を null 以外のケースとして表すことができます。
  • 安定性: ケース型から共用体値を作成した場合、Valueはそのケースの種類と一致します (または、入力がnullされた場合はnull)。
  • 作成の等価性: 値が 2 つの異なるケース型に暗黙的に変換できる場合、両方の作成メンバーが同じ監視可能な動作を生成します。
  • アクセス パターンの整合性: HasValue メンバーと TryGetValue メンバー (存在する場合) は、 Value を直接チェックするのと同じように動作します。

次の例は、カスタム共用体の型を示しています。

[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
    private readonly object? _value;

    public Shape(Circle value) { _value = value; }
    public Shape(Rectangle value) { _value = value; }

    public object? Value => _value;
}

public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
    Shape shape = new Shape(new Circle(5.0));

    var area = shape switch
    {
        Circle c => Math.PI * c.Radius * c.Radius,
        Rectangle r => r.Width * r.Height,
    };
    Console.WriteLine($"{area:F2}"); // output: 78.54
}

非ボックス化アクセス パターン

カスタム共用体の種類では、必要に応じて 、非ボックス化アクセス パターンを 実装して、パターン マッチング中にボックス化を行わずに値型のケースに対して厳密に型指定されたアクセスを有効にすることができます。 このパターンでは、次のものが必要です。

  • HasValueがboolされていない場合にtrueを返すValue型のnullプロパティ。
  • TryGetValueを返し、bool パラメーターを使用して値を提供する、各ケース型のout メソッド。 TryGetValueは、trueがそのケース型の null 以外の値である場合にのみ、Valueを返します。 out パラメーターの型は、ケース型に対して ID に変換できます。また、ケース型が null 許容値型の場合は基になる値型に変換できます。
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
    private readonly int _intValue;
    private readonly bool _boolValue;
    private readonly byte _tag; // 0 = none, 1 = int, 2 = bool

    public IntOrBool(int? value)
    {
        if (value.HasValue)
        {
            _intValue = value.Value;
            _tag = 1;
        }
    }

    public IntOrBool(bool? value)
    {
        if (value.HasValue)
        {
            _boolValue = value.Value;
            _tag = 2;
        }
    }

    public object? Value => _tag switch
    {
        1 => _intValue,
        2 => _boolValue,
        _ => null
    };

    public bool HasValue => _tag != 0;

    public bool TryGetValue(out int value)
    {
        value = _intValue;
        return _tag == 1;
    }

    public bool TryGetValue(out bool value)
    {
        value = _boolValue;
        return _tag == 2;
    }
}
static void NonBoxingExample()
{
    IntOrBool val = new IntOrBool((int?)42);

    var description = val switch
    {
        int i => $"int: {i}",
        bool b => $"bool: {b}",
    };
    Console.WriteLine(description); // output: int: 42
}

パターン マッチングの場合、コンパイラは型パターン チェック (union is T など) と null パターン チェック (union is null など) のHasValueのTryGetValueを呼び出し、値型のケースのボックス化を回避します。 各メンバーは、独自のパターンの種類に適用されます。どちらも、もう一方のパターンのフォールバックでもありません。 メンバーが見つからない場合、コンパイラは代わりにそのパターンの Value プロパティをチェックします。 詳細については、「 パターン マッチング用のコードをコンパイラが生成する方法」を参照してください。

共用体メンバー プロバイダー

共用体型は、その共用体メンバーを、直接入れ子になった IUnionMembers インターフェイスに委任できます。 そのインターフェイスは 、共用体メンバー プロバイダーです。 コンパイラは、継承されたメンバーを含め、プロバイダー インターフェイスでのみ共用体メンバーを検索します。 共用体の型自体でのみ宣言されたメンバーは、共用体メンバーとして使用されません。

共用体メンバー プロバイダーを使用する場合、インターフェイスは次の必須メンバーを宣言する必要があります。

  • 静的 Create メソッド: 1 つのパラメーターと、共用体型に変換可能な戻り値の型 ID を持つ、ケースの種類ごとに 1 つ。 これらのメソッドは、ケースの種類を確立します。
  • Value プロパティ: 含まれている値を返すget アクセサーを持つパブリック objectまたはobject? プロパティ。

必要に応じて、インターフェイスでこれらのメンバーを宣言して、コンパイラがより効率的なパターン マッチング コードを生成できるようにします。

  • TryGetValueメソッド: ケースの種類ごとに 1 つ、ケース型のout パラメーターを持つboolを返します。
  • HasValueプロパティ: Valueがnullされていない場合にtrueを返すget アクセサーを持つパブリック bool プロパティ。

コンパイラがパターン マッチング用のコードを生成する方法: TryGetValue と HasValue 異なる種類のパターンに適用されます。どちらも他のパターンのフォールバックでもありません。

  • union is Tなどの型パターンの場合、コンパイラは宣言時にTryGetValue(out T value)を呼び出し、値を抽出し、ボックス化せずにパターンを適用します。 それ以外の場合、コンパイラは、 Value プロパティにパターンを適用します。このプロパティは、 T に対してランタイム型チェックを実行し、 T が値型の場合に値をボックス化できます。
  • union is nullなどの null パターンの場合、コンパイラは、共用体に値が含まれているかどうかをテストするために宣言されたときにHasValueを呼び出します。 それ以外の場合、コンパイラは null パターンを Value プロパティに直接適用します。

各メンバーは個別に省略可能です。 TryGetValueを使用しない場合、型パターンは Valueを使用します。 HasValueを使用しない場合、null パターンでは Valueが使用されます。 いずれかのメンバーを宣言すると、コンパイラは、そのパターンの種類に対して、より効率的で厳密に型指定されたコードを生成できます。

[System.Runtime.CompilerServices.Union]
public struct Outcome<T> : Outcome<T>.IUnionMembers
{
    private readonly object? _value;

    private Outcome(object? value) => _value = value;

    public interface IUnionMembers
    {
        static Outcome<T> Create(T? value) => new(value);
        static Outcome<T> Create(Exception? value) => new(value);
        object? Value { get; }

        // Optional but recommended: TryGetValue enables efficient pattern matching
        bool TryGetValue(out T value);
        bool TryGetValue(out Exception value);
    }

    object? IUnionMembers.Value => _value;

    public bool TryGetValue(out T value)
    {
        if (_value is T t)
        {
            value = t;
            return true;
        }
        value = default!;
        return false;
    }

    public bool TryGetValue(out Exception value)
    {
        if (_value is Exception e)
        {
            value = e;
            return true;
        }
        value = default!;
        return false;
    }
}

共用体メンバー プロバイダーは、共用体の型にプライベート コンストラクターが必要な場合や、作成ロジックでファクトリ パターンが必要な場合 ( record class 共用体型など) に便利です。

クラス ベースの共用体型

クラスは共用体型にすることもできます。 この種類の共用体は、参照セマンティクスまたは継承が必要な場合に便利です。

[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
    private readonly object? _value;

    public Result(T? value) { _value = value; }
    public Result(Exception? value) { _value = value; }

    public object? Value => _value;
}
static void ClassUnionExample()
{
    Result<string> ok = new Result<string>("success");
    Result<string> err = new Result<string>(new InvalidOperationException("failed"));

    Console.WriteLine(Describe(ok));  // output: OK: success
    Console.WriteLine(Describe(err)); // output: Error: failed

    static string Describe(Result<string> result) => result switch
    {
        string s => $"OK: {s}",
        Exception e => $"Error: {e.Message}",
        null => "null",
    };
}

クラス ベースの共用体の場合、 null パターンは null 参照と null Valueの両方に一致します。

C# 言語仕様

詳細については、 共用体 機能の仕様を参照してください。

こちらも参照ください