Este post foi traduzido automaticamente. Para a versão original, clique aqui.
Essa é a que eu estava esperando. C# 15 introduz a palavra-chave union — uniões discriminadas de verdade com pattern matching exaustivo imposto pelo compilador. Se você já invejou as uniões discriminadas do F# ou os enums do Rust, sabe exatamente por que isso importa.
Bill Wagner publicou a análise detalhada no blog do .NET, e sinceramente? O design é limpo, prático e muito C#. Deixa eu te mostrar o que realmente tem aqui e por que é mais importante do que parece à primeira vista.
O problema que as uniões resolvem
Antes do C# 15, retornar “um dentre vários tipos possíveis” de um método era sempre um compromisso:
object— sem restrições, sem ajuda do compilador, casting defensivo por toda parte- Interfaces marcadoras — melhor, mas qualquer um pode implementá-las. O compilador nunca pode considerar o conjunto completo
- Classes base abstratas — mesmo problema, além dos tipos precisarem de um ancestral comum
Nenhuma dessas opções te dá o que você realmente quer: um conjunto fechado de tipos onde o compilador garante que você tratou todos os casos. É isso que os tipos union fazem.
A sintaxe é lindamente simples
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);
Uma linha. Pet pode conter um Cat, um Dog ou um Bird. Conversões implícitas são geradas automaticamente:
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // Dog { Name = Rex }
E aqui está a mágica — o compilador impõe o matching exaustivo:
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Sem necessidade de discard _. O compilador sabe que esse switch cobre todos os casos possíveis. Se você depois adicionar um quarto tipo à union, cada expressão switch que não o trata produz um aviso. Casos faltantes detectados no tempo de compilação, não em tempo de execução.
Onde isso se torna prático
O exemplo do Pet é bonitinho, mas é aqui que as uniões realmente brilham em código real.
Respostas de API que retornam formas diferentes
public union ApiResult<T>(T, ApiError, ValidationFailure);
Agora todo consumidor é obrigado a tratar sucesso, erro e falha de validação. Chega de bugs de “esqueci de verificar o caso de erro”.
Valor único ou coleção
O padrão OneOrMore<T> mostra como as uniões podem ter um corpo com métodos auxiliares:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}
Os chamadores passam a forma que for mais conveniente:
OneOrMore<string> tags = "dotnet";
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
foreach (var tag in tags.AsEnumerable())
Console.Write($"[{tag}] ");
// [dotnet]
Compor tipos não relacionados
Essa é a funcionalidade matadora em relação às hierarquias tradicionais. Você pode unir tipos que não têm nada em comum — string e Exception, int e IEnumerable<T>. Sem necessidade de ancestral comum.
Uniões personalizadas para bibliotecas existentes
Aqui vai uma decisão de design inteligente: qualquer classe ou struct com um atributo [Union] é reconhecida como um tipo union, desde que siga o padrão básico (construtores públicos para os tipos de caso e uma propriedade Value). Bibliotecas como OneOf que já fornecem tipos similares a uniões podem optar pelo suporte do compilador sem reescrever seus internos.
Para cenários sensíveis a performance com tipos de valor, bibliotecas podem implementar um padrão de acesso sem boxing com os métodos HasValue e TryGetValue.
O panorama geral
Os tipos union fazem parte de uma história mais ampla de exaustividade chegando ao C#:
- Tipos union — matching exaustivo sobre um conjunto fechado de tipos (disponível agora em preview)
- Hierarquias fechadas — o modificador
closedimpede classes derivadas fora do assembly de definição (proposto) - Enums fechados — impede a criação de valores além dos membros declarados (proposto)
Juntas, essas três funcionalidades darão ao C# um dos sistemas de pattern matching type-safe mais completos em qualquer linguagem mainstream.
Experimente hoje
Os tipos union estão disponíveis no .NET 11 Preview 2:
- Instale o SDK .NET 11 Preview
- Aponte para
net11.0no seu projeto - Configure
<LangVersion>preview</LangVersion>
Um detalhe: no Preview 2, você vai precisar declarar UnionAttribute e IUnion no seu projeto já que eles ainda não estão no runtime. Pegue o RuntimePolyfill.cs do repo de docs, ou adicione isso:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Fechando
Os tipos union são uma daquelas funcionalidades que fazem você se perguntar como sobrevivemos sem elas. Pattern matching exaustivo imposto pelo compilador, sintaxe limpa, suporte a generics e integração com o pattern matching existente — é tudo que pedimos, feito do jeito C#.
Experimente no .NET 11 Preview 2, quebre coisas e compartilhe seu feedback no GitHub. Isso é preview, e o time de C# está ouvindo ativamente. Seus edge cases e feedback de design vão moldar a versão final.
Para a referência completa da linguagem, confira a documentação de tipos union e a especificação da funcionalidade.
