Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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
switchque 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
classoustruct, 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?"
Os tipos de união dependem dos UnionAttribute tipos e IUnion do System.Runtime.CompilerServices namespace. O runtime do .NET 11 inclui esses tipos.
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, 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);
Quando um tipo de caso é um tipo de valor (como int), uma declaração de união armazena o valor e o armazena como uma única object? referência.
Os tipos de união personalizados podem usar uma estratégia de armazenamento diferente.
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. Qualquer construtor que você declarar deve usar um this(...) inicializador para delegar, direta ou indiretamente, a um construtor gerado. A união a seguir Length adiciona uma TotalMeters propriedade que usa a correspondência de padrões para lidar com cada tipo de caso, juntamente com um Add método que combina dois comprimentos:
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);
}
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:
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 }
}
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. Se mais de um tipo de caso for igualmente aplicável ao valor de origem, a conversão da união será ambígua e o compilador relatará um erro. Para obter detalhes sobre a prioridade de conversão, consulte a especificação do recurso.
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", "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",
};
}
Correspondência de padrões de união
Quando você usa um tipo ou um padrão de declaração em uma expressão união, o padrão testa o conteúdo da união, não a instância da união. Por exemplo, um Dog padrão testa se Value é um 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
}
O formulário do padrão determina qual valor ele testa:
| Pattern | Comportamento em uma expressão de união |
|---|---|
Tipo ou padrão de declaração, como Dog ou Dog dog |
Testa o conteúdo em relação ao tipo especificado. |
Propriedade ou padrão posicional com um tipo, como Dog { Breed: "Labrador" } |
Testa o conteúdo em relação ao tipo e aplica os padrões aninhados a esse valor de caso. |
Propriedade ou padrão posicional sem um tipo, como { Name: "Rex" } |
Usa as propriedades ou Deconstruct o método da instância da união. |
| Padrão de lista | Usa a contagem da instância da união e os membros de indexação, não os de seu conteúdo. |
var ou descartar _ padrão |
Captura ou corresponde à instância de união de entrada sem desembrulhá-la. |
| Padrão constante ou relacional | Testa o conteúdo. O null padrão também manipula uma referência de união nula ou um wrapper anulável. |
Por exemplo, defina uma Pet união com uma propriedade somente Name leitura que delega ao contido Cat ou Dog. Os padrões de tipo na expressão da switch propriedade testam o conteúdo:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
O padrão Dog { Breed: "Labrador" } tipado testa o conteúdo e lê o do Breedcão, uma propriedade que a união não tem. O padrão { Name: "Rex" } não tipado lê a propriedade da Name união, portanto, também corresponde a um gato chamado 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
O not padrão nega seu padrão aninhado. Em pet is not null, o padrão aninhado null verifica o conteúdo, portanto not null , é um teste útil de has-a-value. Por outro lado, um padrão { }de propriedade sem tipo vazio verifica a instância da união. Ele sempre corresponde a uma união de struct não anulável, mesmo quando Value é nulo:
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
}
Padrões lógicos preservam essas distinções. Em var pet and not null, var pet captura a união de entrada e not null testa se ela tem conteúdo não nulo. Em not null and var pet, o var padrão ainda captura a união de entrada: not não altera o valor passado para o padrão à direita. Por outro lado, em Dog and { Breed: "Labrador" }, o padrão de tipo passa o correspondente Dog para o padrão de propriedade.
Observação
Um padrão de tipo deve ser compatível com pelo menos um tipo de caso. Para as declarações deste artigo, produz o Pet erro do compilador CS8121: nenhum dos tipos de caso pode corresponderPet. pet is Pet Não é um teste de runtime da instância da união. Para verificar se há conteúdo, usar pet is not null ou pet.Value is not null; para capturar a união em si, use um var padrão.
Em suma, os padrões de tipo e declaração testam o conteúdo. Padrões recursivos com um tipo também testam o conteúdo; padrões recursivos sem um tipo usam os próprios membros do sindicato. Essas regras são as mesmas em uma is expressão e uma switch expressão.
Correspondência nula
Para uniões struct, o null padrão verifica se Value é nulo:
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
}
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 sobre os tipos de maiúsculas e minúsculas de uma união quando manipula cada tipo de caso. Se a análise de nulidade também considerar o conteúdo não nulo, você não precisará de um descarte (_) ou var um braço:
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
}
Para lidar com conteúdo nulo, prefira um null braço em vez de um var braço ou descarte (_). O null padrão declara sua intenção e preserva avisos quando você adiciona um novo tipo de caso sem um braço correspondente. Um var braço ou descarte também aceita qualquer caso novo, portanto, o compilador não pode avisá-lo sobre esse caso ausente.
Quando o estado Value nulo rastreado for "talvez nulo", o compilador também avisará se você omitir um null braço. Por exemplo, NullablePet tem um tipo de caso anulável, portanto, o conteúdo do parâmetro pode ser nulo:
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",
};
Remover o null braço dessa opção produz um aviso CS8655.
A análise anulável não garante que uma união gerada tenha conteúdo não nulo em runtime. Uma união padrão contém nulo, incluindo quando se trata de um campo ou elemento de matriz inicializado por padrão. Se todos os tipos de casos não forem anuláveis, o estado Value padrão acompanhado será , no entanto, "não nulo", portanto, o compilador não necessariamente alertará sobre um braço omitido null . Manipule null a correção sempre que o conteúdo padrão puder acessar a opção:
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
}
Para este Pet exemplo, a remoção do null braço não produz nenhum aviso de braço nulo ausente, a menos que o código anterior altere o estado nulo rastreado, mas o comutador é gerado SwitchExpressionException quando recebe o valor padrão. Um teste anterior is null pode alterar o estado controlado e causar um aviso. Esse comportamento é separado da atribuição definitiva: o compilador rejeita uma leitura de uma variável local não atribuída.
Nulidade
O compilador rastreia o estado nulo da propriedade de Value uma união por meio das seguintes regras:
- O estado nulo padrão da propriedade de
Valueuma união será "talvez nulo" se o estado nulo padrão de qualquer tipo de caso for "talvez nulo". Caso contrário, o estado nulo padrão "não é nulo". - Quando você cria um valor de união de um tipo de caso (por meio de um construtor ou conversão de união),
Valueobtém o estado nulo do valor de entrada. - Quando os membros ou
HasValuepadrões de acesso nãoTryGetValue(...)boxing consultam o conteúdo do sindicato, o estado nulo seValuetorna "não nulo" notruebranch.
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 e implementa a IUnion interface. Ele inclui um construtor público para cada tipo de caso e uma Value propriedade. O idioma fornece conversões de união implícitas que chamam esses construtores. 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?.
Talvez você precise de um comportamento diferente se quiser adaptar um tipo existente, criar uma união baseada em classe ou usar uma estratégia de armazenamento personalizada ou se precisar de suporte de interoperabilidade. 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
inparâmetro. O tipo de parâmetro de cada construtor define um tipo de caso. - Uma propriedade pública
Valuedo tipoobject?(ouobject) com umgetacessador.
Todos os membros do sindicato anteriores 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. Seu tipo de união personalizado pode adicionar mais membros.
O compilador pressupõe que os tipos de união personalizados atendem a estas regras comportamentais:
-
Solidez:
Valuesempre retornanullou um valor de um dos tipos de maiúsculas e minúsculas, inclusive para um valor de struct padrão. Uma união personalizada pode representar seu valor padrão como um caso não nulo, ao contrário do conteúdo nulo de uma união gerada padrão. -
Estabilidade: se você criar um valor de união de um tipo de caso,
Valuecorresponderá a esse tipo de caso (ou senulla entrada fornull). - 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
HasValuemembros eTryGetValue, se presentes, se comportam de forma equivalente à verificaçãoValuedireta.
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
HasValuepropriedade do tipoboolque retornatruequandoValuenãonullé . - Um
TryGetValuemétodo para cada tipo de caso que retornaboole entrega o valor por meio de umoutparâmetro.TryGetValueretornatruesomente quandoValueé um valor não nulo desse tipo de caso. Oouttipo do parâmetro é conversível de identidade para o tipo de caso ou para o tipo de valor subjacente quando o tipo de caso é um tipo de valor anulável.
[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
}
Para correspondência de padrões, o compilador solicita TryGetValue verificações de padrão de tipo (como union is T) e HasValue para verificações de padrão nulo (como union is null), evitando o boxing para casos de tipo de valor. Cada membro aplica-se ao seu próprio tipo de padrão, nem é um fallback para o outro. Quando um membro está ausente, o compilador verifica a Value propriedade para esse padrão. Para obter mais informações, consulte Como o compilador gera código para correspondência de padrões.
Provedores de membros do sindicato
Um tipo de união pode delegar seus membros do sindicato a uma interface aninhada IUnionMembers pública que ele contém e implementa diretamente. Essa interface é o provedor de membros do sindicato. O compilador localiza os membros do sindicato apenas na interface do provedor, incluindo membros herdados. Os membros declarados apenas no próprio tipo sindical não são usados como membros do sindicato.
Ao usar um provedor de membros do sindicato, a interface deve declarar estes membros necessários:
-
Métodos estáticos
Create: um para cada tipo de caso, com um único parâmetro e um tipo de retorno conversível de identidade para o tipo de união. Esses métodos estabelecem seus tipos de caso. -
Valuepropriedade: um públicoobjectouobject?uma propriedade com umgetacessador que retorna o valor contido.
Opcionalmente, você pode declarar esses membros na interface para que o compilador gere um código de correspondência de padrões mais eficiente:
-
TryGetValuemétodos: um para cada tipo de caso, retornandoboolcom umoutparâmetro do tipo de caso. -
HasValuepropriedade: uma propriedade públicaboolcom umgetacessador retornandotruequandoValuenãonullé .
Como o compilador gera código para correspondência de padrões: TryGetValue e HasValue se aplica a diferentes tipos de padrões, nenhum deles é um fallback para o outro:
- Para um padrão de tipo, como
union is T, o compilador chamaTryGetValue(out T value)quando declarado, extraindo o valor e aplicando o padrão a ele sem boxing. Caso contrário, o compilador aplica o padrão àValuepropriedade, que executa uma verificação deTtipo de runtime e pode marcar o valor quandoTfor um tipo de valor. - Para um padrão nulo, como
union is null, o compilador chamaHasValuequando declarado para testar se a união contém um valor. Caso contrário, o compilador aplicará o padrão nulo àValuepropriedade diretamente.
Cada membro é opcional de forma independente: sem TryGetValue, os padrões de tipo usam Value; sem HasValue, os padrões nulos usam Value. Declarar um dos membros permite que o compilador gere um código mais eficiente e fortemente tipado para esse tipo de padrão:
[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;
}
}
Os provedores membros da união são úteis quando o tipo de união precisa de um construtor privado ou quando a lógica de criação requer um padrão de fábrica, como com record class tipos de união.
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.
Especificação da linguagem C#
Para obter mais informações, consulte a especificação do recurso Unions .