Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Um tipo de união representa um valor que pode ser um de vários tipos de casos. As uniões fornecem conversões implícitas de cada tipo de caso, correspondência exaustiva de padrões e acompanhamento melhorado da nulidade. Use a union palavra-chave para declarar um tipo de união:
public union Pet(Cat, Dog, Bird);
Esta declaração cria uma Pet união com três tipos de casos: Cat, Dog, e Bird. Pode atribuir qualquer valor de tipo de caso a uma Pet variável. O compilador assegura que switch as expressões cobrem todos os tipos de casos.
A referência da linguagem C# documenta a versão mais recentemente lançada da linguagem C#. Contém também documentação inicial para funcionalidades em versões preliminares públicas para a próxima versão da linguagem.
A documentação identifica qualquer funcionalidade introduzida pela primeira vez nas últimas três versões da língua ou em pré-visualizações públicas atuais.
Sugestão
Para saber quando uma funcionalidade foi introduzida pela primeira vez em C#, consulte o artigo sobre o histórico de versões da linguagem C#.
Declara uma união quando um valor tem de ser exatamente um de um conjunto fixo de tipos e queres que o compilador impeça que todas as possibilidades sejam tratadas. Cenários comuns incluem:
-
Resultados ou erros devolvem: Um método devolve ou um valor de sucesso ou um valor de erro, e o chamador deve lidar com ambos. Uma união como
union Result(Success, Error)torna explícito o conjunto de resultados. -
Despacho de mensagens ou comandos: Um sistema processa um conjunto fechado de tipos de mensagens. Uma união garante que novos tipos de mensagens produzem avisos em tempo de compilação em todos os
switchque ainda não os tratam. - Substituição de interfaces de marcadores ou classes base abstratas: Se usar uma interface ou classe abstrata apenas para agrupar tipos para correspondência de padrões, uma união oferece uma verificação de exaustividade sem exigir herança ou membros partilhados.
Uma união difere de outros tipos de declarações em aspetos importantes:
- Ao contrário de um
classoustruct, um sindicato não define novos membros de dados. Em vez disso, compõe tipos existentes num conjunto fechado de alternativas. - Ao contrário de ,
interfaceuma união é fechada — defines a lista completa de tipos de casos na declaração, e o compilador usa essa lista para verificações de exaustividade. - Ao contrário de ,
recorduma união não acrescenta igualdade, clonagem ou comportamento de desconstrução. Um sindicato foca-se em "qual é o caso?" em vez de "que áreas tem?"
Os tipos de união dependem dos UnionAttribute tipos e IUnion no System.Runtime.CompilerServices espaço de nomes. O runtime do .NET 11 inclui estes tipos.
Declarações sindicais
Uma declaração sindical especifica um nome e uma lista de tipos de casos:
public union Pet(Cat, Dog, Bird);
Os tipos de caso podem ser qualquer tipo que converta em object, incluindo classes, structs, interfaces, parâmetros de tipo, tipos anuláveis e outras uniões. Os exemplos seguintes 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 encaixa o valor e armazena-o como uma única object? referência.
Os tipos de união personalizada podem usar uma estratégia de armazenamento diferente.
Uma declaração sindical pode incluir um órgão com membros adicionais, tal como um struct, sujeito a algumas restrições. As declarações de união não podem incluir campos de instância, auto-propriedades ou eventos semelhantes a campos. Também não podes declarar construtores públicos com um único parâmetro, porque o compilador gera esses construtores como membros de criação sindical. Qualquer construtor que declare deve usar um this(...) inicializador para delegar, direta ou indiretamente, a um construtor gerado. A união seguinte Length adiciona uma TotalMeters propriedade que usa correspondência de padrões para tratar todos os tipos 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 da União
Existe uma conversão implícita de união 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 utilizador para o mesmo tipo, o operador definido pelo utilizador tem prioridade sobre a conversão de união. Se mais do que um tipo de caso for igualmente aplicável ao valor fonte, a conversão da união é ambígua, e o compilador reporta um erro. Para detalhes sobre a prioridade de conversão, consulte a especificação da funcionalidade.
Uma conversão de união para uma estrutura de união nula (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 Union
Quando se usa um padrão de tipo ou declaração numa expressão union, o padrão testa o conteúdo da union, não a instância union. 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
}
A forma do padrão determina qual o valor que testa:
| Padrão | Comportamento numa expressão de união |
|---|---|
Padrão de tipo ou 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, depois aplica os padrões aninhados a esse valor de caso. |
Propriedade ou padrão posicional sem um tipo, como { Name: "Rex" } |
Utiliza as propriedades ou Deconstruct método da instância de união. |
| Padrão de lista | Utiliza a contagem e indexação dos membros da instância sindical, não os do seu conteúdo. |
varou padrão de descarte _ |
Captura ou corresponde à instância de união que entra sem a desembrulhar. |
| Padrão constante ou relacional | Testa o conteúdo. O null padrão também trata uma referência de união nula ou um wrapper anulável. |
Por exemplo, defina uma Pet união com uma propriedade de 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" } digitado testa o conteúdo e lê o do Breedcão, uma propriedade que a união não possui. O padrão { Name: "Rex" } não tipado lê a propriedade da Name união, pelo que 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 anula o seu padrão aninhado. Em pet is not null, o padrão aninhado null verifica o conteúdo, pelo que not null é um teste útil de ter um valor. Em contraste, um padrão vazio de propriedades não tipadas, { }, verifica a instância de união. Corresponde sempre 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
}
Os padrões lógicos preservam estas distinções. Em var pet and not null, var pet captura a união incidente e not null testa se esta tem conteúdos não nulos. Em not null and var pet, o var padrão ainda capta a união recebida: not não altera o valor passado para o padrão da direita. Em contraste, em Dog and { Breed: "Labrador" }, o padrão tipo passa o emparelhado Dog para o padrão de propriedade.
Observação
Um padrão de tipo deve ser compatível com pelo menos um tipo de caixa. Para as Pet declarações deste artigo, pet is Pet produz o erro do compilador CS8121: nenhum dos seus tipos de casos pode corresponder Peta . Não é um teste de execução da instância unionista. Para verificar o conteúdo, use pet is not null ou pet.Value is not null; para captar a própria união, use um var padrão.
Em suma, 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 tipo usam os próprios membros do sindicato. Estas regras são as mesmas numa is expressão e numa switch expressão.
Correspondência nula
Para uniões estruturas, 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 classes, null tem sucesso quando a própria referência à união é nula ou a 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 de união nula (Pet?), null tem sucesso quando o wrapper anulável não tem valor ou quando o da união Value subjacente é nulo.
Exaustividade sindical
Uma switch expressão é exaustiva sobre os tipos de caso de uma união quando esta trata cada tipo de caso. Se a análise de anulabilidade também considerar o conteúdo como não nulo, não precisa de descarte (_) ou 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
}
Para lidar com conteúdos nulos, prefira um null braço em vez de var um braço de descarte (_). O null padrão indica a sua intenção e preserva os avisos quando adiciona um novo tipo de estojo sem um braço correspondente. O arm A var ou descartar também aceita qualquer novo caso, por isso o compilador não pode avisar desse caso em falta.
Quando o estado nulo rastreado de Value é "talvez nulo", o compilador também avisa se omitir um null arm. Por exemplo, NullablePet tem um tipo de caso anulável, pelo que o conteúdo do seu 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 braço null deste interruptor produz o aviso CS8655.
A análise nullable não garante que uma união gerada tenha conteúdos não nulos em tempo de execução. Uma união por defeito contém null, incluindo quando é um campo ou elemento de array inicializado por defeito. Se todos os tipos de caso forem não anuláveis, o estado rastreado por defeito de Value é, no entanto, "não nulo", pelo que o compilador não avisa necessariamente sobre um arm omitido null . Controlar null a correção sempre que o conteúdo padrão puder chegar ao seu switch:
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
}
Neste Pet exemplo, remover o null braço não produz aviso de braço nulo em falta, a menos que um código anterior altere o estado nulo rastreado, mas o interruptor aciona SwitchExpressionException quando recebe o valor padrão. Um teste anterior is null pode alterar o estado rastreado e causar um aviso. Este comportamento é separado da atribuição definida: o compilador rejeita a leitura de uma variável local não atribuída.
Anulabilidade
O compilador acompanha o estado nulo da Value propriedade de uma união através das seguintes regras:
- O estado nulo padrão da
Valuepropriedade de uma união é "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 crias um valor de união a partir de um tipo de caso (através de um construtor ou conversão de união),
Valueobtém-se o estado nulo do valor recebido. - Quando os padrões
HasValuede acesso não-boxing ouTryGetValue(...)membros consultam o conteúdo da união, o estado nulo deValuetorna-se "não nulo" natruerama.
Tipos de união personalizada
O compilador converte uma union declaração numa struct declaração. A estrutura é marcada com o [System.Runtime.CompilerServices.Union] atributo e implementa a IUnion interface. Inclui um construtor público para cada tipo de caso e uma Value propriedade. A linguagem fornece conversões de união implícitas que chamam esses construtores. Essa forma gerada é opinativa. É sempre um struct, sempre caixas do tipo value, e armazena sempre o conteúdo como object?.
Pode precisar de um comportamento diferente se quiser adaptar um tipo existente, criar uma união baseada em classes, ou usar uma estratégia de armazenamento personalizada, ou se precisar de suporte de interoperabilidade. Pode criar manualmente um tipo de sindicato.
Qualquer classe ou struct com um [Union] atributo é um tipo de união se seguir o padrão básico de união. O padrão básico de união exige:
- Um
[Union]atributo do tipo. - Um ou mais construtores públicos, cada um com um único 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 umgetacessório.
Todos os membros anteriores do sindicato devem ser públicos. O compilador utiliza estes membros para implementar conversões de união, correspondência de padrões e verificações de exaustividade. Também pode implementar o padrão de acesso não-boxing ou criar um tipo de união baseado em classes. O seu tipo de união personalizado pode adicionar mais membros.
O compilador assume que os tipos de união personalizados satisfaz estas regras comportamentais:
-
Solidez:
Valuedevolvenullsempre um valor de um dos tipos de casos, incluindo para um valor padrão de estrutura. Uma união personalizada pode representar o seu valor padrão como um caso não nulo, ao contrário do conteúdo nulo de uma união gerada por defeito. -
Estabilidade: Se criar um valor de união a partir de um tipo de caso,
Valuecorresponde a esse tipo de caso (ou énullse a entrada foinull). - Equivalência de criação: Se um valor for implicitamente convertível para dois tipos de casos diferentes, ambos os membros da criação produzem o mesmo comportamento observável.
-
Consistência do padrão de acesso: Os
HasValuemembros andTryGetValue, se presentes, comportam-se de forma equivalente à verificaçãoValuedireta.
O exemplo seguinte 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 sem boxing
Um tipo de união personalizado pode opcionalmente implementar o padrão de acesso não-boxing para permitir o acesso fortemente tipado a casos do tipo valor sem boxing durante o pattern matching. Este padrão exige:
- Uma
HasValuepropriedade de tipoboolque devolvetruequandoValuenãonullé . - Um
TryGetValuemétodo para cada tipo de caso que devolveboole entrega o valor através de umoutparâmetro.TryGetValueretornatrueapenas quandoValueé um valor não nulo desse tipo de caso. Oouttipo do parâmetro é convertível em identidade para o tipo de caso, ou para o tipo de valor subjacente quando o tipo de caso é um tipo de valor nulo.
[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 exige TryGetValue verificações de tipo padrão (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 — nenhum é um plano B para o outro. Quando falta um membro, o compilador verifica a Value propriedade para esse padrão em vez disso. Para mais informações, veja Como o compilador gera código para correspondência de padrões.
Fornecedores membros do sindicato
Um tipo de sindicato pode delegar os seus membros sindicais a uma interface pública aninhada IUnionMembers que contém e implementa diretamente. Essa interface é o fornecedor dos membros do sindicato. O compilador encontra membros de sindicato apenas na interface do fornecedor, incluindo membros herdados. Os membros declarados apenas pelo tipo de sindicato em si não são usados como membros do sindicato.
Ao utilizar um fornecedor membro sindical, a interface deve declarar estes membros obrigatórios:
-
Métodos estáticos
Create: Um para cada tipo de caso, com um único parâmetro e um tipo de retorno convertível para o tipo union. Estes métodos definem os seus tipos de casos. -
ValuePropriedade: Um públicoobjectouobject?propriedade com umgetacessório que devolve o valor contido.
Pode, opcionalmente, declarar estes membros na interface para que o compilador gere 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 umgetacessório que retornatruequandoValuenãonullé .
Como o compilador gera código para correspondência de padrões: TryGetValue e HasValue aplicam a diferentes tipos de padrões — nenhum é um recurso de recurso para o outro:
- Para um padrão de tipos, como
union is T, o compilador chamaTryGetValue(out T value)quando declarado, extraindo o valor e aplicando o padrão sem boxing. Caso contrário, o compilador aplica o padrão àValuepropriedade, que realiza uma verificaçãoTde tipo em tempo de execução e pode bloquear o valor quandoTé 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 aplica o padrão nulo diretamente àValuepropriedade.
Cada membro é opcional de forma independente: sem TryGetValue, os padrões de tipo usam Value; sem HasValue, padrões nulos usam Value. Declarar qualquer um dos membros permite ao compilador gerar 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 fornecedores de membros de sindicato são úteis quando o tipo de união necessita de um construtor privado ou quando a lógica de criação requer um padrão de fábrica, como acontece com record class os tipos de união.
Tipos de união baseados em classes
Uma classe pode também ser um tipo union. Este tipo de união é útil quando 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 classes, o null padrão corresponde tanto a uma referência nula como a um nulo Value.
Especificação da linguagem C#
Para mais informações, consulte a especificação de funcionalidades Unions .