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 버전
형식 호환성 고려사항
int를long에 할당 가능하므로 자동 변환됨uint와long간에는 음수 범위 차이로 변환 불가- 이 경우 별도의 메서드 오버로드 필요
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()연산자에서 완전히 제너릭 타입 참조 가능- 제너릭 형식을 문자열로 얻을 수 있음
실용적인 팁과 주의사항
팁
field키워드 활용: 지연 초기화 패턴을 구현할 때field ??= value형태로 사용하면 매우 효율적- 확장 블록의 발견성: 정적 메서드를 확장 블록으로 변환하면 IntelliSense에서 훨씬 더 잘 발견됨
- 제너릭 제약 최대한 활용:
INumber<T>,IAdditionOperators<T>등을 사용하여 광범위하게 적용 가능한 확장 작성
주의사항
- Breaking Change 주의:
field키워드는 기존 필드 이름과 충돌할 수 있으므로 기존 코드 검토 필수 - 확장 속성의 한계: 상태를 저장할 수 없다는 제약을 명확히 이해
- 형식 호환성: 제너릭 제약을 명확히 하지 않으면 예상 밖의 형식 변환 발생 가능
- 성능 고려: 복합 할당 연산자가 필요한 경우는 제곱성 대량 데이터 구조에만 사용
코드 예제
예제 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# 생태계의 코드 품질과 개발자 경험을 한 단계 업그레이드할 것으로 기대됩니다.