다음을 통해 공유


LINQ를 확장하는 방법

모든 LINQ 기반 메서드는 두 가지 유사한 패턴 중 하나를 따릅니다. 열거 가능한 시퀀스를 수행합니다. 다른 시퀀스 또는 단일 값을 반환합니다. 셰이프의 일관성을 사용하면 비슷한 셰이프로 메서드를 작성하여 LINQ를 확장할 수 있습니다. 실제로 .NET 라이브러리는 LINQ가 처음 도입된 이후 많은 .NET 릴리스에서 새로운 메서드를 얻었습니다. 이 문서에서는 동일한 패턴을 따르는 고유한 메서드를 작성하여 LINQ를 확장하는 예제를 볼 수 있습니다.

LINQ 쿼리에 대한 사용자 지정 메서드 추가

인터페이스에 확장 메서드를 추가하여 LINQ 쿼리에 사용하는 메서드 집합을 IEnumerable<T> 확장합니다. 예를 들어 표준 평균 또는 최대 작업 외에도 값 시퀀스에서 단일 값을 계산하는 사용자 지정 집계 메서드를 만듭니다. 또한 값 시퀀스에 대한 사용자 지정 필터 또는 특정 데이터 변환으로 작동하고 새 시퀀스를 반환하는 메서드를 만듭니다. 이러한 메서드의 예는 Distinct, SkipReverse.

인터페이스를 IEnumerable<T> 확장할 때 열거 가능한 컬렉션에 사용자 지정 메서드를 적용할 수 있습니다. 자세한 내용은 확장 메서드를 참조하세요.

집계 메서드는 값 집합에서 단일 값을 계산합니다. LINQ는 , AverageMin.를 비롯한 Max여러 집계 메서드를 제공합니다. 인터페이스에 확장 메서드를 추가하여 고유한 집계 메서드를 IEnumerable<T> 만들 수 있습니다.

C# 14부터 확장 블록을 선언하여 여러 확장 멤버를 포함할 수 있습니다. 키워드 extension 와 수신기 매개 변수를 괄호로 하여 확장 블록을 선언합니다. 다음 코드 예제에서는 확장 블록에서 호출 Median 된 확장 메서드를 만드는 방법을 보여 있습니다. 이 메서드는 형식 double의 숫자 시퀀스에 대한 중앙값을 계산합니다.

extension(IEnumerable<double>? source)
{
    public double Median()
    {
        if (source is null || !source.Any())
        {
            throw new InvalidOperationException("Cannot compute median for a null or empty set.");
        }

        var sortedList =
            source.OrderBy(number => number).ToList();

        int itemIndex = sortedList.Count / 2;

        if (sortedList.Count % 2 == 0)
        {
            // Even number of items.
            return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
        }
        else
        {
            // Odd number of items.
            return sortedList[itemIndex];
        }
    }
}

정적 메서드에 this 한정자를 추가하여 확장 메서드를 선언할 수도 있습니다. 다음 코드는 동등한 Median 확장 메서드를 보여 큽니다.

public static class EnumerableExtension
{
    public static double Median(this IEnumerable<double>? source)
    {
        if (source is null || !source.Any())
        {
            throw new InvalidOperationException("Cannot compute median for a null or empty set.");
        }

        var sortedList =
            source.OrderBy(number => number).ToList();

        int itemIndex = sortedList.Count / 2;

        if (sortedList.Count % 2 == 0)
        {
            // Even number of items.
            return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
        }
        else
        {
            // Odd number of items.
            return sortedList[itemIndex];
        }
    }
}

인터페이스에서 다른 집계 메서드를 호출하는 것과 동일한 방식으로 열거 가능한 컬렉션에 대해 확장 메서드를 IEnumerable<T> 호출합니다.

다음 코드 예제에서는 형식Median의 배열에 메서드를 사용 double 하는 방법을 보여 있습니다.

double[] numbers = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query = numbers.Median();

Console.WriteLine($"double: Median = {query}");
// This code produces the following output:
//     double: Median = 4.85

다양한 형식의 시퀀스를 허용하도록 집계 메서드를 오버로드 할 수 있습니다. 표준 방법은 각 형식에 대한 오버로드를 만드는 것입니다. 또 다른 방법은 제네릭 형식을 사용하는 오버로드를 만들고 대리자를 사용하여 특정 형식으로 변환하는 것입니다. 두 방법을 모두 결합할 수도 있습니다.

지원하려는 각 형식에 대한 특정 오버로드를 만들 수 있습니다. 다음 코드 예제에서는 형식에 Median 대한 메서드의 오버로드를 보여 있습니다 int .

// int overload
public static double Median(this IEnumerable<int> source) =>
    (from number in source select (double)number).Median();

이제 다음 코드와 같이 형식과 Median 형식 모두 integer 에 대한 오버로드를 호출 double 할 수 있습니다.

double[] numbers1 = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query1 = numbers1.Median();

Console.WriteLine($"double: Median = {query1}");

int[] numbers2 = [1, 2, 3, 4, 5];
var query2 = numbers2.Median();

Console.WriteLine($"int: Median = {query2}");
// This code produces the following output:
//     double: Median = 4.85
//     int: Median = 3

제네릭 개체 시퀀스를 허용하는 오버로드를 만들 수도 있습니다. 이 오버로드는 대리자를 매개 변수로 사용하여 제네릭 형식의 개체 시퀀스를 특정 형식으로 변환합니다.

다음 코드는 대리자를 매개 변수로 사용하는 Median 메서드의 Func<T,TResult> 오버로드를 보여줍니다. 이 대리자는 제네릭 형식 T의 개체를 사용하고 형식 double의 개체를 반환합니다.

// generic overload
public static double Median<T>(
    this IEnumerable<T> numbers, Func<T, double> selector) =>
    (from num in numbers select selector(num)).Median();

이제 모든 형식의 Median 개체 시퀀스에 대한 메서드를 호출할 수 있습니다. 형식에 고유한 메서드 오버로드가 없는 경우 대리자 매개 변수를 전달해야 합니다. C#에서는 이 용도로 람다 식을 사용할 수 있습니다. 또한 Visual Basic에서 메서드 호출 대신 또는 Aggregate 절을 사용하는 Group By 경우에만 이 절 범위에 있는 값이나 식을 전달할 수 있습니다.

다음 예제 코드는 정수 배열 및 문자열 배열에 대한 메서드를 호출 Median 하는 방법을 보여 줍니다. 문자열의 경우 배열의 문자열 길이에 대한 중앙값이 계산됩니다. 이 예제에서는 각 사례에 Func<T,TResult> 대한 메서드에 대리자 매개 변수를 Median 전달하는 방법을 보여 있습니다.

int[] numbers3 = [1, 2, 3, 4, 5];

/*
    You can use the num => num lambda expression as a parameter for the Median method
    so that the compiler will implicitly convert its value to double.
    If there is no implicit conversion, the compiler will display an error message.
*/
var query3 = numbers3.Median(num => num);

Console.WriteLine($"int: Median = {query3}");

string[] numbers4 = ["one", "two", "three", "four", "five"];

// With the generic overload, you can also use numeric properties of objects.
var query4 = numbers4.Median(str => str.Length);

Console.WriteLine($"string: Median = {query4}");
// This code produces the following output:
//     int: Median = 3
//     string: Median = 4

값 시 IEnumerable<T>퀀스를 반환하는 사용자 지정 쿼리 메서드를 사용하여 인터페이스를 확장할 수 있습니다. 이 경우 메서드는 형식 IEnumerable<T>의 컬렉션을 반환해야 합니다. 이러한 메서드를 사용하여 값 시퀀스에 필터 또는 데이터 변환을 적용할 수 있습니다.

다음 예제에서는 첫 번째 요소부터 시작하여 컬렉션의 다른 모든 요소를 반환하는 확장 AlternateElements 메서드를 만드는 방법을 보여줍니다.

// Extension method for the IEnumerable<T> interface.
// The method returns every other element of a sequence.
public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
    int index = 0;
    foreach (T element in source)
    {
        if (index % 2 == 0)
        {
            yield return element;
        }

        index++;
    }
}

다음 코드와 같이 인터페이스에서 다른 메서드를 호출하는 것처럼 열거 가능한 컬렉션에 IEnumerable<T> 대해 이 확장 메서드를 호출할 수 있습니다.

string[] strings = ["a", "b", "c", "d", "e"];

var query5 = strings.AlternateElements();

foreach (var element in query5)
{
    Console.WriteLine(element);
}
// This code produces the following output:
//     a
//     c
//     e

이 문서에 표시된 각 예제에는 다른 수신기가 있습니다. 즉, 각 메서드는 고유한 수신기를 지정하는 다른 확장 블록에서 선언되어야 합니다. 다음 코드 예제에서는 세 개의 서로 다른 확장 블록이 있는 단일 정적 클래스를 보여 줍니다. 각 클래스에는 이 문서에 정의된 메서드 중 하나가 포함되어 있습니다.

public static class EnumerableExtension
{
    extension(IEnumerable<double>? source)
    {
        public double Median()
        {
            if (source is null || !source.Any())
            {
                throw new InvalidOperationException("Cannot compute median for a null or empty set.");
            }

            var sortedList =
                source.OrderBy(number => number).ToList();

            int itemIndex = sortedList.Count / 2;

            if (sortedList.Count % 2 == 0)
            {
                // Even number of items.
                return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
            }
            else
            {
                // Odd number of items.
                return sortedList[itemIndex];
            }
        }
    }

    extension(IEnumerable<int> source)
    {
        public double Median() =>
            (from number in source select (double)number).Median();
    }

    extension<T>(IEnumerable<T> source)
    {
        public double Median(Func<T, double> selector) =>
            (from num in source select selector(num)).Median();

        public IEnumerable<T> AlternateElements()
        {
            int index = 0;
            foreach (T element in source)
            {
                if (index % 2 == 0)
                {
                    yield return element;
                }

                index++;
            }
        }
    }
}

최종 확장 블록은 제네릭 확장 블록을 선언합니다. 수신기에 대한 형식 매개 변수는 자체에 extension 선언됩니다.

앞의 예제에서는 각 확장 블록에서 하나의 확장 멤버를 선언합니다. 대부분의 경우 동일한 수신기에 대해 여러 확장 멤버를 만듭니다. 이러한 경우 단일 확장 블록에서 해당 멤버에 대한 확장을 선언해야 합니다.