LINQ  - Adicionando extensões funcionais a consultas

Hoje veremos como incluir extensões funcionais em consultas LINQ usando C#.

Existem operações que muitas vezes são executadas em coleções e que residem em classes de utilitários.

Você gostaria que essas operações fossem usadas nas coleções de uma maneira mais uniforme do que ter que passar a referência à coleção para a classe de utilitário.

Como podemos fazer isso ?

Você estende o conjunto de métodos que usa para consultas LINQ adicionando métodos de extensão à interface IEnumerable<T>. 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 de tais métodos são Distinct, Skip e Reverse.

Dessa forma podemos usar métodos de extensão para ajudar a alcançar um estilo de programação mais funcional para usar em operações com coleções. Ao estender a interface IEnumerable<T>, você pode aplicar seus métodos personalizados a qualquer coleção enumerável.

Os métodos de extensão ou extensions methods são uma implementação do padrão de projeto estrutural Composite e permitem que você adicione uma nova funcionalidade a um tipo de dado que já foi definido sem ter que criar um novo tipo derivado; dessa forma a funcionalidade se comporta como um outro membro do tipo.

Essa é uma forma de aplicar o padrão aberto/fechado que diz : 'Você deve ser capaz de estender um comportamento de uma classe, sem modificá-la.'

Adicionando um método Agregado

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

Vejamos um exemplo de código que mostra como criar um método de extensão chamado Mediana para calcular uma mediana para uma sequência de números do tipo double.

Para implementar um método de extensão na linguagem C# basta seguir o roteiro:

  1. Defina uma classe estática para conter o método de extensão. (Esta classe deve estar visível para o código cliente.)
  2. Implemente o método de extensão como um método estático com pelo menos a mesma visibilidade que a classe que a contém;
  3. O primeiro parâmetro do método especifica o tipo no qual o método opera. Ele deve ser precedido pelo modificador this.
  4. No código de chamada, adicione uma diretiva using para especificar o namespace que contém a classe do método de extensão.
  5. Chame os métodos como se fossem métodos de instância no tipo.

Para isso vamos criar um projeto Console do tipo .NET Core usando o .NET 5.0 e no projeto criar uma pasta chamada Extensions.

A seguir nesta pasta crie a classe estática LINQExtension e define o método estático Mediana:

    public static class LINQExtension
    {
        public static double Mediana(this IEnumerable<double> colecao)
        {
            if (!(colecao?.Any() ?? false))
            {
                throw new InvalidOperationException("Mediana não pode ser calculada para valores nulos ou um conjunto vazio.");
            }
            var listaOrdenada = (from numero in colecao
                                            orderby numero
                                            select numero).ToList();
            int itemIndex = listaOrdenada.Count / 2;
            if (listaOrdenada.Count % 2 == 0)
            {
                // número par de itens
                return (listaOrdenada[itemIndex] + listaOrdenada[itemIndex - 1]) / 2;
            }
            else
            {
                // número impar de itens
                return listaOrdenada[itemIndex];
            }
        }
    }

Para testar este método vamos definir o código a seguir no método Main da classe Programa onde vamos chamar o método de extensão para qualquer coleção enumerável da mesma maneira que chama outros métodos agregados da interface IEnumerable<T>.

   using Linq_AddExtensions.Extensions;
   using System;

    class Program
    {
        static void Main(string[] args)
        {
            double[] numerosDouble = { 13.5, 17.8, 92.3, 0.1, 15.7,19.99, 9.08, 6.33, 2.1, 14.88 };

            var resultado = numerosDouble.Mediana();
            Console.WriteLine("Cálculo da mediana para a sequência : \n");
            Console.WriteLine(" 13.5, 17.8, 92.3, 0.1, 15.7,19.99, 9.08, 6.33, 2.1, 14.88 \n");
            Console.WriteLine($"Resultado da Mediana : {resultado}");
            Console.ReadLine();
        }
    }

O resultado pode ser visto a seguir:



Tudo bem, mas e se quisermos calcular a media para números inteiros ?

É isso que vamos mostrar a seguir...

Sobrecarregando um método agregado para aceitar vários tipos

Podemos sobrecarregar seu método agregado 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 pegará um tipo genérico e o converterá em um tipo específico usando um delegado. Você também pode combinar as duas abordagens.

A seguir temos o código usado para criar uma sobrecarga que calcula a Mediana para números do tipos int :


  
        public static double Mediana(this IEnumerable<int> colecao) =>
                        (from num in colecao select (double)num).Mediana();
 

Agora podemos invocar as sobrecargas do método Mediana para os tipos int e double:


  
      static void Main(string[] args)
        {
            double[] numerosDouble = { 13.5, 17.8, 92.3, 0.1, 15.7,19.99, 9.08, 6.33, 2.1, 14.88 };
            var resultado = numerosDouble.Mediana();
            Console.WriteLine("Cálculo da mediana para a sequência : \n");
            Console.WriteLine(" 13.5, 17.8, 92.3, 0.1, 15.7,19.99, 9.08, 6.33, 2.1, 14.88 \n");
            Console.WriteLine($"Resultado da Mediana : {resultado}\n\n");
            int[] numerosInt = { 61, 52, 43, 34, 25 };
            var resultado2 = numerosInt.Mediana();
            Console.WriteLine("Cálculo da mediana para a sequência : \n");
            Console.WriteLine("  61, 52, 43, 34, 25 \n");
            Console.WriteLine($"Resultado da Mediana : {resultado2}");
            Console.ReadLine();
        }

O resultado é o seguinte :

Podemos criar um também uma sobrecarga genérica para o método Mediana.

Para isso podemos usar o seguinte código:


 public static double Mediana<T>(this IEnumerable<T> numeros,
                                                     Func<T, double> selector) =>
                                                             (from num in numeros select selector(num)).Mediana();
 

Essa sobrecarga pega um delegate 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 mostra uma sobrecarga do método Mediana que usa 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.

Agora você pode chamar o método Mediana para uma sequência de objetos de qualquer tipo. Se o tipo não tem sua própria sobrecarga de método, você deve passar um parâmetro delegate, e, podemos usar uma expressão lambda para essa finalidade.

        static void Main(string[] args)
        {
            int[] numerosInt = { 21, 32, 43, 54, 65 };
            /*
              Podemos usar a expressão lambda num=>num 
              como parãmetro para o método Mediana
              de forma que o compilado via converter implicitamente
              o valor para double se não houver conversão implicita
              vai ocorrer um erro
            */
            var resultado1 = numerosInt.Mediana(num => num);
            Console.WriteLine($"Mediana (Int) : {resultado1}");
            string[] numerosString = { "um", "dois", "três", "quatro", "cinco" };
            // Com a sobrecarga genéreica podemos usar as propriedades numerica dos objetos
            var resultado2 = numerosString.Mediana(str => str.Length);
            Console.WriteLine($"Mediana (String) :  {resultado2}");
            Console.ReadLine();
        }

Pegue o projeto aqui :  Linq_AddExtensions.zip

"E não comuniqueis com as obras infrutuosas das trevas, mas antes condenai-as. Porque o que eles fazem em oculto até dizê-lo é torpe. Mas todas estas coisas se manifestam, sendo condenadas pela luz, porque a luz tudo manifesta."
Efésios 5:11-13

Referências:


José Carlos Macoratti