C# 15 유니온 | NDepend 팀


C# 15 유니온

C# 유니온 계획

기본 개념과 코드 예시


Pet pet = new Dog("Rover");

var description = pet switch {

Dog(var name) => name,

Cat(var adjective) => $"A {adjective} cat",

Bird bird => $"A {bird.Species}",

};

public union Pet (Dog, Cat, Bird);

public record class Dog(string Name);

public record struct Cat(string Adjective);

public record class Bird(string Species);

public record class Shark(int Teeth);

주요 특징

유니온 타입 정의

  • Pet은 Dog, Cat, Bird의 유니온으로 정의됨

  • 구성 타입들을 반복적으로 지정할 필요 없음

  • Pet이 정의를 완전히 캡슐화함

암시적 변환

  • Pet pet = new Dog("Rover");에서 컴파일러가 Dog에서 Pet으로의 암시적 변환을 인식

  • Pet은 Dog의 기본 클래스가 아님에도 변환 가능

C#과 F#의 차이점

C# 방식 (구조적 유니온/타입 유니온):

  • 유니온을 구성하는 타입들을 유니온과 별도로 정의해야 함

  • 각 타입이 독립적으로 존재

F# 방식 (판별 유니온/태그 유니온):


type Vehicle =

| Car of model : string * year : int

| Truck of model : string * year : int * payload : float

| Bicycle of model : string

  • 유니온 내부에 케이스들을 직접 정의

  • 타입보다는 태그로서 기능

패턴 매칭의 완전성 검사

  • 컴파일러가 Pet이 Dog, Cat, Bird만 가능함을 인지

  • 모든 케이스를 처리하면 switch 표현식에서 경고 없음

  • 나중에 Shark를 추가하면 해당 케이스를 처리하지 않은 모든 switch에서 경고/오류 발생

폐쇄형 유니온 타입

  • 가능성의 집합이 고정되고 컴파일러가 강제함

  • 클래스 상속과 대조적 (다형성을 통해 새 서브클래스 추가 가능)

  • 개방-폐쇄 원칙(OCP)이 클래스 상속에는 적용되지만 유니온에는 적용되지 않음

구현 방식

내부 구조


#region public union Pet (Dog, Cat, Bird);

public partial record struct Pet : IUnion

{

public Pet(Dog value) => Value = value;

public Pet(Cat value) => Value = value;

public Pet(Bird value) => Value = value;

public object? Value { get; }

}

#endregion

설계 특징

  • record struct로 구현

  • 단일 읽기 전용 object? Value 속성 보유

  • C#에서 거의 모든 타입을 object 참조로 표현 가능

  • 클래스, 구조체, 인터페이스, 델리게이트, 열거형, 원시 타입 등

  • 단, 관리되지 않는 포인터는 불가

값 타입 전용 유니온 최적화

현재 구현의 트레이드오프

  • Cat과 같은 값 타입이 Value 속성에 저장될 때 박싱됨

  • 박싱된 객체는 가비지 컬렉터가 관리해야 함

  • 작은 성능 오버헤드 발생

잠재적 최적화 방안

  1. 동일한 메모리 크기를 공유하는 값 타입들의 저장소 최적화

  2. 유니온 내 가장 큰 값 타입 기준으로 공간 할당 (작은 타입은 패딩 오버헤드)

  3. C# 팀은 값 타입 전용 유니온의 레이아웃을 수동으로 정의할 방법 제공 예정

MemoryMarshal을 활용한 바이트 캐스팅 예시


using System.Diagnostics;

using System.Runtime.InteropServices;

Span<byte> bytes8 = stackalloc byte[8] { 2, 0, 0, 0, 1, 0, 0, 0 };

// Convert first 4 bytes to uint

uint value32 = MemoryMarshal.Read<uint>(bytes8);

Debug.Assert(value32 == 2);

// Convert all 8 bytes to ulong

ulong value64 = MemoryMarshal.Read<ulong>(bytes8);

Debug.Assert(value64 == 4294967298);

// Convert all 8 bytes to MyStruct

ref MyStruct myStruct = ref MemoryMarshal.AsRef<MyStruct>(bytes8);

Debug.Assert(myStruct.X == 2);

Debug.Assert(myStruct.Y == 1);

[StructLayout(LayoutKind.Sequential)]

struct MyStruct { public uint X; public uint Y; }

패턴 매칭 switch

Value 속성을 통한 패턴 매칭


var description = pet.Value switch {

Dog(var name) => name,

Cat(var adjective) => $"A {adjective} cat",

Bird bird => $"A {bird.Species}",

};

  • 컴파일러가 Value 속성 호출을 추론

  • 간단하고 직관적인 구현

향후 계획 및 고려사항

릴리스 일정

  • C# 15 (2026년 11월): 가능성은 있지만 확정되지 않음

  • C# 14 GA (2025년 11월) 이후 작업 시작 예정

협업 계획

  • .NET Core Base Class Library 팀과 협력

  • 언어 기능 릴리스 시 많은 유니온 타입이 이미 사용 가능하도록 준비

F# 상호 운용성

  • 과제: F# 판별 유니온과 C# 타입 유니온의 근본적 차이

  • 계획: 상호 운용성 모델 작업 예정

  • 추가 가능성: F#처럼 유니온 범위 내에서 직접 타입 정의 허용 고려

주요 인물 및 출처

  • Mads Torgersen: C# 프로그래밍 언어 수석 디자이너

  • 2025년 8월 비디오: 1:02:25부터 1:10:30까지 유니온 관련 내용

  • 2024년 관련 블로그 포스트: “C# Discriminated Union: What’s Driving the C# Community’s Inquiries?”

실용적 활용 시나리오

  • 파서 구현

  • 메시징 프로토콜

  • 값 타입만으로 구성된 유니온이 일반적인 시나리오가 될 가능성

주의사항

  • 유니온은 확정된 기능이 아닌 가능성

  • 구현 방식과 세부사항은 변경될 수 있음

  • OCP가 유니온에는 적용되지 않으므로 확장 시 기존 코드 수정 필요

2개의 좋아요