[Day 1 / .NET Conf 2025] C# 14의 새로운 기능 | Dustin Campbell


C# 14 새로운 기능 소개

핵심 요약

Dustin Campbell이 발표한 C# 14의 주요 기능은 개발자 경험을 크게 향상시키는 혁신적인 문법 개선들입니다. 특히 null 안전성 강화, 자동 속성의 필드 접근, 그리고 확장 멤버의 구현으로 더 깔끔하고 표현력 있는 코드 작성이 가능해집니다. 이 발표는 기존 C# 6부터 준비했던 기능들을 드디어 실현하면서, 각 기능이 어떻게 코드 복잡성을 줄이고 생산성을 높이는지를 실제 예제로 보여줍니다.


상세 요약

1단계: 기본 개선 사항

1.1 Null 조건부 할당 연산자 (Null-Coalescing Assignment with ?.)

배경

  • C# 6에서 null 조건 연산자(?.)가 도입되었지만 할당에는 사용할 수 없었습니다
  • 개발자들이 null 확인 후 속성을 할당할 때마다 복잡한 null 체크 코드를 작성해야 했습니다

C# 14의 해결책

  • 이제 할당 구문에서 ?. 연산자를 직접 사용할 수 있습니다
  • 예: person?.HookUpEvent() - person이 null이면 전체 블록이 실행되지 않음

주의사항

  • null일 경우 부수 효과(side effect)가 발생하지 않을 수 있다는 점을 명확히 이해해야 합니다

1.2 Lambda 표현식의 매개변수 형식 생략

개선 사항

  • 기존: ref, out 같은 매개변수 수정자가 있을 때 형식을 명시해야 함
  • C# 14: 이제 out var success 형식으로 형식 명시 없이 사용 가능
  • 컴파일러가 코드 제안(code fix)으로 자동 수정 가능

실제 활용

  • delegate 내에서 ref struct를 사용하는 등의 패턴에서 코드가 더 간결해짐

2단계: 자동 속성 필드 접근 (Field Access in Auto Properties)

2.1 문제점 해결

기존 방식의 한계

  • C# 3에서 도입된 자동 속성은 필드 선언을 안 해도 되는 편리함을 제공
  • 하지만 setter에서 추가 로직이 필요하면 즉시 "절벽"으로 떨어짐
  • 결국 전체 속성을 수동으로 작성하고 필드를 명시해야 함

C# 14의 혁신

  • 새 키워드 field 도입으로 백킹 필드에 직접 접근 가능
  • 자동 속성과 커스텀 로직을 결합 가능
// C# 14 예시
public string Name
{
    get;
    set
    {
        field = value;  // 암묵적 백킹 필드 접근
        OnNameChanged();
    }
}

2.2 주의사항 및 호환성

Breaking Change 고려

  • field는 예약 키워드가 되어 기존 코드에서 필드 이름으로 사용된 경우 문제 발생
  • 해결책 1: this.field 또는 @field로 명시적 구분
  • 해결책 2: 컴파일러 경고를 통해 인지

2.3 실용적 활용: 지연 초기화 속성

자동 속성 필드 접근은 지연 초기화 패턴에 특히 유용합니다:

public IReadOnlyList<Person> Relatives
{
    get => field ??= [];  // null이면 빈 컬렉션으로 초기화
}

이 방식으로 대량의 속성을 간결하게 구현 가능합니다.


3단계: 확장 멤버 (Extension Members)

3.1 확장 메서드에서 확장 멤버로의 진화

배경

  • C# 3(약 20년 전)에 도입된 확장 메서드는 LINQ 쿼리를 지원하기 위해 만들어짐
  • 그 이후 생태계 전반에서 광범위하게 활용되는 강력한 기능이 됨
  • 개발자들은 오래전부터 "확장 속성(extension properties)"을 요청해옴

설계의 어려움

  • 메서드는 매개변수 리스트가 있어서 확장하기 쉬움
  • 하지만 다른 멤버(속성, 인덱서)는 매개변수 리스트가 없음
  • 제너릭 타입 매개변수와 this 매개변수를 어디에 선언할 것인가의 문제

3.2 확장 블록 (Extension Blocks) - 핵심 해결책

문법 구조

extension IEnumerable<T> where T : INumber<T>
{
    public IEnumerable<T> Range(T start, T count)
    {
        // 구현...
    }
}

주요 특징

  • 확장 대상(IEnumerable<T)을 명확히 선언
  • 제너릭 제약사항(where T : INumber<T)을 명시
  • 컴파일 후에는 정적 클래스 내의 정적 메서드로 생성됨
  • 기존 확장 메서드와 동일하게 동작하지만 더 깔끔한 문법

고정 메모리 구조

extension block
├─ 제너릭 타입 매개변수 선언
├─ 제약사항 선언
└─ 멤버들
    ├─ 메서드
    ├─ 속성
    └─ 인덱서

3.3 확장 속성

extension IEnumerable<T> where T : INumber<T>
{
    public IEnumerable<T> Scaled
    {
        get => this.Select(x => x * 10);
        set { /* 구현... */ }
    }
}

제약사항

  • 인스턴스 필드 불가: 확장 대상이 외부 인스턴스이므로 상태를 추가할 수 없음
  • 필드 기반 자동 속성 불가: 동일한 이유로 백킹 필드를 생성할 수 없음
  • Setter 제약: 사이드 이펙트만 가능하며 값을 저장할 방법이 없음

3.4 정적 확장 (Static Extensions)

확장 블록을 활용한 정적 메서드의 재발견:

extension IEnumerable<int>
{
    public IEnumerable<int> Range(int start, int count)
    {
        for (int i = 0; i < count; i++)
            yield return start + i;
    }
}

// 호출
IEnumerable<int>.Range(1, 10)  // IntelliSense에서 발견 가능

장점

  • 기존: Enumerable.Range() - 발견이 어려움
  • C# 14: IEnumerable<int>.Range() - IntelliSense에서 바로 보임

3.5 제너릭 제약과 수치 연산 (Generic Math)

.NET 7에서 도입된 INumber<T>를 활용한 일반화된 범위 생성:

extension IEnumerable<T> where T : INumber<T>
{
    public IEnumerable<T> Range(T start, T count)
    {
        var current = start;
        for (int i = 0; i < count; i++)
        {
            yield return current;
            current += T.One;
        }
    }
}

// 사용
IEnumerable<int>.Range(1, 10);      // int 버전
IEnumerable<long>.Range(1L, 10);    // long 버전
IEnumerable<double>.Range(1.0, 10); // double 버전

형식 호환성 고려사항

  • intlong에 할당 가능하므로 자동 변환됨
  • uintlong 간에는 음수 범위 차이로 변환 불가
  • 이 경우 별도의 메서드 오버로드 필요

4단계: 확장 연산자 (Extension Operators)

4.1 기본 개념

C# 14에서 처음 도입되는 확장 연산자는 확장 블록 내에서 연산자를 오버로드할 수 있게 합니다.

4.2 예제: 벡터 스칼라 곱셈

extension IEnumerable<T> where T : INumber<T>
{
    public static IEnumerable<T> operator *(IEnumerable<T> vector, T scalar)
    {
        return vector.Select(x => x * scalar);
    }
}

// 사용
var result = Enumerable.Range(1, 20) * 10;
// 결과: [10, 20, 30, ..., 200]

언법 해석

  • IEnumerable<T>를 벡터로 취급
  • * 연산자로 스칼라 곱셈 구현
  • 모든 원소에 일괄 곱셈 적용

4.3 주의사항

  • 연산자 오버로드는 정적 메서드여야 함
  • 일반적인 C# 연산자 오버로드 규칙 준수 필요

5단계: 복합 할당 연산자 (Compound Assignment Operators)

5.1 특징

기존 연산자 오버로드와는 다른 새로운 유형입니다:

차이점

  • 정적이 아닌 인스턴스 멤버
  • 반환값 없음 (void)
  • 객체를 제자리에서 수정
  • 새로운 객체를 생성하는 대신 기존 객체 변경

5.2 구현 예제

extension Array<T> where T : INumber<T>
{
    public static IEnumerable<T> operator *(IEnumerable<T> vector, T scalar)
    {
        return vector.Select(x => x * scalar);
    }

    // 복합 할당 연산자
    public void operator *=(T scalar)
    {
        for (int i = 0; i < this.Length; i++)
        {
            this[i] *= scalar;
        }
    }
}

// 사용
int[] vector = [1, 2, 3, 4, 5];
vector *= 6;  // 각 원소에 6을 곱함
// 결과: [6, 12, 18, 24, 30]

5.3 실용적 의미

  • 성능 최적화: 새 배열을 생성하지 않고 제자리 수정
  • 메모리 효율: 대용량 컬렉션 처리 시 매우 효율적
  • 비용 절감 타입: 생성 비용이 높은 타입에 유용

6단계: 추가 개선 사항

6.1 부분 이벤트 및 생성자 (Partial Events and Constructors)

  • 소스 생성자(source generator) 시나리오를 지원하기 위해 부분(partial) 키워드 확장
  • 생성된 코드와 수동 작성 코드의 통합을 더욱 용이하게 함

6.2 비구속 타입 (Unbound Types) in nameof

  • nameof() 연산자에서 완전히 제너릭 타입 참조 가능
  • 제너릭 형식을 문자열로 얻을 수 있음

실용적인 팁과 주의사항

  1. field 키워드 활용: 지연 초기화 패턴을 구현할 때 field ??= value 형태로 사용하면 매우 효율적
  2. 확장 블록의 발견성: 정적 메서드를 확장 블록으로 변환하면 IntelliSense에서 훨씬 더 잘 발견됨
  3. 제너릭 제약 최대한 활용: INumber<T>, IAdditionOperators<T> 등을 사용하여 광범위하게 적용 가능한 확장 작성

주의사항

  1. Breaking Change 주의: field 키워드는 기존 필드 이름과 충돌할 수 있으므로 기존 코드 검토 필수
  2. 확장 속성의 한계: 상태를 저장할 수 없다는 제약을 명확히 이해
  3. 형식 호환성: 제너릭 제약을 명확히 하지 않으면 예상 밖의 형식 변환 발생 가능
  4. 성능 고려: 복합 할당 연산자가 필요한 경우는 제곱성 대량 데이터 구조에만 사용

코드 예제

예제 1: 자동 속성 필드 접근 - 캐싱 패턴

public class DataService
{
    private List<Data> _cachedData;
    
    public IReadOnlyList<Data> CachedData
    {
        get => field ??= LoadDataFromSource();  // 지연 로드
        set => field = value;
    }
    
    private List<Data> LoadDataFromSource()
    {
        // 비용이 큰 작업
        return FetchFromDatabase();
    }
}

예제 2: 확장 메서드에서 확장 블록으로 리팩토링

// 기존 방식
public static class EnumerableExtensions
{
    public static IEnumerable<T> Between<T>(this IEnumerable<T> source, T min, T max) 
        where T : IComparable<T>
    {
        return source.Where(x => x.CompareTo(min) >= 0 && x.CompareTo(max) <= 0);
    }
}

// C# 14 방식
extension IEnumerable<T> where T : IComparable<T>
{
    public IEnumerable<T> Between(T min, T max)
    {
        return this.Where(x => x.CompareTo(min) >= 0 && x.CompareTo(max) <= 0);
    }
}

예제 3: 확장 연산자와 복합 할당

// 벡터 타입 정의
public struct Vector
{
    public double[] Components;
    
    public Vector(params double[] components)
    {
        Components = components;
    }
}

// 확장 연산자
extension Vector
{
    // 스칼라 곱셈
    public static Vector operator *(Vector v, double scalar)
    {
        var result = new Vector(v.Components.Length);
        for (int i = 0; i < v.Components.Length; i++)
            result.Components[i] = v.Components[i] * scalar;
        return result;
    }
    
    // 인자리 스칼라 곱셈
    public void operator *=(double scalar)
    {
        for (int i = 0; i < Components.Length; i++)
            Components[i] *= scalar;
    }
}

// 사용
Vector v = new(1, 2, 3);
v *= 2;  // [2, 4, 6]으로 제자리 수정

종합 평가

C# 14는 언어 설계의 일관성확장성에 중점을 두고 있습니다. 특히:

  • 자동 속성 필드 접근: 20년간의 “절벽” 문제를 우아하게 해결
  • 확장 블록: 복잡한 문법 구조를 통일된 패턴으로 단순화
  • 확장 연산자: 함수형 프로그래밍과 메타프로그래밍 가능성 대폭 확대

이러한 기능들은 C# 생태계의 코드 품질과 개발자 경험을 한 단계 업그레이드할 것으로 기대됩니다.

4개의 좋아요