콘텐츠로 이동

C# Discriminated Unions 패턴

Discriminated Union(판별 합집합)은 여러 타입 중 정확히 하나가 될 수 있는 타입입니다. F#, Rust, Haskell 등 함수형 언어에서 핵심 기능이지만, C#은 공식 지원이 없어 패턴으로 구현합니다.

// 기반 타입을 sealed abstract로 정의
public abstract class Result {
private Result() {} // 외부 상속 차단
public sealed class Ok : Result {
public string Value { get; }
public Ok(string value) => Value = value;
}
public sealed class Error : Result {
public string Message { get; }
public Error(string message) => Message = message;
}
}
// 사용
Result result = new Result.Ok("성공");
string output = result switch {
Result.Ok ok => $"성공: {ok.Value}",
Result.Error error => $"실패: {error.Message}",
_ => throw new UnreachableException()
};
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;
double Area(Shape shape) => shape switch {
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentOutOfRangeException()
};

record는 구조적 동등성과 with 복사를 자동 제공합니다.

C# 컴파일러는 아직 완전성 검사(exhaustiveness check)를 강제하지 않습니다. _ 패턴에서 예외를 던지는 방식으로 런타임 안전성을 확보합니다.

// 새 타입 추가 시 switch에서 누락되면 런타임 예외
public record Pentagon : Shape; // 추가
// 아래 switch는 런타임에 UnreachableException 발생
double area = new Pentagon() switch {
Circle c => ...,
Rectangle r => ...,
Triangle t => ...,
_ => throw new UnreachableException($"처리 안 된 Shape: {shape.GetType()}")
};
public abstract record Option<T> {
public record Some(T Value) : Option<T>;
public record None : Option<T>;
public static Option<T> Of(T? value) =>
value is null ? new None() : new Some(value);
public TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> onNone) =>
this switch {
Some s => onSome(s.Value),
None => onNone(),
_ => throw new UnreachableException()
};
}
Option<string> name = Option<string>.Of(GetName());
string display = name.Match(n => $"안녕, {n}!", () => "이름 없음");
public abstract record Result<T> {
public record Ok(T Value) : Result<T>;
public record Err(string Message) : Result<T>;
public Result<U> Map<U>(Func<T, U> f) => this switch {
Ok ok => new Result<U>.Ok(f(ok.Value)),
Err e => new Result<U>.Err(e.Message),
_ => throw new UnreachableException()
};
public Result<U> FlatMap<U>(Func<T, Result<U>> f) => this switch {
Ok ok => f(ok.Value),
Err e => new Result<U>.Err(e.Message),
_ => throw new UnreachableException()
};
}

NuGet의 OneOf 패키지를 사용하면 더 간결하게 구현됩니다.

using OneOf;
OneOf<int, string, bool> val = 42;
string result = val.Match(
i => $"정수: {i}",
s => $"문자열: {s}",
b => $"불린: {b}"
);
  • sealed abstract class/record + private 생성자로 외부 상속 차단
  • C# 9 record 타입으로 간결한 DU 계층 구성
  • switch 표현식으로 패턴 매칭, _에서 예외로 완전성 보장
  • Option<T>, Result<T> 패턴으로 null/예외를 타입으로 표현
  • C# 향후 버전에서 native DU 문법 논의 중