Bagikan melalui


Jenis union (referensi C#)

Jenis union mewakili nilai yang bisa menjadi salah satu dari beberapa jenis kasus. Serikat pekerja menyediakan konversi implisit dari setiap jenis kasus, pencocokan pola lengkap, dan pelacakan nullability yang ditingkatkan. union Gunakan kata kunci untuk mendeklarasikan jenis serikat:

public union Pet(Cat, Dog, Bird);

Deklarasi ini membuat serikat Pet dengan tiga jenis kasus: Cat, Dog, dan Bird. Anda dapat menetapkan nilai jenis kasus apa pun ke Pet variabel. Pengkompilasi memastikan bahwa switch ekspresi mencakup semua jenis kasus.

Referensi bahasa C# mendokumentasikan versi bahasa C# yang paling baru dirilis. Ini juga berisi dokumentasi awal untuk fitur dalam pratinjau publik untuk rilis bahasa yang akan datang.

Dokumentasi mengidentifikasi fitur apa pun yang pertama kali diperkenalkan dalam tiga versi terakhir bahasa atau dalam pratinjau publik saat ini.

Petunjuk / Saran

Untuk menemukan kapan fitur pertama kali diperkenalkan di C#, lihat artikel tentang riwayat versi bahasa C#.

Nyatakan serikat ketika nilai harus tepat salah satu dari sekumpulan jenis tetap dan Anda ingin pengkompilasi memberlakukan bahwa setiap kemungkinan ditangani. Skenario umum meliputi:

  • Hasil kesalahan: Metode mengembalikan nilai keberhasilan atau nilai kesalahan, dan pemanggil harus menangani keduanya. Serikat seperti union Result(Success, Error) membuat serangkaian hasil menjadi eksplisit.
  • Pengiriman pesan atau perintah: Sistem memproses sekumpulan jenis pesan tertutup. Gabungan memastikan jenis pesan baru menghasilkan peringatan waktu kompilasi di setiap switch yang belum menanganinya.
  • Mengganti antarmuka penanda atau kelas dasar abstrak: Jika Anda menggunakan antarmuka atau kelas abstrak semata-mata untuk jenis grup untuk pencocokan pola, serikat pekerja memberi Anda pemeriksaan kelelahan tanpa memerlukan warisan atau anggota bersama.

Serikat pekerja berbeda dari deklarasi jenis lain dengan cara penting:

  • class Tidak seperti atau struct, serikat tidak menentukan anggota data baru. Sebaliknya, ia menyusun jenis yang ada ke dalam sekumpulan alternatif tertutup.
  • interfaceTidak seperti , serikat tertutup—Anda menentukan daftar lengkap jenis kasus dalam deklarasi, dan pengkompilasi menggunakan daftar tersebut untuk pemeriksaan kelelahan.
  • recordTidak seperti , serikat tidak menambahkan perilaku kesetaraan, kloning, atau dekonstruksi. Serikat berfokus pada "kasus mana itu?" daripada "bidang apa yang dimilikinya?"

Penting

Di .NET 11 Pratinjau 2, runtime tidak menyertakan UnionAttribute antarmuka dan IUnion . Untuk menggunakan jenis serikat, Anda harus mendeklarasikannya sendiri. Untuk melihat deklarasi yang diperlukan, lihat Implementasi Serikat.

Deklarasi serikat

Deklarasi serikat kerja menentukan nama dan daftar jenis kasus:

public union Pet(Cat, Dog, Bird);

Jenis kasus dapat berupa jenis apa pun yang dikonversi ke object, termasuk kelas, struct, antarmuka, parameter jenis, jenis nullable, dan serikat lainnya. Contoh berikut menunjukkan kemungkinan jenis kasus yang berbeda:

public record class Cat(string Name);
public record class Dog(string Name);
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);

Ketika jenis kasus adalah jenis nilai (seperti int), nilai dikotak saat disimpan di properti gabungan Value . Serikat buruh menyimpan kontennya sebagai referensi tunggal object? .

Deklarasi serikat dapat mencakup isi dengan anggota tambahan, sama seperti struktur, tunduk pada beberapa batasan. Deklarasi serikat tidak dapat menyertakan bidang instans, properti otomatis, atau peristiwa seperti bidang. Anda juga tidak dapat mendeklarasikan konstruktor publik dengan satu parameter, karena kompilator menghasilkan konstruktor tersebut sebagai anggota pembuatan serikat pekerja:

public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        T single => [single],
        IEnumerable<T> multiple => multiple,
        _ => []
    };
}

Konversi serikat pekerja

Konversi serikat implisit ada dari setiap jenis kasus ke jenis serikat. Anda tidak perlu memanggil konstruktor secara eksplisit:

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

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

Konversi gabungan berfungsi dengan memanggil konstruktor yang dihasilkan yang sesuai. Jika operator konversi implisit yang ditentukan pengguna ada untuk jenis yang sama, operator yang ditentukan pengguna lebih diprioritaskan daripada konversi serikat pekerja. Untuk detail tentang prioritas konversi, lihat spesifikasi bahasa.

Konversi union ke union struct nullable (T?) juga berfungsi ketika T adalah jenis union:

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

    Console.WriteLine(Describe(maybePet)); // output: Dog: 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",
    };
}

Pencocokan union

Saat Anda mencocokkan pola pada jenis serikat, pola berlaku untuk properti serikat Value , bukan nilai gabungan itu sendiri. Perilaku "unwrapping" ini berarti penyatuan transparan terhadap pencocokan pola:

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

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

Dua pola adalah pengecualian untuk aturan ini: var pola dan pola buang _ berlaku untuk nilai gabungan itu sendiri, bukan propertinya Value . Gunakan var untuk mengambil nilai union saat GetPet() mengembalikan Pet? (Nullable<Pet>):

if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }

Dalam pola logis, setiap cabang mengikuti aturan pembungkusan satu per satu. Pola berikut menguji bahwa Pet? tidak null danValue bukan null:

GetPet() switch
{
    var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks Value
}

Nota

Karena pola berlaku untuk Value, pola seperti pet is Pet biasanya tidak cocok, Pet karena diuji terhadap konten serikat, bukan serikat itu sendiri.

Pencocokan null

Untuk penyatuan struct, null pola memeriksa apakah Value null:

static void NullHandling()
{
    Pet pet = default;
    Console.WriteLine(pet.Value is null); // output: True

    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
}

Untuk serikat berbasis kelas, null berhasil ketika referensi serikat itu sendiri null atau propertinya Value null:

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 */ }

Untuk jenis struct union nullable (Pet?), null berhasil ketika pembungkus nullable tidak memiliki nilai atau ketika union Value yang mendasar adalah null.

Kelelahan serikat

Ekspresi switch lengkap saat menangani semua jenis kasus serikat. Pengkompilasi memperingatkan hanya jika jenis kasus tidak ditangani. Anda tidak perlu menyertakan pola buang (_) atau var pola untuk mencocokkan jenis apa pun:

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

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

Jika status null properti serikat adalah Value "mungkin null", Anda juga harus menangani null untuk menghindari peringatan:

static void NullHandling()
{
    Pet pet = default;
    Console.WriteLine(pet.Value is null); // output: True

    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
}

Keterbatalan

Pengkompilasi melacak status null properti gabungan Value melalui aturan berikut:

  • Saat Anda membuat nilai union dari jenis kasus (melalui konstruktor atau konversi union), Value mendapatkan status null dari nilai masuk.
  • Saat pola HasValue akses non-tinju atau TryGetValue(...) anggota mengkueri konten serikat, status Value null menjadi "tidak null" pada true cabang.

Jenis serikat kustom

Kompilator mengonversi union deklarasi menjadi struct deklarasi. Struktur ditandai dengan [System.Runtime.CompilerServices.Union] atribut , mengimplementasikan IUnion antarmuka . Ini termasuk konstruktor publik dan konversi implisit untuk setiap jenis kasus bersama dengan Value properti. Bentuk yang dihasilkan dipendapatkan. Ini selalu merupakan struktur, selalu kotak kasus jenis nilai, dan selalu menyimpan konten sebagai object?.

Ketika Anda memerlukan perilaku yang berbeda - seperti serikat berbasis kelas, strategi penyimpanan kustom, dukungan interop, atau jika Anda ingin menyesuaikan jenis yang ada - Anda dapat membuat jenis serikat secara manual.

Kelas atau struktur apa pun dengan [Union] atribut adalah jenis gabungan jika mengikuti pola penyatuan dasar. Pola penyatuan dasar memerlukan:

  • Atribut [Union] pada jenis .
  • Satu atau beberapa konstruktor publik, masing-masing dengan satu menurut nilai atau in parameter. Jenis parameter setiap konstruktor mendefinisikan jenis kasus.
  • Properti publik Value berjenis object? (atau object) dengan get aksesor.

Semua anggota serikat harus publik. Kompilator menggunakan anggota ini untuk menerapkan konversi serikat pekerja, pencocokan pola, dan pemeriksaan kelelahan. Anda juga dapat menerapkan pola akses non-tinju atau membuat jenis serikat berbasis kelas.

Pengkompilasi mengasumsikan bahwa jenis serikat kustom memenuhi aturan perilaku ini:

  • Kedamaian: Value selalu mengembalikan null atau nilai dari salah satu jenis kasus - tidak pernah nilai dari jenis yang berbeda. Untuk penyatuan struct, default menghasilkan Value dari null.
  • Stabilitas: Jika Anda membuat nilai serikat dari jenis kasus, Value cocok dengan jenis kasus tersebut (atau apakah null inputnya adalah null).
  • Kesetaraan pembuatan: Jika nilai secara implisit dapat dikonversi ke dua jenis kasus yang berbeda, kedua anggota pembuatan menghasilkan perilaku yang dapat diamati yang sama.
  • Konsistensi pola akses: Anggota HasValue dan TryGetValue , jika ada, berperilaku setara dengan memeriksa Value secara langsung.

Contoh berikut menunjukkan jenis serikat kustom:

[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
}

Pola akses non-tinju

Jenis gabungan kustom dapat secara opsional menerapkan pola akses non-tinju untuk memungkinkan akses yang sangat ditik ke kasus jenis nilai tanpa tinju selama pencocokan pola. Pola ini memerlukan:

  • Properti HasValue jenis bool yang mengembalikan true jika Value bukan null.
  • Metode TryGetValue untuk setiap jenis kasus yang mengembalikan bool dan memberikan nilai melalui out parameter.
[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
}

Pengkompilasi lebih memilih TryGetValue daripada Value properti saat menerapkan pencocokan pola, yang menghindari jenis nilai tinju.

Jenis serikat berbasis kelas

Kelas juga dapat menjadi jenis serikat. Jenis penyatuan ini berguna ketika Anda memerlukan semantik referensi atau warisan:

[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",
    };
}

Untuk serikat berbasis kelas, null pola cocok dengan referensi null dan null Value.

Implementasi serikat

Atribut dan antarmuka berikut mendukung jenis gabungan pada waktu kompilasi dan runtime:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
    public sealed class UnionAttribute : Attribute;

    public interface IUnion
    {
        object? Value { get; }
    }
}

Deklarasi gabungan yang dihasilkan oleh kompilator mengimplementasikan IUnion. Anda dapat memeriksa nilai union apa pun saat runtime dengan menggunakan IUnion:

if (value is IUnion { Value: null }) { /* the union's value is null */ }

Ketika Anda mendeklarasikan union jenis, kompilator menghasilkan struct yang mengimplementasikan IUnion. Misalnya, Pet deklarasi (public union Pet(Cat, Dog, Bird);) menjadi setara dengan:

[Union] public struct Pet : IUnion
{
    public Pet(Cat value) => Value = value;
    public Pet(Dog value) => Value = value;
    public Pet(Bird value) => Value = value;
    public object? Value { get; }
}

Penting

Di .NET 11 Pratinjau 2, jenis ini tidak disertakan dalam runtime. Untuk menggunakan jenis serikat, Anda harus mendeklarasikannya dalam proyek Anda. Mereka akan disertakan dalam pratinjau .NET di masa mendatang.

Spesifikasi bahasa C#

Untuk informasi selengkapnya, lihat spesifikasi fitur Unions .

Baca juga