C# Union 유형: 차별된 Union이 마침내 등장합니다.

· 13분 읽기

상당한 시간 동안 C#을 작성해 왔다면 “여러 가지 중 하나"가 될 수 있는 것을 모델링하려고 할 때 벽에 부딪혔을 가능성이 높습니다. 어쩌면 성공 값이나 오류를 반환하는 메서드가 필요할 수도 있습니다. 어쩌면 각각 완전히 다른 데이터를 사용하는 신용카드, 은행 송금, 디지털 지갑을 처리하는 결제 시스템을 구축하고 있었을 수도 있습니다. 아니면 그냥 F#이나 Rust를 보고 “왜 C#에서는 그걸 가질 수 없지?“라고 생각했을 수도 있습니다.

기다림이 거의 끝났습니다. 차별화된 노동조합이 C#으로 출시됩니다.

이는 수년간 가장 많이 요청된 언어 기능 중 하나였으며 커뮤니티 토론은 2017년 이전부터 이어졌습니다. C# 언어 디자인 팀은 철저한 패턴 일치를 통해 폐쇄형 계층 구조를 정의하기 위해 union 키워드를 도입하는 제안을 작업해 왔습니다. 이 게시물에서는 차별적 공용체(discriminated Union)가 무엇인지, 왜 그렇게 중요한지, 지금까지 우리가 이를 어떻게 속였는지, 그리고 제안된 구문이 실제로 어떤 모습인지에 대해 각각에 대한 실제 코드 예제를 통해 안내하고 싶습니다.

시작하기 전에 간단히 알아두어야 할 점은 이 글을 쓰는 시점에서 통합 유형 기능은 아직 제안 및 미리 보기 단계에 있다는 것입니다. 여기서 설명하는 구문과 동작은 공개적으로 사용 가능한 최신 디자인 문서와 C# 언어 디자인 팀의 토론을 기반으로 합니다. 최종 출시 이전에는 상황이 변경될 수 있습니다. 확정된 사항과 아직 논의 중인 사항에 대해 명확히 말씀드리겠습니다.

차별적 노동조합이란 무엇입니까?

본질적으로 구별된 공용체(때때로 “태그된 공용체” 또는 “합계 유형"이라고도 함)는 각 변형이 서로 다른 데이터를 전달할 수 있는 고정된 가능한 값 세트 중 하나를 보유할 수 있는 유형입니다. “구별된” 부분은 런타임이 항상 어떤 변형인지 알고 있음을 의미합니다. 이를 식별하는 태그가 있습니다.

enum처럼 생각하면 되지만, 각 구성원은 자신만의 데이터 페이로드를 운반할 수 있습니다.

다른 언어를 사용해 본 적이 있다면 아마도 이전에 이 개념을 본 적이 있을 것입니다.

**F#**는 첫날부터 차별적인 노동조합을 운영해 왔습니다.

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of base: float * height: float

Rust는 동일한 아이디어로 enum를 사용합니다.

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

TypeScript는 태그된 공용체와 비슷한 결과를 얻습니다.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

이러한 모든 경우에 컴파일러는 가능한 모든 변형을 알고 있으며 사용자가 이를 모두 처리하도록 강제할 수 있습니다. 이것이 초능력입니다: 완전성 검사. 새로운 변형을 추가하면 컴파일러는 이를 처리하는 것을 잊어버린 모든 곳에서 알려줍니다.

C#에서는 이를 표현하는 최고의 방법이 없었습니다. 지금까지.

오늘날 노동조합을 시뮬레이션하는 방법

수년에 걸쳐 C# 커뮤니티에서는 각각 고유한 장단점이 있는 여러 가지 해결 방법을 생각해 냈습니다. 가장 일반적인 접근 방식을 살펴보겠습니다.

상속이 포함된 추상 레코드

최신 C#에서 가장 관용적인 해결 방법은 봉인된 파생 유형이 있는 추상 레코드를 사용하는 것입니다.

public abstract record Shape
{
    public sealed record Circle(double Radius) : Shape;
    public sealed record Rectangle(double Width, double Height) : Shape;
    public sealed record Triangle(double Base, double Height) : Shape;

    private Shape() { } // Prevent external inheritance
}

이것은 합리적으로 잘 작동합니다. 불변성, 값 평등을 얻고 패턴 일치를 사용할 수 있습니다.

double Area(Shape shape) => shape switch
{
    Shape.Circle c => Math.PI * c.Radius * c.Radius,
    Shape.Rectangle r => r.Width * r.Height,
    Shape.Triangle t => 0.5 * t.Base * t.Height,
    _ => throw new InvalidOperationException("Unknown shape")
};
```그러나 중요한 단점이 있습니다. 컴파일러는 계층 구조가 닫혀 있다는 것을 모르므로 항상 `_` 폐기 팔이 필요합니다. 그렇지 않으면 경고가 표시됩니다. 새로운 변형을 추가하면 컴파일러는 이를 처리하는 것을 잊은 모든 위치에 대해 알려주지 않습니다. 폐기  자동으로 이를 삼켜버립니다. 그것은 목적을 완전히 무너뜨립니다.

### 원오브 도서관

 다른 인기 있는 접근 방식은 [OneOf](https://github.com/mcintyre321/OneOf) NuGet 패키지입니다.

```csharp
public OneOf<Success<Order>, NotFound, ValidationError> ProcessOrder(OrderRequest request)
{
    // ...
}

// Usage
var result = ProcessOrder(request);
result.Switch(
    success => Console.WriteLine($"Order {success.Value.Id} processed"),
    notFound => Console.WriteLine("Order not found"),
    error => Console.WriteLine($"Validation failed: {error.Message}")
);

OneOf는 일반 유형 매개변수를 통해 컴파일 타임에 완전성 검사를 제공합니다. 그러나 위치 일치(첫 번째 유형, 두 번째 유형 등)에 의존하고 일반 시그니처는 다루기 힘들 정도로 빠르며 언어의 패턴 일치와 통합되지 않습니다. 영리한 해킹이지만 여전히 해킹입니다.

수동 열거형 + 데이터 패턴

일부 개발자는 enum 태그와 데이터 컨테이너를 사용하여 클래식 경로를 사용합니다.

public enum PaymentType { CreditCard, BankTransfer, DigitalWallet }

public class Payment
{
    public PaymentType Type { get; init; }
    public CreditCardInfo? CreditCard { get; init; }
    public BankTransferInfo? BankTransfer { get; init; }
    public DigitalWalletInfo? DigitalWallet { get; init; }
}

이것은 깨지기 쉽습니다. TypeCreditCard로 설정하는 것 외에는 BankTransfer 속성을 채우는 것을 막을 수 없습니다. 컴파일러는 도움을 줄 수 없으며 모든 곳에서 런타임 오류와 null 검사가 발생하게 됩니다. 이는 유형 모델링에 대한 “문자열 유형” 접근 방식이며 확장되지 않습니다.

이러한 접근 방식은 모두 근본적인 문제를 공유합니다. 언어를 사용하는 대신 언어와 싸우고 있습니다. 컴파일러는 닫힌 가능성 집합에 대해 추론할 수 없으므로 차별 공용체의 가장 귀중한 속성인 철저한 검사를 잃게 됩니다.

C# 제안: union 키워드

C# 언어 팀의 제안에는 각각 선택적으로 데이터를 전달하는 명명된 멤버의 닫힌 집합을 정의하는 전용 union 키워드가 도입되었습니다. 제안된 기본 구문은 다음과 같습니다.

union Shape
{
    Circle(double Radius),
    Rectangle(double Width, double Height),
    Triangle(double Base, double Height)
}

그게 다야. 깔끔하고 간결하며 즉시 읽을 수 있습니다. 공용체 내부의 각 구성원은 자체 데이터를 사용하여 고유한 변형을 정의합니다. 컴파일러는 Shape가 이 세 가지 중 하나만 될 수 있다는 것을 알고 있습니다.

내부적으로 컴파일러는 추상 레코드를 사용하여 직접 작성하는 것과 유사하지만 컴파일러가 유형의 폐쇄적 특성을 완전히 인식하는 봉인된 유형 계층 구조를 생성합니다. 이는 컴파일러가 패턴 일치에 철저함을 적용할 수 있다는 것을 의미하며, 이것이 주요 이점입니다.

가치 전용 회원

조합원은 데이터를 가지고 다닐 필요가 없습니다. 데이터를 전달하는 멤버와 단순 값 멤버를 혼합할 수 있습니다.

union Option<T>
{
    Some(T Value),
    None
}

이는 기능적 프로그래머가 수년간 C#에서 요구해 왔던 고전적인 Option/Maybe 유형입니다. None에는 데이터가 없습니다. 단지 태그일 뿐입니다.

일반 공용체

위의 Option<T> 예제에서 볼 수 있듯이 공용체는 제네릭을 지원합니다. 다음은 좀 더 관련된 예입니다.

union Result<T, E>
{
    Ok(T Value),
    Error(E Err)
}

이는 예상되는 실패 사례에 대한 예외에 의존하지 않는 전체 오류 처리 스타일을 열어줍니다. 이는 수년간 Rust 및 함수형 언어의 표준 관행이었습니다.

메소드가 있는 공용체

또한 이 제안은 공용체가 다른 유형과 마찬가지로 메소드, 계산된 속성 및 구현 인터페이스를 가질 수 있도록 허용합니다.```csharp union Shape { Circle(double Radius), Rectangle(double Width, double Height), Triangle(double Base, double Height);

public double Area => this switch
{
    Circle(var r) => Math.PI * r * r,
    Rectangle(var w, var h) => w * h,
    Triangle(var b, var h) => 0.5 * b * h
};

public double Perimeter => this switch
{
    Circle(var r) => 2 * Math.PI * r,
    Rectangle(var w, var h) => 2 * (w + h),
    Triangle(var b, var h) => b + h + Math.Sqrt(b * b + h * h)
};

}


`switch` 내부의 `Area`  `Perimeter` 표현식에는 기본 arm이 필요하지 않습니다. 컴파일러는 통합이 철저하다는 것을 알고 있습니다. ,  가지 변형만 있고  가지 모두 처리됩니다. 나중에  번째 변형을 추가하면 컴파일러는 이를 처리하지 않는 모든 `switch` 플래그를 지정합니다.

## 패턴 매칭 통합

패턴 일치는 버전 7.0부터 C#에서 발전해 왔으며 공용체 형식은 해당 시스템의 일급 시민으로 설계되었습니다.

### 철저한 스위치 표현

가장 영향력 있는 기능은 철저한 스위치 확인입니다. 공용체를 사용하면 컴파일러는 가능한 모든 경우를 **알고** 있습니다.

```csharp
string Describe(Shape shape) => shape switch
{
    Circle(var r)         => $"A circle with radius {r}",
    Rectangle(var w, var h) => $"A {w}x{h} rectangle",
    Triangle(var b, var h)  => $"A triangle with base {b} and height {h}"
};

폐기 팔이 없습니다. 아니요 _ => throw new NotImplementedException(). 대소문자를 잊어버린 경우 컴파일러는 경고가 아닌 오류를 내보냅니다. 이는 안전성이 근본적으로 개선된 것입니다.

중첩 패턴 일치

공용체는 중첩된 패턴으로 자연스럽게 구성됩니다.

union Expr
{
    Literal(double Value),
    Add(Expr Left, Expr Right),
    Multiply(Expr Left, Expr Right),
    Negate(Expr Inner)
}

double Evaluate(Expr expr) => expr switch
{
    Literal(var v)          => v,
    Add(var left, var right) => Evaluate(left) + Evaluate(right),
    Multiply(var left, var right) => Evaluate(left) * Evaluate(right),
    Negate(var inner)        => -Evaluate(inner)
};

이러한 종류의 재귀적 데이터 구조는 컴파일러, 인터프리터, 규칙 엔진 및 수학적 모델링에서 매우 일반적입니다. 현재 C#에서는 깊은 클래스 계층 구조와 방문자 패턴이 필요합니다. 공용체를 사용하면 코드가 훨씬 더 간단해집니다.

보호 조항

공용체와의 패턴 일치는 예상한 대로 when 가드를 지원합니다.

string Classify(Shape shape) => shape switch
{
    Circle(var r) when r > 100   => "Large circle",
    Circle(var r) when r > 10    => "Medium circle",
    Circle(_)                     => "Small circle",
    Rectangle(var w, var h) when w == h => "Square",
    Rectangle _                   => "Rectangle",
    Triangle _                    => "Triangle"
};

실제 예

공용체 유형이 코드를 극적으로 향상시키는 몇 가지 실제 시나리오를 살펴보겠습니다.

결과 패턴: 예상되는 오류에 대한 예외 교체

최신 애플리케이션 개발에서 가장 일반적인 패턴 중 하나는 제어 흐름에 대한 예외를 사용하지 않고 성공하거나 실패할 수 있는 작업을 나타내는 것입니다. 예외는 예외적이어야 합니다(예: 네트워크 오류 또는 메모리 부족 상태 등). 유효성 검사 오류 또는 “찾을 수 없음” 결과는 예상된 결과이며 예외는 아닙니다.

union Result<T, E>
{
    Ok(T Value),
    Error(E Err)
}

union OrderError
{
    NotFound(Guid OrderId),
    InsufficientStock(string ProductId, int Requested, int Available),
    PaymentDeclined(string Reason),
    ValidationFailed(IReadOnlyList<string> Errors)
}

Result<Order, OrderError> ProcessOrder(OrderRequest request)
{
    if (!Validate(request, out var errors))
        return new ValidationFailed(errors);

    var product = catalog.Find(request.ProductId);
    if (product is null)
        return new NotFound(request.OrderId);

    if (product.Stock < request.Quantity)
        return new InsufficientStock(request.ProductId, request.Quantity, product.Stock);

    var paymentResult = paymentGateway.Charge(request.Payment);
    if (!paymentResult.Success)
        return new PaymentDeclined(paymentResult.Message);

    var order = CreateOrder(request, product);
    return new Ok(order);
}

그런 다음 호출자는 가능한 모든 결과를 처리해야 합니다.

var result = ProcessOrder(request);

var response = result switch
{
    Ok(var order) => Results.Created($"/orders/{order.Id}", order),
    Error(NotFound(var id)) => Results.NotFound($"Order {id} not found"),
    Error(InsufficientStock(var pid, var req, var avail)) =>
        Results.Conflict($"Product {pid}: requested {req}, only {avail} available"),
    Error(PaymentDeclined(var reason)) =>
        Results.UnprocessableEntity($"Payment declined: {reason}"),
    Error(ValidationFailed(var errors)) =>
        Results.BadRequest(new { Errors = errors })
};

try-catch 블록도 없고, 잊어버린 예외 유형도 없고, 런타임에 예상치 못한 일도 없습니다. 모든 실패 모드는 유형 서명에 표시되며 컴파일러에 의해 적용됩니다. 이는 API 안정성이 크게 향상되었습니다.

도메인 모델링: 결제 유형

다음은 전자상거래 시스템에서 다양한 결제 방법을 처리하는 실제 도메인 모델링 예입니다.

union PaymentMethod
{
    CreditCard(string CardNumber, string Expiry, string Cvv),
    BankTransfer(string Iban, string Bic),
    DigitalWallet(string Provider, string Token),
    CashOnDelivery
}

decimal CalculateProcessingFee(PaymentMethod method, decimal amount) => method switch
{
    CreditCard _                         => amount * 0.029m + 0.30m,
    BankTransfer _                       => 1.50m,
    DigitalWallet(provider: "PayPal", _) => amount * 0.034m + 0.35m,
    DigitalWallet _                      => amount * 0.025m,
    CashOnDelivery                       => 4.99m
};

string FormatForReceipt(PaymentMethod method) => method switch
{
    CreditCard(var num, _, _)       => $"Credit Card ending in {num[^4..]}",
    BankTransfer(var iban, _)       => $"Bank Transfer ({iban[..4]}****)",
    DigitalWallet(var provider, _)  => $"Digital Wallet ({provider})",
    CashOnDelivery                  => "Cash on Delivery"
};

이를 5개의 파일에 분산된 5개의 서로 다른 구현이 있는 인터페이스 또는 추상 클래스가 있고 방문자 패턴이 맨 위에 있을 수 있는 현재 접근 방식과 비교해 보세요. 통합 접근 방식은 데이터 정의와 작업을 함께 유지하고 읽기 가능하며 철저하게 검사합니다.

상태 머신

상태 머신은 주문 처리, 워크플로 엔진, 연결 관리, UI 상태 등 소프트웨어의 모든 곳에 있습니다. 노동조합은 이를 명시적이고 안전하게 만듭니다.

union ConnectionState
{
    Disconnected,
    Connecting(string Host, int Port, DateTime StartedAt),
    Connected(string Host, int Port, TcpClient Client),
    Reconnecting(string Host, int Port, int Attempt, TimeSpan Delay),
    Failed(string Host, Exception Error)
}

ConnectionState HandleEvent(ConnectionState state, ConnectionEvent evt) => (state, evt) switch
{
    (Disconnected, Connect(var host, var port)) =>
        new Connecting(host, port, DateTime.UtcNow),

    (Connecting(var h, var p, _), ConnectionSucceeded(var client)) =>
        new Connected(h, p, client),

    (Connecting(var h, var p, _), ConnectionFailed(var ex)) =>
        new Reconnecting(h, p, Attempt: 1, Delay: TimeSpan.FromSeconds(1)),

    (Reconnecting(var h, var p, var attempt, _), ConnectionSucceeded(var client)) =>
        new Connected(h, p, client),

    (Reconnecting(var h, var p, var attempt, _), ConnectionFailed(var ex)) when attempt >= 5 =>
        new Failed(h, ex),

    (Reconnecting(var h, var p, var attempt, _), ConnectionFailed(_)) =>
        new Reconnecting(h, p, attempt + 1, TimeSpan.FromSeconds(Math.Pow(2, attempt))),

    (Connected(_, _, var client), Disconnect) =>
        new Disconnected,

    _ => state // Ignore events that don't apply to current state
};

각 상태는 해당 상태와 관련된 데이터를 정확하게 전달합니다. 해당 변형에 존재하지 않기 때문에 Connecting 상태에 있을 때 실수로 TcpClient에 액세스할 수 없습니다. 유형 시스템은 상태 시스템의 불변성을 적용합니다.

직렬화 및 상호 운용성 고려 사항공용체 유형과 관련하여 즉시 떠오르는 실용적인 질문 중 하나는 다음과 같습니다. 이를 어떻게 직렬화합니까? API를 구축하거나 데이터를 저장하는 경우 올바르게 작동하려면 JSON 직렬화가 필요합니다.

디자인 팀은 공용체 유형에 대한 기본 제공 System.Text.Json 지원을 논의해 왔습니다. 예상되는 접근 방식에는 판별자 속성을 사용한 직렬화가 포함됩니다.

{
  "$type": "Circle",
  "radius": 5.0
}
{
  "$type": "Rectangle",
  "width": 10.0,
  "height": 20.0
}

이는 JsonDerivedType 특성을 사용하여 .NET 7에 도입된 기존 다형성 직렬화 지원과 일치합니다. 기본적으로 변형 이름을 유형 판별자로 사용하여 공용체는 기본적으로 System.Text.Json와 함께 작동할 것으로 예상됩니다.

Entity Framework Core의 경우 TPH(계층별 테이블) 상속 매핑이 이미 작동하는 방식과 유사하게 판별자 열을 사용하여 공용체 값을 저장하는 접근 방식이 있을 가능성이 높습니다. 정확한 EF Core 통합은 아직 설계 중이지만 폐쇄형 계층 구조를 처리하기 위한 인프라는 이미 존재합니다.

공용체가 내부적으로 표준 IL 클래스 계층 구조로 컴파일되기 때문에 다른 .NET 언어와의 상호 운용이 원활해야 한다는 점은 주목할 가치가 있습니다. C# 공용체를 사용하는 F# 코드는 이를 표준 형식 계층 구조로 간주하고 그 반대의 경우도 마찬가지입니다.

사용 방법

작성 시점을 기준으로 공용체 유형은 최신 .NET SDK 미리 보기에서 미리 보기 기능으로 사용할 수 있습니다. 제안된 구문을 실험하려면 다음을 수행해야 합니다.

  1. 최신 .NET 미리보기 SDK 설치
  2. 프로젝트 파일에서 미리보기 언어 버전을 활성화합니다.
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
</Project>

미리보기 기능은 변경될 수 있다는 점에 유의하세요. 구문, 동작 및 컴파일러 진단은 최종 릴리스 이전에 크게 발전할 수 있습니다. 미리 보기 언어 기능에 의존하는 프로덕션 코드를 출시하지 마세요. 하지만 반드시 이를 실험하고 피드백을 제공하세요. C# 팀은 csharplang 저장소 토론을 적극적으로 모니터링합니다.

제안의 진행 상황을 확인하고 싶다면 주요 시청 장소는 다음과 같습니다.

  • 언어 디자인 토론을 위한 dotnet/csharplang 저장소
  • 컴파일러 구현 진행 상황을 위한 dotnet/roslyn 저장소
  • 공식 공지를 위한 .NET 블로그

기존 접근방식과의 비교

다양한 접근 방식이 어떻게 결합되어 있는지 확인할 수 있도록 간단한 비교를 해보겠습니다.

기능초록기록OneOf<T1,T2>열거형 + 데이터노동조합 유형
완전성 검사❌ 아니요✅ 예❌ 아니요✅ 예
패턴 매칭✅ 예❌ 한정❌ 매뉴얼✅ 네이티브
컴파일러 강제 종료❌ 아니요✅ 예❌ 아니요✅ 예
변형별 데이터✅ 예✅ 예⚠️ 깨지기 쉬움✅ 예
가독성⚠️ 자세한 내용⚠️ 위치❌ 가난✅ 우수
직렬화✅ 매뉴얼⚠️ 복합✅ 매뉴얼✅ 내장
상용구⚠️ 보통✅ 낮음⚠️ 높음✅ 최소
외부 종속성 없음✅ 예❌ 누겟✅ 예✅ 예

이것이 .NET 생태계에 미치는 영향

차별적인 공용체의 도입은 전체 .NET 생태계에 파급력을 미칠 것입니다. 내가 기대하는 내용은 다음과 같습니다.

라이브러리 디자인은 개선될 것입니다. 현재 “찾을 수 없음"을 나타내거나 유효성 검사 실패에 대한 예외를 발생시키기 위해 null를 반환하는 API는 대신 Result<T, E> 유형을 반환할 수 있습니다. 이는 유형 서명에서 오류 모드를 명시적으로 만듭니다. 문서나 소스 코드를 읽는 것이 아니라 메서드 서명을 보면 무엇이 잘못될 수 있는지 알 수 있습니다.

도메인 모델링의 표현력이 더욱 풍부해졌습니다. 문제 도메인과 코드 표현 사이의 격차가 극적으로 줄어듭니다. 도메인 전문가가 “지불은 신용 카드, 은행 송금 또는 대금 상환이 될 수 있습니다"라고 말하면 이를 상속 계층 구조로 변환하는 대신 통합으로 직접 모델링할 수 있습니다.

C# 개발자는 F# 아이디어에 접근할 수 있습니다. 많은 C# 개발자는 멀리서 F#의 형식 시스템을 존경했지만 조직에서 F#을 채택할 수 없었습니다. Union 형식은 F#의 가장 강력한 기능 중 하나를 C#에 가져오며 이는 전체 .NET 생태계에 도움이 됩니다.

런타임 오류가 줄어듭니다. 완전성 검사만으로도 전체 버그 범주를 방지할 수 있습니다. 새로운 변형을 공용체에 추가할 때마다 컴파일러는 업데이트가 필요한 코드베이스의 모든 위치를 안내합니다. 더 이상 잊어버린 switch 사례가 없으며, 프로덕션에서만 나타나는 기본 분기에 더 이상 NotImplementedException가 없습니다.

결론

차별적인 공용체는 거의 10년 동안 C# 커뮤니티의 희망 목록 1위에 올랐으며 그럴 만한 이유가 있습니다. 이는 유형 시스템의 근본적인 격차, 즉 컴파일러 강제 안전을 통해 “여러 가지 중 하나"가 될 수 있는 데이터를 모델링하는 기능을 해결합니다.

제안된 union 키워드는 C#의 패턴 일치와 긴밀하게 통합되고, 제네릭과 함께 작동하고, 메서드 및 인터페이스를 지원하고, 런타임이 아닌 컴파일 타임에 버그를 잡는 철저한 검사를 가능하게 하는 깔끔하고 간결한 구문을 제공합니다.

도메인 모델을 구축하든, 명시적 오류 유형으로 API를 설계하든, 상태 머신을 구현하든, 아니면 어색한 상속 계층 구조를 좀 더 자연스러운 것으로 대체하려고 하든 공용체 유형은 C# 작성 방식을 바꿀 것입니다.

우리는 이것을 오랫동안 기다려왔습니다. 구문은 우아하고 기존 언어 기능과의 통합은 사려 깊으며 전체 .NET 생태계에 실질적인 영향을 미칠 것입니다.

미리 보기 릴리스를 주시하고, 기능을 실험하고, 언어 디자인 팀에 피드백을 제공하세요. 이것은 일단 사용해 보면 이 기능 없이 어떻게 살았는지 궁금해하게 되는 기능 중 하나입니다.