C# Discriminated Union 패턴 구현
판별 합집합(Discriminated Union, DU)은 값이 여러 타입 중 하나임을 타입 시스템으로 표현하는 구조입니다. F#이나 Rust의 enum과 동일한 개념으로, C#에는 내장 문법이 없지만 레코드 계층, OneOf 라이브러리, 패턴 매칭으로 구현할 수 있습니다.
1. 레코드 계층 — 가장 관용적인 C# 방식
섹션 제목: “1. 레코드 계층 — 가장 관용적인 C# 방식”// abstract record로 기반 타입 선언abstract record Shape;record Circle(double Radius) : Shape;record Rectangle(double Width, double Height) : Shape;record Triangle(double Base, double Height) : Shape;
// switch expression으로 소진(exhaustive) 매칭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 UnreachableException()};
// 새 타입 추가 시 컴파일러가 경고 발생 (C# 9 이상)2. Result — 에러 처리 DU
섹션 제목: “2. Result — 에러 처리 DU”abstract record Result<T>;record Ok<T>(T Value) : Result<T>;record Err<T>(string Message) : Result<T>;
// 헬퍼 메서드static Result<T> Succeed<T>(T value) => new Ok<T>(value);static Result<T> Fail<T>(string msg) => new Err<T>(msg);
// 사용Result<int> Parse(string s) => int.TryParse(s, out int v) ? Succeed(v) : Fail<int>($"'{s}'는 정수가 아닙니다");
var result = Parse("42");string msg = result switch{ Ok<int> ok => $"값: {ok.Value}", Err<int> err => $"오류: {err.Message}", _ => throw new UnreachableException()};3. Map/Bind — Railway-Oriented Programming
섹션 제목: “3. Map/Bind — Railway-Oriented Programming”static class ResultExtensions{ public static Result<U> Map<T, U>( this Result<T> r, Func<T, U> f) => r switch { Ok<T> ok => new Ok<U>(f(ok.Value)), Err<T> err => new Err<U>(err.Message), _ => throw new UnreachableException() };
public static Result<U> Bind<T, U>( this Result<T> r, Func<T, Result<U>> f) => r switch { Ok<T> ok => f(ok.Value), Err<T> err => new Err<U>(err.Message), _ => throw new UnreachableException() };}
// 파이프라인 구성Result<string> process = Parse("42") .Map(v => v * 2) .Bind(v => v > 50 ? Succeed($"큼: {v}") : Fail<string>("너무 작음"));4. OneOf 라이브러리
섹션 제목: “4. OneOf 라이브러리”// NuGet: OneOfusing OneOf;
// 3개 타입의 합집합OneOf<string, int, bool> value = "hello";
// Match로 모든 케이스 처리 (컴파일 타임 강제)string result = value.Match( s => $"문자열: {s}", i => $"정수: {i}", b => $"불리언: {b}");
// Switchvalue.Switch( s => Console.WriteLine(s.ToUpper()), i => Console.WriteLine(i * 2), b => Console.WriteLine(!b));
// Is/AsT0/AsT1/AsT2if (value.IsT0) string s = value.AsT0;5. Option — null 대안
섹션 제목: “5. Option — null 대안”abstract record Option<T>;record Some<T>(T Value) : Option<T>;record None<T> : Option<T>;
static Option<T> From<T>(T? value) where T : class => value is not null ? new Some<T>(value) : new None<T>();
static Option<T> From<T>(T? value) where T : struct => value.HasValue ? new Some<T>(value.Value) : new None<T>();
// 사용Option<User> FindUser(int id) => _db.TryGet(id, out var user) ? new Some<User>(user) : new None<User>();
string display = FindUser(1) switch{ Some<User> u => u.Value.Name, None<User> => "알 수 없음", _ => throw new UnreachableException()};6. C# 12 — 타입 별칭과 미래 전망
섹션 제목: “6. C# 12 — 타입 별칭과 미래 전망”// C# 12: using alias로 DU 타입명 간소화using ParseResult = OneOf.OneOf<int, string>;
ParseResult TryParse(string s) => int.TryParse(s, out int v) ? v : s;
// C# 미래 (제안 중): 내장 union 타입// union type Result = Ok(int) | Error(string); ← 아직 표준 미채택7. 성능 고려사항
섹션 제목: “7. 성능 고려사항”// 레코드 기반 DU: 힙 할당 발생// 고빈도 경로에서는 구조체 기반으로
readonly struct ValueResult<T> where T : struct{ public readonly T Value; public readonly string? Error; public bool IsOk => Error is null;
public ValueResult(T value) { Value = value; Error = null; } public ValueResult(string error) { Value = default; Error = error; }}
// 힙 할당 없음, 스택에서 처리var r = new ValueResult<int>(42);C#에서 DU를 구현하는 가장 관용적인 방법은 abstract record + 자식 레코드 계층입니다. switch expression으로 소진 매칭을 하고, Map/Bind로 파이프라인을 구성하면 Railway-Oriented Programming 패턴을 안전하게 구현할 수 있습니다. 외부 라이브러리를 허용한다면 OneOf가 더 간결한 문법을 제공합니다.