Compartilhar via


Como estender o LINQ

Todos os métodos baseados em LINQ seguem um dos dois padrões semelhantes. Eles pegam uma sequência enumerável. Eles retornam uma sequência diferente ou um único valor. A consistência da forma permite que você estenda LINQ escrevendo métodos com uma forma semelhante. Na verdade, as bibliotecas do .NET ganharam novos métodos em muitas versões do .NET desde que o LINQ foi introduzido pela primeira vez. Neste artigo, você verá exemplos de extensão do LINQ escrevendo seus próprios métodos que seguem o mesmo padrão.

Adicionar métodos personalizados para consultas LINQ

Estenda o conjunto de métodos usados para consultas LINQ adicionando métodos de extensão à IEnumerable<T> interface. Por exemplo, além da média padrão ou das operações máximas, você cria um método de agregação personalizado para calcular um único valor de uma sequência de valores. Você também cria um método que funciona como um filtro personalizado ou uma transformação de dados específica para uma sequência de valores e retorna uma nova sequência. Exemplos desses métodos são Distinct, Skipe Reverse.

Ao estender a IEnumerable<T> interface, você pode aplicar seus métodos personalizados a qualquer coleção enumerável. Para obter mais informações, consulte Métodos de extensão.

Um método de agregação calcula um único valor de um conjunto de valores. O LINQ fornece vários métodos de agregação, incluindo Average, Mine Max. Você pode criar seu próprio método de agregação adicionando um método de extensão à IEnumerable<T> interface.

A partir do C# 14, você pode declarar um bloco de extensão para conter vários membros de extensão. Você declara um bloco de extensão com a palavra-chave extension seguida pelo parâmetro receptor entre parênteses. O exemplo de código a seguir mostra como criar um método de extensão chamado Median em um bloco de extensão. O método calcula uma mediana para uma sequência de números de tipo 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];
        }
    }
}

Você também pode adicionar o this modificador a um método estático para declarar um método de extensão. O código a seguir mostra o método de extensão equivalente 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];
        }
    }
}

Você chama qualquer método de extensão para qualquer coleção enumerável da mesma forma que chama outros métodos de agregação da IEnumerable<T> interface.

O exemplo de código a seguir mostra como usar o Median método para uma matriz de tipo 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

Você pode sobrecarregar seu método de agregação para que ele aceite sequências de vários tipos. A abordagem padrão é criar uma sobrecarga para cada tipo. Outra abordagem é criar uma sobrecarga que pega um tipo genérico e convertê-lo em um tipo específico, usando um delegado. Você também pode combinar ambas as abordagens.

Você pode criar uma sobrecarga específica para cada tipo que deseja dar suporte. O exemplo de código a seguir mostra uma sobrecarga do Median método para o int tipo.

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

Agora você pode chamar as sobrecargas Median para os tipos integer e double, conforme mostrado no código a seguir:

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

Você também pode criar uma sobrecarga que aceita uma sequência genérica de objetos. Essa sobrecarga usa um delegado como um parâmetro e o usa para converter uma sequência de objetos de um tipo genérico em um tipo específico.

O código a seguir mostra uma sobrecarga do Median método que usa o Func<T,TResult> delegado como um parâmetro. Esse delegado usa um objeto do tipo genérico T e retorna um objeto do tipo double.

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

Agora você pode chamar o Median método para uma sequência de objetos de qualquer tipo. Se o tipo não tiver sua própria sobrecarga de método, você precisará passar um parâmetro delegado. Em C#, você pode usar uma expressão lambda para essa finalidade. Além disso, somente no Visual Basic, se você usar a Aggregate cláusula ou Group By em vez da chamada de método, poderá passar qualquer valor ou expressão que esteja no escopo desta cláusula.

O código de exemplo a seguir mostra como chamar o Median método para uma matriz de inteiros e uma matriz de cadeias de caracteres. Para cadeias de caracteres, a mediana dos comprimentos das cadeias de caracteres na matriz é calculada. O exemplo mostra como passar o Func<T,TResult> parâmetro delegado para o Median método para cada caso.

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

Você pode estender a IEnumerable<T> interface com um método de consulta personalizado que retorna uma sequência de valores. Nesse caso, o método deve retornar uma coleção do tipo IEnumerable<T>. Esses métodos podem ser usados para aplicar filtros ou transformações de dados a uma sequência de valores.

O exemplo a seguir mostra como criar um método de extensão chamado AlternateElements que retorna todos os outros elementos em uma coleção, começando pelo primeiro elemento.

// 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++;
    }
}

Você pode chamar esse método de extensão para qualquer coleção enumerável, assim como você chamaria outros métodos da IEnumerable<T> interface, conforme mostrado no código a seguir:

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

Cada exemplo mostrado neste artigo tem um receptor diferente. Isso significa que cada método deve ser declarado em um bloco de extensão diferente que especifica o receptor exclusivo. O exemplo de código a seguir mostra uma única classe estática com três blocos de extensão diferentes, cada um dos quais contém um dos métodos definidos neste artigo:

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++;
            }
        }
    }
}

O bloco de extensão final declara um bloco de extensão genérico. O parâmetro de tipo para o receptor é declarado no extension ele próprio.

O exemplo anterior declara um membro de extensão em cada bloco de extensão. Na maioria dos casos, você cria vários membros de extensão para o mesmo receptor. Nesses casos, você deve declarar as extensões para esses membros em um único bloco de extensão.