Discriminated Union | CSharp
- Discriminated Union | CSharp
Um discriminated union é um tipo de dado que pode representar várias formas (ou variantes), mas somente uma delas de cada vez. Cada forma é "discriminada" (ou identificada) por um rótulo (tag) que indica qual das variantes está sendo usada.
Em outras palavras: | É um tipo que pode ser um entre vários tipos possíveis, com uma forma de distinguir qual está em uso.
O C# não tem discriminated unions nativos como F# ou Rust, mas desde o C# 9+ (e ainda mais com o C# 10, 11 e 12), conseguimos chegar muito perto do mesmo conceito usando:
recordtypessealedhierarchies (comabstractesealed)switchexpressions e pattern matching
🧩 Exemplo: modelando um Forma (discriminated union)
Em F#, faríamos assim:
type Forma =
| Circulo of float
| Retangulo of float * float
Em C# moderno (C# 11+ com .NET 7+), podemos fazer:
public abstract record Forma;
public sealed record Circulo(double Raio) : Forma;
public sealed record Retangulo(double Largura, double Altura) : Forma;
👉 Aqui:
Formaé a base abstrata.CirculoeRetangulosão variantes seladas (ou seja, o compilador sabe que só existem essas).
Isso nos dá imutabilidade, igualdade por valor e uma hierarquia fechada, o que é ótimo para simular um discriminated union.
🧮 Usando pattern matching (como match do F#)
Podemos tratar os casos com o switch expression:
double CalcularArea(Forma forma) =>
forma switch
{
Circulo c => Math.PI * c.Raio * c.Raio,
Retangulo r => r.Largura * r.Altura,
_ => throw new ArgumentException("Forma desconhecida")
};
O compilador:
- verifica exaustividade (avisa se faltou algum tipo);
- infere tipos corretos para cada branch.
🧠 Com C# 12 — ainda melhor!
No C# 12, podemos até usar o novo Primary Constructors em classes e pattern matching aprimorado:
public abstract class Forma;
public class Circulo(double raio) : Forma
{
public double Raio { get; } = raio;
}
public class Retangulo(double largura, double altura) : Forma
{
public double Largura { get; } = largura;
public double Altura { get; } = altura;
}
double CalcularArea(Forma forma) => forma switch
{
Circulo(var r) => Math.PI * r * r,
Retangulo(var w, var h) => w * h,
_ => throw new NotSupportedException()
};
Aqui o padrão Circulo(var r) é literalmente um pattern match sobre os parâmetros do construtor — idêntico à ideia de “desconstruir” no F#.
⚙️ Alternativa com OneOf (library popular)
Há também bibliotecas como OneOf que implementam discriminated unions diretamente:
using OneOf;
public class Forma : OneOfBase<Circulo, Retangulo> {}
public record Circulo(double Raio);
public record Retangulo(double Largura, double Altura);
Uso:
Forma forma = new Circulo(10);
forma.Switch(
c => Console.WriteLine(Math.PI * c.Raio * c.Raio),
r => Console.WriteLine(r.Largura * r.Altura)
);
Essa lib já trata o pattern matching e exaustividade automaticamente.
🧾 Em resumo
| Conceito | F# | C# equivalente |
|---|---|---|
| Tipo discriminado | type Forma = Circulo of ... | abstract record Forma + sealed record variantes |
| Variantes | Construtores (Circulo, Retangulo) | record filhos |
| Casamento de padrões | match forma with ... | switch expression |
| Segurança de tipo | Total | Quase total (ou 100% com sealed) |