Partilhar via


Escreve as tuas próprias extensões para o LINQ

Todos os métodos baseados em LINQ seguem um de dois padrões semelhantes. Eles utilizam uma sequência enumerável. Eles devolvem ou uma sequência diferente ou um único valor. A consistência da forma permite estender o LINQ escrevendo métodos com uma forma semelhante. Na verdade, as bibliotecas .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

Expanda o conjunto de métodos que utiliza para consultas LINQ adicionando métodos de extensão à IEnumerable<T> interface. Por exemplo, além das operações médias ou máximas padrão, você cria um método de agregação personalizado para calcular um único valor a partir 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 de tais métodos são Distinct, Skip, e 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 Membros de extensão.

Um método agregado calcula um único valor a partir de um conjunto de valores. O LINQ fornece vários métodos agregados, incluindo Average, Mine Max. Crie o seu próprio método de agregado 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. Declare um bloco de extensão com a palavra-chave extension seguida do 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 do 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];
        }
    }
}

Chame qualquer um dos métodos de extensão para qualquer coleção enumerável da mesma forma que chama outros métodos agregados a partir da IEnumerable<T> interface.

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

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

Cria uma sobrecarga específica para cada tipo que queres suportar. 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 pode chamar as Median sobrecargas para ambos 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 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 método Median que recebe o delegado Func<T,TResult> como parâmetro. Este delegado pega 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 a sua própria sobrecarga de métodos, passe um parâmetro delegado. Em C#, você pode usar uma expressão lambda para essa finalidade. Além disso, apenas no Visual Basic, se usares a Aggregate cláusula ou Group By em vez da chamada de método, podes passar qualquer valor ou expressão que esteja no âmbito 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, é calculada a mediana dos comprimentos das cadeias de caracteres na matriz. O exemplo mostra como passar o Func<T,TResult> parâmetro delegate 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

Estenda a IEnumerable<T> interface com um método de consulta personalizado que devolve uma sequência de valores. Nesse caso, o método deve retornar uma coleção do tipo IEnumerable<T>. Tais 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 a partir do 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++;
    }
}

Chame este método de extensão para qualquer coleção enumerável, tal como chamaria outros métodos da IEnumerable<T> interface, como mostrado no seguinte código:

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 recetor diferente. Isto significa que deve declarar cada método num bloco de extensão diferente que especifica o recetor único. 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 recetor é declarado no próprio extension.

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 recetor. Nesses casos, declare as extensões desses membros num único bloco de extensão.