Menu

C# 확장 메서드: 문법, IEnumerable, LINQ와 규칙

C#에서 확장 메서드가 동작하는 방식: 첫 매개변수에 this를 붙인 정적 클래스의 정적 메서드, 인스턴스 메서드처럼 호출하기, string과 열거형과 IEnumerable<T> 확장하기, LINQ가 확장 메서드로 만들어진 방식, 인스턴스 메서드가 우선하는 이유, null 수신자, 필요한 using 지시문을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

확장 메서드는 기존 타입을 수정하거나, 상속하거나, 감싸지 않고 그 타입에 메서드를 추가합니다. 평범한 정적 메서드를 작성하고 첫 매개변수에 this를 표시하면, 호출하는 쪽은 그 타입에 원래부터 있던 것처럼 점 문법으로 씁니다:

출력:

Extension methods let yo...
12
short
Extension...

title.Truncate(24)는 정확히 StringExtensions.Truncate(title, 24)로 컴파일됩니다. 점 문법은 편의일 뿐입니다. 메서드는 여전히 정적이고, 여전히 StringExtensions에 있으며, string의 public 멤버만 쓸 수 있습니다.

선언 규칙

  • 메서드는 static이어야 하고, static이며 제네릭이 아니고 다른 클래스 안에 중첩되지 않은 클래스에 있어야 합니다. 정적이 아니거나 제네릭인 클래스는 CS1106 Extension method must be defined in a non-generic static class, 중첩된 클래스는 CS1109 Extension methods must be defined in a top level static class가 됩니다.
  • this는 첫 매개변수에만 붙으며, 그 매개변수의 타입이 확장되는 타입입니다. 나머지 매개변수는 평범합니다.
  • 클래스는 제네릭일 수 없지만 메서드 자체는 제네릭일 수 있습니다(this IEnumerable<T> source).
  • 외부 코드가 보는 것만 봅니다. public 멤버(같은 어셈블리 안이라면 internal 멤버도)입니다. private 필드는 private으로 남습니다.

관례는 확장하는 대상의 이름을 따서 클래스 이름을 짓고(StringExtensions, EnumerableExtensions), MyApp.Extensions 같은 네임스페이스에 모아 두는 것입니다.

인터페이스와 IEnumerable<T> 확장하기

인터페이스를 확장하면 그 인터페이스를 구현하는 모든 타입에 메서드가 추가됩니다. 그래서 IEnumerable<T>를 확장하면 배열, 리스트, 집합, 딕셔너리의 키 컬렉션, LINQ 쿼리 결과에 한꺼번에 메서드가 생깁니다:

출력:

9.5
50
65
f0 f3 f6

EveryNth는 제네릭입니다. T가 수신자로부터 추론되므로 여기서는 문자열에, 그 밖의 어떤 요소 타입에도 동작합니다. yield return을 쓰므로 기본 제공 LINQ 연산자처럼 지연 실행되며, 이들과 연결할 수 있습니다.

LINQ는 확장 메서드로 만들어져 있습니다

Where, Select, OrderBy, Sum, First를 비롯한 LINQ 전체는 정적 클래스 System.Linq.Enumerable에 정의된 IEnumerable<T>의 확장 메서드입니다. 그래서 list.Where(...)가 컴파일되려면 파일 맨 위에 using System.Linq;가 있어야 하고, 어떤 컬렉션도 Where 메서드를 구현하지 않는데도 LINQ가 모든 컬렉션 타입에 동작합니다:

출력:

72, 88, 95
72, 88, 95

두 줄은 같은 코드입니다. 확장 문법은 안에서 밖으로 읽히는 중첩 호출을 단계가 일어나는 순서대로 읽히는 체인으로 바꿔 주며, 이것이 확장 메서드가 존재하는 주된 이유입니다. 확장 메서드는 C# 3에서 LINQ와 함께 추가되었습니다. 연산자는 LINQ 페이지에서 다룹니다.

네임스페이스를 가져와야 합니다

확장 메서드는 정적 클래스의 네임스페이스를 using으로 가져왔을 때만 범위에 있습니다. 가져오지 않으면 메서드가 없는 것처럼 호출이 실패합니다:

error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

확장 메서드 네임스페이스의 using을 추가하세요(C# 10부터는 프로젝트에 한 번 global using으로):

출력:

EUR 49.90

반대로 이 점은 쓸모가 있습니다. 어디서나 쓰고 싶지는 않은 확장 메서드를 일부 파일만 가져오는 네임스페이스에 둘 수 있습니다.

인스턴스 메서드가 우선합니다

타입에 이미 적용 가능한 인스턴스 메서드가 있으면 컴파일러는 그것을 쓰고 확장 메서드는 고려하지 않습니다. 시그니처가 같은 확장 메서드는 점 문법을 쓰는 모든 호출 지점에서 죽은 코드입니다:

출력:

Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call

인수 하나짜리 확장 메서드는 인스턴스 메서드에 졌고, 인수 두 개짜리는 문자열을 받는 인스턴스 메서드가 없으므로 쓰였습니다. 이는 라이브러리 업데이트가 내 확장 메서드와 같은 이름의 인스턴스 메서드를 추가하면 내 호출이 조용히 라이브러리 버전으로 바뀐다는 뜻이기도 합니다. 겹칠 가능성이 낮은 확장 메서드 이름을 고르세요.

null에 대해 호출하기

호출은 실제로 정적 호출이므로 null 수신자는 호출 지점에서 예외를 던지지 않습니다. 메서드는 첫 인수로 null을 받고 그것이 무슨 뜻인지 정합니다:

출력:

True
AL
ArgumentNullException: fullName

missing.IsBlank()가 true를 반환하는 것은 편리하지만, null에 대한 멤버 호출은 예외를 던질 거라고 기대하는 사람을 놀라게 할 수 있습니다. 이름에서 분명히 드러나는 메서드(IsBlank, OrEmpty)에서만 null을 받아들이고, 그 밖에서는 LINQ처럼 ArgumentNullException을 던지세요.

열거형 확장하기

열거형은 자기 메서드를 가질 수 없으므로 확장 메서드의 자연스러운 대상입니다:

enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

static class OrderStatusExtensions
{
    public static bool IsFinal(this OrderStatus s) =>
        s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}

// usage: if (order.Status.IsFinal()) { ... }

C# 14의 확장 멤버

C# 14 이전에는 메서드만 확장이 될 수 있었습니다. C# 14는 한 수신자에 대한 멤버를 묶고 확장 속성과 정적 멤버도 허용하는 extension 블록을 추가했습니다:

// C# 14 and later
public static class StringExtensions
{
    extension(string s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // extension property
        public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
    }
}

전통적인 this 매개변수 확장 메서드도 여전히 유효하며, 기존 코드와 라이브러리 거의 전부가 이것을 씁니다.

언제 작성할까

타입을 소유하지 않을 때(string, DateTime, 프레임워크 인터페이스), 인터페이스의 모든 구현에 같은 헬퍼를 주고 싶을 때, 또는 중첩 호출보다 체인이 더 읽기 좋을 때 확장 메서드가 어울립니다. 클래스를 소유하고 있다면 대신 진짜 인스턴스 메서드를 추가하세요. private 상태를 쓸 수 있고 읽는 사람이 찾아보는 곳에 나타납니다. 그리고 object를 확장하지 마세요. 프로그램의 모든 값의 자동 완성 목록에 메서드가 들어갑니다.

자주 묻는 질문

C#에서 확장 메서드란 무엇인가요?

다른 타입의 인스턴스 메서드인 것처럼 호출할 수 있는 정적 메서드입니다. 정적 클래스 안에 작성하고 첫 매개변수 앞에 this를 붙입니다: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. 그러면 string을 바꾸거나 상속하지 않고도 모든 문자열에서 name.IsBlank()가 동작합니다.

확장 메서드를 찾지 못하는 이유는 무엇인가요?

확장 메서드는 그 네임스페이스를 가져왔을 때만 보입니다. 정적 클래스가 MyApp.Extensions에 있다면 호출하는 파일에 using MyApp.Extensions;를 추가하세요. 그렇지 않으면 컴파일러가 타입에 그런 이름의 정의도, 접근 가능한 확장 메서드도 없다는 CS1061을 보고합니다. 클래스가 static이고, 제네릭이 아니며, 중첩되지 않았는지도 확인하세요.

확장 메서드가 인스턴스 메서드를 재정의할 수 있나요?

아니요. 컴파일러는 적용 가능한 인스턴스 메서드를 먼저 찾고, 없을 때만 확장 메서드를 고려합니다. 인스턴스 메서드와 이름과 매개변수가 같은 확장 메서드는 인스턴스 문법을 쓰는 모든 호출 지점에서 조용히 무시됩니다.

C#에서 null에 대해 확장 메서드를 호출할 수 있나요?

네. 호출은 첫 인수가 null인 정적 메서드 호출로 컴파일되므로 호출 자체에서는 NullReferenceException이 발생하지 않습니다. 무엇을 할지는 메서드가 정합니다. null을 올바른 입력으로 취급하거나(IsNullOrEmpty 스타일 헬퍼처럼), LINQ 메서드처럼 ArgumentNullException을 던집니다.

C#에 확장 속성이 있나요?

C# 14 이전에는 없습니다. 그때까지는 메서드만 확장이 될 수 있으므로, 속성이었으면 하는 것도 메서드로 씁니다(GetFullName()). C# 14는 메서드뿐 아니라 확장 속성과 정적 멤버도 선언할 수 있는 확장 블록을 추가했습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기