다음을 통해 공유


확장 멤버(C# 프로그래밍 가이드)

확장 멤버를 사용하면 새 파생 형식을 만들거나, 다시 컴파일하거나, 원래 형식을 수정하지 않고도 기존 형식에 메서드를 "추가"할 수 있습니다.

C# 14부터 확장 메서드를 정의하는 데 사용하는 두 가지 구문이 있습니다. C# 14는 형식 또는 형식 인스턴스에 대해 여러 확장 멤버를 정의하는 컨테이너를 추가 extension 합니다. C# 14 이전에는 정적 메서드의 첫 번째 매개 변수에 한정자를 추가하여 this 메서드가 매개 변수 형식 인스턴스의 멤버로 표시됨을 나타냅니다.

확장 메서드는 정적 메서드이지만 확장 형식의 인스턴스 메서드인 것처럼 호출됩니다. C#, F# 및 Visual Basic으로 작성된 클라이언트 코드의 경우 확장 메서드 호출과 형식에 정의된 메서드 간에 명백한 차이가 없습니다. 두 형태의 확장 메서드는 동일한 IL(중간 언어)로 컴파일됩니다. 확장 멤버의 소비자는 확장 메서드를 정의하는 데 사용된 구문을 알 필요가 없습니다.

가장 일반적인 확장 멤버는 기존 System.Collections.IEnumerableSystem.Collections.Generic.IEnumerable<T> 형식에 쿼리 기능을 추가하는 LINQ 표준 쿼리 연산자입니다. 표준 쿼리 연산자를 사용하려면 먼저 using System.Linq 디렉티브를 사용하여 범위로 가져옵니다. 그런 다음 구현하는 IEnumerable<T> 모든 형식에 인스턴스 메서드(예: GroupBy, OrderByAverage등)가 있는 것으로 나타납니다. 이러한 추가 메서드는 IntelliSense 문 완성에서 다음과 같은 IEnumerable<T>List<T>형식의 Array 인스턴스 뒤 "dot"를 입력할 때 확인할 수 있습니다.

OrderBy 예제

다음 예제에서는 정수 배열에서 표준 쿼리 연산자 OrderBy 메서드를 호출하는 방법을 보여 줍니다. 괄호 안의 식은 람다 표현식입니다. 많은 표준 쿼리 연산자는 람다 식을 매개 변수로 사용합니다. 자세한 내용은 람다 식을 참조하세요.

int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
    Console.Write(i + " ");
}
//Output: 10 15 21 26 39 45

확장 메서드는 정적 메서드로 정의되지만 인스턴스 메서드 구문을 사용하여 호출됩니다. 첫 번째 매개 변수는 메서드가 작동하는 형식을 지정합니다. 매개 변수는 한정자를 따릅니다. 확장 메서드는 지시문을 사용하여 네임스페이스를 소스 코드로 명시적으로 가져올 때만 범위에 있습니다 using .

확장 멤버 선언

C# 14부터 확장 블록을 선언할 수 있습니다. 확장 블록은 형식 또는 해당 형식의 인스턴스에 대한 확장 멤버를 포함하는 중첩되지 않은 비제네릭 정적 클래스의 블록입니다. 다음 코드 예제에서는 형식에 대한 확장 블록을 정의합니다 string . 확장 블록에는 문자열의 단어를 계산하는 메서드인 하나의 멤버가 포함됩니다.

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

C# 14 이전에는 첫 번째 매개 변수에 한정자를 추가하여 this 확장 메서드를 선언합니다.

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

두 형태의 확장은 중첩되지 않은 비제네릭 정적 클래스 내에서 정의되어야 합니다.

또한 인스턴스 멤버에 액세스하기 위한 구문을 사용하여 애플리케이션에서 호출할 수 있습니다.

string s = "Hello Extension Methods";
int i = s.WordCount();

확장 멤버는 기존 형식에 새 기능을 추가하지만 확장 멤버는 캡슐화 원칙을 위반하지 않습니다. 확장 형식의 모든 멤버에 대한 액세스 선언은 확장 멤버에 적용됩니다.

MyExtensions 클래스와 WordCount 메서드는 static이며, 다른 모든 static 멤버처럼 액세스할 수 있습니다. 메서드는 WordCount 다음과 같이 다른 static 메서드와 같이 호출할 수 있습니다.

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

위의 C# 코드는 확장 블록과 this 확장 멤버의 구문 모두에 적용됩니다. 앞의 코드는 다음과 같습니다.

  • 새로운 string을(를) s이라는 이름으로 선언하고 "Hello Extension Methods" 값을 할당합니다.
  • MyExtensions.WordCount 주어진 인수 s를 호출합니다.

자세한 내용은 사용자 지정 확장 메서드를 구현하고 호출하는 방법을 참조하세요.

일반적으로 확장 멤버를 구현하는 것보다 훨씬 더 자주 호출할 수 있습니다. 확장 멤버는 확장 클래스의 멤버로 선언된 것처럼 호출되므로 클라이언트 코드에서 사용하기 위해 특별한 지식이 필요하지 않습니다. 특정 형식에 확장 멤버를 사용하도록 설정하려면 메서드가 using 정의된 네임스페이스에 대한 지시문을 추가하기만 하면됩니다. 예를 들어 표준 쿼리 연산자를 사용하려면 코드에 다음 using 지시문을 추가합니다.

using System.Linq;

컴파일 시간에 확장 멤버 바인딩

확장 멤버를 사용하여 클래스 또는 인터페이스를 확장할 수 있지만 클래스에 정의된 동작을 재정의할 수는 없습니다. 인터페이스 또는 클래스 멤버와 이름 및 서명이 같은 확장 멤버는 호출되지 않습니다. 컴파일 시 확장 멤버는 항상 형식 자체에 정의된 인스턴스(또는 정적) 멤버보다 우선 순위가 낮습니다. 즉, 형식에 이름이 지정된 Process(int i)메서드가 있고 시그니처가 같은 확장 메서드가 있는 경우 컴파일러는 항상 멤버 메서드에 바인딩됩니다. 컴파일러가 멤버 호출을 발견하면 먼저 형식의 멤버에서 일치 항목을 찾습니다. 일치하는 항목이 없으면 형식에 대해 정의된 확장 멤버를 검색합니다. 찾은 첫 번째 확장 멤버에 바인딩됩니다. 다음 예제에서는 C# 컴파일러가 형식의 인스턴스 멤버 또는 확장 멤버에 바인딩할지 여부를 결정할 때 따르는 규칙을 보여 줍니다. 정적 클래스 Extensions 에는 다음을 구현하는 모든 형식에 대해 정의된 확장 멤버가 포함됩니다.IMyInterface

public interface IMyInterface
{
    void MethodB();
}

// Define extension methods for IMyInterface.

// The following extension methods can be accessed by instances of any
// class that implements IMyInterface.
public static class Extension
{
    public static void MethodA(this IMyInterface myInterface, int i) =>
        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

    public static void MethodA(this IMyInterface myInterface, string s) =>
        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");

    // This method is never called in ExtensionMethodsDemo1, because each
    // of the three classes A, B, and C implements a method named MethodB
    // that has a matching signature.
    public static void MethodB(this IMyInterface myInterface) =>
        Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
}

C# 14 확장 멤버 구문을 사용하여 해당 확장을 선언할 수 있습니다.

public static class Extension
{
    extension(IMyInterface myInterface)
    {
        public void MethodA(int i) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

        public void MethodA(string s) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");

        // This method is never called in ExtensionMethodsDemo1, because each
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public void MethodB() =>
            Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
    }
}

클래스 A, B, 및 C 모두 인터페이스를 구현합니다.

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
class A : IMyInterface
{
    public void MethodB() { Console.WriteLine("A.MethodB()"); }
}

class B : IMyInterface
{
    public void MethodB() { Console.WriteLine("B.MethodB()"); }
    public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}

class C : IMyInterface
{
    public void MethodB() { Console.WriteLine("C.MethodB()"); }
    public void MethodA(object obj)
    {
        Console.WriteLine("C.MethodA(object obj)");
    }
}

MethodB 확장 메서드는 이름 및 서명이 클래스에서 이미 구현한 메서드와 정확히 일치하기 때문에 호출되지 않습니다. 컴파일러가 일치하는 서명이 있는 인스턴스 메서드를 찾을 수 없는 경우 일치하는 확장 메서드에 바인딩됩니다(있는 경우).

// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();

// For a, b, and c, call the following methods:
//      -- MethodA with an int argument
//      -- MethodA with a string argument
//      -- MethodB with no argument.

// A contains no MethodA, so each call to MethodA resolves to
// the extension method that has a matching signature.
a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

// A has a method that matches the signature of the following call
// to MethodB.
a.MethodB();            // A.MethodB()

// B has methods that match the signatures of the following
// method calls.
b.MethodA(1);           // B.MethodA(int)
b.MethodB();            // B.MethodB()

// B has no matching method for the following call, but
// class Extension does.
b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

// C contains an instance method that matches each of the following
// method calls.
c.MethodA(1);           // C.MethodA(object)
c.MethodA("hello");     // C.MethodA(object)
c.MethodB();            // C.MethodB()
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

일반적인 사용 패턴

컬렉션 기능

이전에는 지정된 형식에 대한 인터페이스를 구현 System.Collections.Generic.IEnumerable<T> 하고 해당 형식의 컬렉션에서 작동하는 기능을 포함하는 "컬렉션 클래스"를 만드는 것이 일반적이었습니다. 이러한 유형의 컬렉션 개체를 만드는 데는 아무런 문제가 없지만, System.Collections.Generic.IEnumerable<T>에 대한 확장을 사용하여 동일한 기능을 수행할 수 있습니다. 확장은 해당 형식에 대해 System.Array를 구현하는 System.Collections.Generic.List<T>System.Collections.Generic.IEnumerable<T>과 같은 모든 컬렉션에서 기능을 호출할 수 있는 장점이 있습니다. Int32 배열을 사용하는 이 예제는 이 문서의 앞부분에서 찾을 수 있습니다.

레이어 별 기능

Onion 아키텍처 또는 기타 계층화된 애플리케이션 디자인을 사용하는 경우 애플리케이션 경계를 넘어 통신하는 데 사용할 수 있는 도메인 엔터티 또는 데이터 전송 개체 집합이 있는 것이 일반적입니다. 이러한 개체는 일반적으로 기능이 없거나, 모든 애플리케이션 계층에 적용될 수 있는 최소한의 기능만을 포함합니다. 확장 메서드를 사용하여 각 애플리케이션 계층과 관련된 기능을 추가할 수 있습니다.

public class DomainEntity
{
    public int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

새 확장 블록 구문을 사용하여 C# 14 이상에서 해당 FullName 속성을 선언할 수 있습니다.

static class DomainEntityExtensions
{
    extension(DomainEntity value)
    {
        string FullName => $"{value.FirstName} {value.LastName}";
    }
}

미리 정의된 형식 확장

재사용 가능한 기능을 만들어야 할 때 새 개체를 만드는 대신 .NET 또는 CLR 형식과 같은 기존 형식을 확장할 수 있습니다. 예를 들어, 확장 메서드를 사용하지 않는 경우, 코드의 여러 위치에서 호출될 수 있는 SQL Server에서 쿼리를 실행하는 작업을 수행하는 Engine 또는 Query 클래스를 만들 수 있습니다. 그러나 확장 메서드를 사용하여 클래스를 System.Data.SqlClient.SqlConnection 확장하여 SQL Server에 연결된 모든 위치에서 해당 쿼리를 수행할 수 있습니다. 다른 예로는 클래스에 공통 기능을 System.String 추가하고, 개체의 System.IO.Stream 데이터 처리 기능을 확장하고, System.Exception 특정 오류 처리 기능에 대한 개체를 확장할 수 있습니다. 이러한 유형의 사용 사례는 상상력과 감각에 의해서만 제한됩니다.

미리 정의된 형식은 값으로 메서드에 struct 전달되므로 형식을 확장하기 어려울 수 있습니다. 즉, 구조체에 대한 변경 사항은 구조체의 복사본에 적용됩니다. 확장 메서드가 종료되면 이러한 변경 내용은 표시되지 않습니다. 첫 번째 인수에 ref 한정자를 추가하여 확장 메서드로 ref 만들 수 있습니다. 키워드 refthis 키워드 앞이나 뒤에 위치할 수 있으며, 의미상의 차이는 없습니다. ref 수정자를 추가하면 첫 번째 인수가 참조로 전달된다는 것을 나타냅니다. 이 기술을 사용하면 확장 중인 구조체의 상태를 변경하는 확장 메서드를 작성할 수 있습니다(프라이빗 멤버에 액세스할 수 없음). 구조체로 제한되는 값 형식 또는 제네릭 형식만 확장 메서드의 첫 번째 매개 변수나 확장 블록의 수신기로 사용할 수 있습니다. 이러한 규칙에 대한 자세한 내용은 제약 조건을 참조하십시오. 다음 예제에서는 확장 메서드를 ref 사용하여 결과를 다시 할당하거나 키워드를 사용하여 함수 ref 를 통해 전달할 필요 없이 기본 제공 형식을 직접 수정하는 방법을 보여줍니다.

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

해당하는 확장 블록은 다음 코드에 나와 있습니다.

public static class IntExtensions
{
    extension(int number)
    {
        public void Increment()
            => number++;
    }

    // Take note of the extra ref keyword here
    extension(ref int number)
    {
        public void RefIncrement()
            => number++;
    }
}

수신기에 대한 값별 및 참조별 매개 변수 모드를 구분하려면 다른 확장 블록이 필요합니다.

다음 예제에서는 수신기에 적용되는 ref 차이점을 확인할 수 있습니다.

int x = 1;

// Takes x by value leading to the extension method
// Increment modifying its own copy, leaving x unchanged
x.Increment();
Console.WriteLine($"x is now {x}"); // x is now 1

// Takes x by reference leading to the extension method
// RefIncrement changing the value of x directly
x.RefIncrement();
Console.WriteLine($"x is now {x}"); // x is now 2

사용자 정의 구조체 형식에 확장 멤버를 추가하여 ref 동일한 기술을 적용할 수 있습니다.

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

위의 샘플은 C# 14의 확장 블록을 사용하여 만들 수도 있습니다.

public static class AccountExtensions
{
    extension(ref Account account)
    {
        // ref keyword can also appear before the this keyword
        public void Deposit(float amount)
        {
            account.balance += amount;

            // The following line results in an error as an extension
            // method is not allowed to access private members
            // account.secret = 1; // CS0122
        }
    }
}

다음과 같이 이러한 확장 메서드에 액세스할 수 있습니다.

Account account = new()
{
    id = 1,
    balance = 100f
};

Console.WriteLine($"I have ${account.balance}"); // I have $100

account.Deposit(50f);
Console.WriteLine($"I have ${account.balance}"); // I have $150

일반 지침

적절하고 가능할 때마다 개체의 코드를 수정하거나 새 형식을 파생하여 기능을 추가하는 것이 좋습니다. 확장 메서드는 .NET 에코시스템 전체에서 재사용 가능한 기능을 만드는 데 중요한 옵션입니다. 원래 원본이 제어되지 않거나 파생 개체가 부적절하거나 불가능한 경우 또는 기능에 범위가 제한된 경우 확장 멤버를 사용하는 것이 좋습니다.

파생 형식에 대한 자세한 내용은 상속을 참조 하세요.

지정된 형식에 대한 확장 메서드를 구현하는 경우 다음 사항을 기억하세요.

  • 형식에 정의된 메서드와 동일한 시그니처가 있는 경우 확장 메서드가 호출되지 않습니다.
  • 확장 메서드는 네임스페이스 수준에서 범위로 가져옵니다. 예를 들어 명명 Extensions된 단일 네임스페이스에 확장 메서드를 포함하는 여러 정적 클래스가 있는 경우 모두 지시문에 의해 using Extensions; 범위로 가져옵니다.

구현한 클래스 라이브러리의 경우 어셈블리의 버전 번호가 증가하지 않도록 확장 메서드를 사용하면 안 됩니다. 소스 코드를 소유한 라이브러리에 중요한 기능을 추가하려면 어셈블리 버전 관리의 .NET 지침을 따르세요. 자세한 내용은 어셈블리 버전 관리를 참조하세요.

참고하십시오