Compartilhar via


Tipos de união (referência de C#)

Um tipo de união representa um valor que pode ser um dos vários tipos de casos. As uniões fornecem conversões implícitas de cada tipo de caso, correspondência completa de padrões e controle de nulidade aprimorado. Use a union palavra-chave para declarar um tipo de união:

public union Pet(Cat, Dog, Bird);

Essa declaração cria uma Pet união com três tipos de casos: Cat, Doge Bird. Você pode atribuir qualquer valor de tipo de caso a uma Pet variável. O compilador garante que switch as expressões abrangem todos os tipos de maiúsculas e minúsculas.

A linguagem C# faz referência a documentos da versão mais recentemente lançada da linguagem C#. Ele também contém a documentação inicial para funcionalidades em pré-visualizações públicas para o próximo lançamento do idioma.

A documentação identifica qualquer recurso introduzido pela primeira vez nas três últimas versões do idioma ou nas versões prévias públicas atuais.

Dica

Para descobrir quando um recurso foi introduzido pela primeira vez em C#, consulte o artigo sobre o histórico de versão da linguagem C#.

Declare uma união quando um valor deve ser exatamente um de um conjunto fixo de tipos e você deseja que o compilador imponha que todas as possibilidades sejam tratadas. Cenários comuns incluem:

  • Retorna resultado ou erro: um método retorna um valor de êxito ou um valor de erro e o chamador deve manipular ambos. Uma união como union Result(Success, Error) torna o conjunto de resultados explícito.
  • Envio de mensagens ou comandos: um sistema processa um conjunto fechado de tipos de mensagens. Uma união garante que novos tipos de mensagens produzam avisos de tempo de compilação em todos os switch que ainda não os manipulam.
  • Substituindo interfaces de marcador ou classes base abstratas: se você usar uma interface ou classe abstrata apenas para agrupar tipos para correspondência de padrões, uma união fornecerá verificação de esgotamento sem a necessidade de herança ou membros compartilhados.

Uma união difere de outras declarações de tipo de maneiras importantes:

  • Ao contrário de um class ou struct, um sindicato não define novos membros de dados. Em vez disso, ele compõe os tipos existentes em um conjunto fechado de alternativas.
  • Ao contrário de uma interfaceunião fechada, você define a lista completa de tipos de casos na declaração e o compilador usa essa lista para verificações de esgotamento.
  • Ao contrário de um record, uma união não adiciona igualdade, clonagem ou comportamento de desconstrução. Um sindicato se concentra em "que caso é?" em vez de "que campos ele tem?"

Importante

No .NET 11 Versão Prévia 2, o runtime não inclui o e IUnion a UnionAttribute interface. Para usar tipos de união, você mesmo deve declará-los. Para ver as declarações necessárias, consulte a implementação da União.

Declarações de união

Uma declaração de união especifica um nome e uma lista de tipos de caso:

public union Pet(Cat, Dog, Bird);

Os tipos de caso podem ser qualquer tipo que se converte object, incluindo classes, structs, interfaces, parâmetros de tipo, tipos anuláveis e outras uniões. Os exemplos a seguir mostram diferentes possibilidades de tipo de caso:

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);

Quando um tipo de maiúsculas e minúsculas é um tipo de valor (como int), o valor é encaixotado quando armazenado na propriedade da Value união. Os sindicatos armazenam seu conteúdo como uma única object? referência.

Uma declaração sindical pode incluir um corpo com membros adicionais, assim como um struct, sujeito a algumas restrições. As declarações de união não podem incluir campos de instância, propriedades automáticas ou eventos semelhantes a um campo. Você também não pode declarar construtores públicos com um único parâmetro, pois o compilador gera esses construtores como membros de criação de união:

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

Conversões de união

Existe uma conversão de união implícita de cada tipo de caso para o tipo de união. Você não precisa chamar um construtor explicitamente:

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

As conversões de união funcionam chamando o construtor gerado correspondente. Se existir um operador de conversão implícita definido pelo usuário para o mesmo tipo, o operador definido pelo usuário tem prioridade sobre a conversão de união. Para obter detalhes sobre a prioridade de conversão, consulte a especificação do idioma.

Uma conversão de união em um struct de união anulável (T?) também funciona quando T é um tipo de união:

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

Correspondência de união

Quando você faz a correspondência padrão em um tipo de união, os padrões se aplicam à propriedade da Value união, não ao próprio valor da união. Esse comportamento de "desembrulhamento" significa que a união é transparente para correspondência de padrões:

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
}

Dois padrões são exceções a essa regra: o var padrão e o padrão de descarte _ se aplicam ao próprio valor da união, não à sua Value propriedade. Use var para capturar o valor da união quando GetPet() retornar um Pet? (Nullable<Pet>):

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

Em padrões lógicos, cada branch segue a regra de desembrulhamento individualmente. O padrão a seguir testa que o Pet? valor não é nulo eValue não é nulo:

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

Observação

Como os padrões se aplicam, Valueum padrão como pet is Pet normalmente não corresponde, uma vez que Pet é testado em relação ao conteúdo da união, não à união em si.

Correspondência nula

Para uniões struct, o null padrão verifica se Value é nulo:

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
}

Para uniões baseadas em classe, null é bem-sucedida quando a própria referência sindical é nula ou sua Value propriedade é nula:

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

Para tipos de struct união anuláveis (Pet?), null é bem-sucedido quando o wrapper anulável não tem nenhum valor ou quando a união Value subjacente é nula.

Esgotamento da união

Uma switch expressão é exaustiva quando manipula todos os tipos de maiúsculas e minúsculas de uma união. O compilador avisará somente se um tipo de caso não for tratado. Você não precisa incluir um padrão de descarte (_) ou var um padrão para corresponder a qualquer tipo:

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
}

Se o estado nulo da propriedade do Value sindicato for "talvez nulo", você também deverá lidar null para evitar um aviso:

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
}

Nulidade

O compilador rastreia o estado nulo da propriedade de Value uma união por meio das seguintes regras:

  • Quando você cria um valor de união de um tipo de caso (por meio de um construtor ou conversão de união), Value obtém o estado nulo do valor de entrada.
  • Quando os membros ou TryGetValue(...) padrões de acesso não HasValue boxing consultam o conteúdo do sindicato, o estado nulo se Value torna "não nulo" no true branch.

Tipos de união personalizados

O compilador converte uma union declaração em uma struct declaração. O struct é marcado com o [System.Runtime.CompilerServices.Union] atributo, implementa a IUnion interface. Ele inclui um construtor público e uma conversão implícita para cada tipo de caso, juntamente com uma Value propriedade. Esse formulário gerado é opinativo. É sempre um struct, sempre caixas maiúsculas e minúsculas de tipo de valor e sempre armazena conteúdo como object?.

Quando você precisa de um comportamento diferente - como uma união baseada em classe, uma estratégia de armazenamento personalizada, suporte de interoperabilidade ou se quiser adaptar um tipo existente - você pode criar um tipo de união manualmente.

Qualquer classe ou struct com um [Union] atributo será um tipo de união se ele seguir o padrão de união básico. O padrão de união básico requer:

  • Um [Union] atributo no tipo.
  • Um ou mais construtores públicos, cada um com um único por valor ou in parâmetro. O tipo de parâmetro de cada construtor define um tipo de caso.
  • Uma propriedade pública Value do tipo object? (ou object) com um get acessador.

Todos os membros do sindicato devem ser públicos. O compilador usa esses membros para implementar conversões de união, correspondência de padrões e verificações de esgotamento. Você também pode implementar o padrão de acesso não boxing ou criar um tipo de união baseado em classe.

O compilador pressupõe que os tipos de união personalizados atendem a estas regras comportamentais:

  • Solidez: Value sempre retorna null ou um valor de um dos tipos de maiúsculas e minúsculas - nunca um valor de um tipo diferente. Para uniões struct, default produz um Value de null.
  • Estabilidade: se você criar um valor de união de um tipo de caso, Value corresponderá a esse tipo de caso (ou se null a entrada for null).
  • Equivalência de criação: se um valor for implicitamente conversível para dois tipos de maiúsculas e minúsculas diferentes, ambos os membros de criação produzirão o mesmo comportamento observável.
  • Consistência de padrão de acesso: os HasValue membros e TryGetValue , se presentes, se comportam de forma equivalente à verificação Value direta.

O exemplo a seguir mostra um tipo de união personalizado:

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

Padrão de acesso não boxing

Um tipo de união personalizado pode, opcionalmente, implementar o padrão de acesso não boxing para habilitar o acesso fortemente tipado a casos de tipo de valor sem boxing durante a correspondência de padrões. Esse padrão requer:

  • Uma HasValue propriedade do tipo bool que retorna true quando Value não nullé .
  • Um TryGetValue método para cada tipo de caso que retorna bool e entrega o valor por meio de um out parâmetro.
[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
}

O compilador prefere TryGetValue a propriedade ao implementar a Value correspondência de padrões, o que evita tipos de valor boxing.

Tipos de união baseados em classe

Uma classe também pode ser um tipo de união. Esse tipo de união é útil quando você precisa de semântica de referência ou herança:

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

Para uniões baseadas em classe, o null padrão corresponde a uma referência nula e a um valor nulo Value.

Implementação da união

O atributo e a interface a seguir dão suporte a tipos de união em tempo de compilação e runtime:

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

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

Declarações de união geradas pelo compilador implementam IUnion. Você pode verificar se há qualquer valor de união em runtime usando IUnion:

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

Quando você declara um union tipo, o compilador gera um struct que implementa IUnion. Por exemplo, a Pet declaração (public union Pet(Cat, Dog, Bird);) torna-se equivalente a:

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

Importante

No .NET 11 Versão Prévia 2, esses tipos não são incluídos no runtime. Para usar tipos de união, você deve declará-los em seu projeto. Eles serão incluídos em uma versão prévia futura do .NET.

Especificação da linguagem C#

Para obter mais informações, consulte a especificação do recurso Unions .

Consulte também