확장 멤버 선언
확장 멤버 선언 (C# 참조)
C#에서 어떤 타입의 소스를 직접 고칠 수 없을 때, 그 타입에 기능을 더해 주는 방법이 있어요. 그게 바로 **확장 멤버(extension member)**예요. C# 14부터는 최상위에 있는 비제네릭 static class에 extension 블록을 써서 확장 멤버를 선언할 수 있는데, 이 멤버는 메서드나 프로퍼티가 될 수 있고 인스턴스 멤버처럼도, 정적 멤버처럼도 보이게 선언할 수 있어요. 그 전 버전의 C#에서는 최상위 비제네릭 정적 클래스에 선언한 정적 메서드의 첫 번째 매개변수에 this 수정자를 붙이는 방식으로 *확장 메서드(extension method)*를 지원했죠.
extension 블록은 확장 멤버에 적용할 타입과 **수신자(receiver)**를 지정해요. 이 extension 선언 안에는 메서드, 프로퍼티, 인덱서, 연산자까지 선언할 수 있답니다. 아래 예시는 인스턴스 확장 메서드 하나와 인스턴스 프로퍼티 하나, 그리고 정적 연산자 메서드 하나를 정의하는 단일 확장 블록을 보여 줘요.
[!NOTE] 이 문서의 모든 예시에는 멤버와 확장 블록에 XML 주석이 달려 있어요.
extension블록의 주석 노드가 확장 대상 타입과 수신자 매개변수를 설명하고, C# 컴파일러는 이 노드를 확장 블록 안의 모든 멤버용으로 생성되는 멤버에 복사해 넣어요. 이 예시들은 확장 멤버의 XML 문서를 생성할 때 권장하는 스타일을 보여 주는 거예요.
public static class NumericSequences
{
/// <summary>
/// Defines extensions for integer sequences.
/// </summary>
/// <param name="sequence">The sequence used as a receiver.</param>
extension(IEnumerable<int> sequence)
{
/// <summary>
/// Adds a scalar value to each element in the sequence.
/// </summary>
/// <param name="operand">The amount to add.</param>
/// <returns>
/// A new sequence where each value contains the updated value.
/// </returns>
public IEnumerable<int> AddValue(int operand)
{
foreach (var item in sequence)
{
yield return item + operand;
}
}
/// <summary>
/// Gets the median value of the sequence.
/// </summary>
/// <remarks>
/// This value is calculated when requested.
/// </remarks>
public int Median
{
get
{
var sortedList = sequence.OrderBy(n => n).ToList();
int count = sortedList.Count;
int middleIndex = count / 2;
if (count % 2 == 0)
{
// Even number of elements: average the two middle elements
return (sortedList[middleIndex - 1] + sortedList[middleIndex]) / 2;
}
else
{
// Odd number of elements: return the middle element
return sortedList[middleIndex];
}
}
}
/// <summary>
/// Concatenates two sequences of integers into a single sequence.
/// </summary>
/// <remarks>The resulting sequence enumerates all elements from <paramref name="left"/> in order,
/// followed by all elements from <paramref name="right"/>. Enumeration is deferred and performed lazily as the
/// returned sequence is iterated.</remarks>
/// <param name="left">The first sequence of integers to concatenate. Cannot be null.</param>
/// <param name="right">The second sequence of integers to concatenate. Cannot be null.</param>
/// <returns>A sequence that contains the elements of the first sequence followed by the
/// elements of the second sequence.</returns>
public static IEnumerable<int> operator +(IEnumerable<int> left, IEnumerable<int> right)
=> left.Concat(right);
}
}
여기서 extension이 수신자 sequence를 정의해요. sequence는 IEnumerable<int> 타입이죠. 수신자 타입은 비제네릭이거나 개방 제네릭(open generic), 폐쇄 제네릭(closed generic) 타입 모두 될 수 있어요. sequence라는 이름은 그 확장 안에 선언된 모든 인스턴스 멤버의 범위 안에서 쓸 수 있고, 확장 메서드와 프로퍼티 모두 이 sequence에 접근해요.
확장 멤버에 접근하는 방법은 마치 수신자 타입의 멤버인 것처럼 하면 돼요.
IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);
var median = numbers.Median;
var combined = numbers + Enumerable.Range(100, 10);
하나의 블록에는 수신자를 공유하는 한 멤버를 몇 개든 선언할 수 있고, 하나의 클래스에 확장 블록도 여러 개를 둘 수 있어요. 서로 다른 확장이 꼭 같은 타입이나 이름의 수신자를 쓸 필요도 없어요. 확장 매개변수는 멤버가 전부 정적인 경우라면 매개변수 이름조차 생략할 수 있어요.
public static class NumericStaticExtensions
{
extension(IEnumerable<int>)
{
// Method:
public static IEnumerable<int> Generate(int low, int count, int increment)
{
for (int i = 0; i < count; i++)
yield return low + (i * increment);
}
// Property:
public static IEnumerable<int> Identity => Enumerable.Empty<int>();
}
}
정적 확장은 마치 수신자 타입의 정적 멤버인 것처럼 호출해요.
var newSequence = IEnumerable<int>.Generate(5, 10, 2);
var identity = IEnumerable<int>.Identity;
연산자는 그 타입에 사용자 정의 연산자가 있는 것처럼 호출하면 돼요.
[!IMPORTANT] 확장은 멤버 선언에 *범위(scope)*를 도입하지 않아요. 하나의 클래스에 선언된 모든 멤버는 여러 확장에 걸쳐 있더라도 반드시 고유한 시그니처를 가져야 해요. 생성되는 시그니처는 정적 멤버의 경우 수신자 타입을 이름에 포함하고, 확장 인스턴스 멤버의 경우 수신자 매개변수를 포함해요.
다음 예시는 this 수정자를 사용하는 확장 메서드를 보여 줘요.
public static class NumericSequenceExtensionMethods
{
public static IEnumerable<int> AddValue(this IEnumerable<int> sequence, int operand)
{
foreach (var item in sequence)
yield return item + operand;
}
}
AddValue 메서드는 IEnumerable<int> 인터페이스의 멤버인 것처럼 다른 어떤 메서드에서도 호출할 수 있어요.
IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);
두 형태의 확장 메서드는 같은 중간 언어(IL)로 생성돼요. 호출하는 쪽에서는 둘을 전혀 구분할 수 없죠. 실제로 기존 확장 메서드를 새로운 멤버 구문으로 바꿔도 중단 변경(breaking change)이 생기지 않아요. 두 형식 모두 바이너리 호환과 소스 호환이 돼요.
제네릭 확장 블록
확장 블록에 선언한 확장 멤버의 형식 매개변수를 어디에 두느냐는 그 형식 매개변수가 필요한 위치에 따라 정해져요.
- 형식 매개변수가 수신자에서 쓰이면
extension선언에 그 형식 매개변수를 추가해요. - 형식이 수신자에 지정된 형식 매개변수와 구별된다면 멤버 선언에 형식 매개변수를 추가해요.
- 같은 형식 매개변수를 양쪽 모두에 지정할 수는 없어요.
다음 예시는 두 확장 멤버가 추가 형식 매개변수를 하나 더 필요로 하는 IEnumerable<T> 확장 블록을 보여 줘요.
public static class GenericExtensions
{
extension<TReceiver>(IEnumerable<TReceiver> source)
{
public IEnumerable<TReceiver> Spread(int start, int count)
=> source.Skip(start).Take(count);
public IEnumerable<TReceiver> Append<TArg>(IEnumerable<TArg> second, Func<TArg, TReceiver> Converter)
{
foreach(TReceiver item in source)
{
yield return item;
}
foreach (TArg item in second)
{
yield return Converter(item);
}
}
public IEnumerable<TReceiver> Prepend<TArg>(IEnumerable<TArg> second, Func<TArg, TReceiver> converter)
{
foreach (TArg item in second)
{
yield return converter(item);
}
foreach (TReceiver item in source)
{
yield return item;
}
}
public static IEnumerable<TReceiver> Identity => Enumerable.Empty<TReceiver>();
}
}
Append와 Prepend 멤버는 변환에 필요한 추가 형식 매개변수를 지정하지만, 어떤 멤버도 수신자의 형식 매개변수를 다시 반복하지 않아요.
동일한 내용을 확장 메서드로 표현하면 그 형식 매개변수가 어떻게 인코딩되는지가 드러나요.
public static class GenericExtensions
{
public static IEnumerable<T> Spread<T>(this IEnumerable<T> source, int start, int count)
=> source.Skip(start).Take(count);
public static IEnumerable<T1> Append<T1, T2>(this IEnumerable<T1> source, IEnumerable<T2> second, Func<T2, T1> Converter)
{
foreach (T1 item in source)
{
yield return item;
}
foreach (T2 item in second)
{
yield return Converter(item);
}
}
public static IEnumerable<T1> Prepend<T1, T2>(this IEnumerable<T1> source, IEnumerable<T2> second, Func<T2, T1> Converter)
{
foreach (T2 item in second)
{
yield return Converter(item);
}
foreach (T1 item in source)
{
yield return item;
}
}
}
확장 인덱서
C# 15부터는 extension 블록 안에 인덱서를 선언할 수 있어요. 확장 인덱서는 마치 인덱서가 수신자 타입에 선언된 것처럼 수신자를 인덱싱할 수 있게 해 줘요. 인덱서는 항상 인스턴스 멤버이기 때문에, 인덱서를 선언하는 확장 블록은 반드시 이름 있는 수신자 매개변수를 제공해야 해요. 확장 인덱서는 get/set 접근자, 식 본문(expression-bodied) 접근자, ref 반환 접근자까지 일반 인덱서와 동일한 기능을 지원해요.
다음 예시는 IEnumerable<int>에 지정된 위치의 요소를 반환하는 get 전용 인덱서를 선언해요.
public static class SequenceIndexer
{
extension(IEnumerable<int> sequence)
{
public int this[int index] => sequence.ElementAt(index);
}
}
수신자 타입의 멤버인 것처럼 수신자를 인덱싱하면 돼요.
int third = numbers[2];