Skip to main content

Discriminated Union | CSharp

  • Discriminated Union | CSharp
info

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:

  • record types
  • sealed hierarchies (com abstract e sealed)
  • switch expressions 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.
  • Circulo e Retangulo sã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#.


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

ConceitoF#C# equivalente
Tipo discriminadotype Forma = Circulo of ...abstract record Forma + sealed record variantes
VariantesConstrutores (Circulo, Retangulo)record filhos
Casamento de padrõesmatch forma with ...switch expression
Segurança de tipoTotalQuase total (ou 100% com sealed)