Compartir a través de


Cómo extender LINQ

Todos los métodos basados en LINQ siguen uno de los dos patrones similares. Toman una secuencia enumerable. Devuelven una secuencia diferente o un único valor. La coherencia de la forma permite extender LINQ escribiendo métodos con una forma similar. De hecho, las bibliotecas de .NET obtuvieron nuevos métodos en muchas versiones de .NET desde que LINQ se introdujo por primera vez. En este artículo, verá ejemplos de extensión de LINQ escribiendo sus propios métodos que siguen el mismo patrón.

Adición de métodos personalizados para consultas LINQ

Puede ampliar el conjunto de métodos que se usan para las consultas LINQ agregando métodos de extensión a la IEnumerable<T> interfaz . Por ejemplo, además de las operaciones medias o máximas estándar, se crea un método de agregado personalizado para calcular un valor único a partir de una secuencia de valores. También se crea un método que funciona como un filtro personalizado o una transformación de datos específica para una secuencia de valores y devuelve una nueva secuencia. Algunos ejemplos de estos métodos son Distinct, Skipy Reverse.

Al extender la IEnumerable<T> interfaz, puede aplicar los métodos personalizados a cualquier colección enumerable. Para obtener más información, vea Métodos de extensión.

Un método de agregado calcula un valor único a partir de un conjunto de valores. LINQ proporciona varios métodos agregados, incluidos Average, Miny Max. Puede crear su propio método de agregado agregando un método de extensión a la IEnumerable<T> interfaz.

A partir de C# 14, puede declarar un bloque de extensión para que contenga varios miembros de extensión. Se declara un bloque de extensión con la palabra clave extension seguida del parámetro receiver entre paréntesis. En el ejemplo de código siguiente se muestra cómo crear un método de extensión llamado Median en un bloque de extensión. El método calcula una mediana para una secuencia 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];
        }
    }
}

También puede agregar el this modificador a un método estático para declarar un método de extensión. El código siguiente muestra el método de extensión 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];
        }
    }
}

Puede llamar a este método de extensión para cualquier colección enumerable de la misma manera en la que llamaría a otros métodos de agregación desde la interfaz IEnumerable<T>.

En el ejemplo de código siguiente se muestra cómo usar el Median método para una 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

Puede sobrecargar el método de agregado para que acepte secuencias de varios tipos. El enfoque estándar consiste en crear una sobrecarga para cada tipo. Otro enfoque consiste en crear una sobrecarga que toma un tipo genérico y la convierte en un tipo específico mediante un delegado. También puede combinar ambos enfoques.

Puede crear una sobrecarga específica para cada tipo que desee admitir. En el ejemplo de código siguiente se muestra una sobrecarga del Median método para el int tipo .

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

Ahora puede llamar a las sobrecargas Median para los tipos integer y double, como se muestra en el código siguiente:

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

También puede crear una sobrecarga que acepte una secuencia genérica de objetos. Esta sobrecarga toma un delegado como parámetro y lo usa para convertir una secuencia de objetos de un tipo genérico a un tipo específico.

El código siguiente muestra una sobrecarga del Median método que toma el Func<T,TResult> delegado como parámetro. Este delegado toma un objeto de tipo genérico T y devuelve un objeto de 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();

Ahora puede llamar al Median método para una secuencia de objetos de cualquier tipo. Si el tipo no tiene su propia sobrecarga de métodos, deberá pasar un parámetro de delegado. En C#, puede usar una expresión lambda para este propósito. Además, solo en Visual Basic, si usa la cláusula Aggregate o Group By en lugar de la llamada al método, puede pasar cualquier valor o expresión que esté dentro del alcance de esta cláusula.

En el código de ejemplo siguiente se muestra cómo llamar al Median método para una matriz de enteros y una matriz de cadenas. En el caso de las cadenas, se calcula la mediana de las longitudes de las cadenas de la matriz. En el ejemplo se muestra cómo pasar el Func<T,TResult> parámetro delegado al 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

Puede ampliar la IEnumerable<T> interfaz con un método de consulta personalizado que devuelva una secuencia de valores. En este caso, el método debe devolver una colección de tipo IEnumerable<T>. Estos métodos se pueden usar para aplicar filtros o transformaciones de datos a una secuencia de valores.

En el ejemplo siguiente se muestra cómo crear un método de extensión denominado AlternateElements que devuelve todos los demás elementos de una colección, empezando por el primer 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++;
    }
}

Puede llamar a este método de extensión para cualquier colección enumerable igual que llamaría a otros métodos desde la IEnumerable<T> interfaz, como se muestra en el código siguiente:

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 ejemplo que se muestra en este artículo tiene un receptor diferente. Esto significa que cada método debe declararse en un bloque de extensión diferente que especifique el receptor único. En el ejemplo de código siguiente se muestra una sola clase estática con tres bloques de extensión diferentes, cada uno de los cuales contiene uno de los métodos definidos en este artículo:

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

El bloque de extensión final declara un bloque de extensión genérico. El parámetro de tipo para el receptor se declara directamente en el extension.

En el ejemplo anterior se declara un miembro de extensión en cada bloque de extensión. En la mayoría de los casos, se crean varios miembros de extensión para el mismo receptor. En esos casos, debe declarar las extensiones para esos miembros en un único bloque de extensiones.