如何扩展 LINQ

所有基于 LINQ 的方法都遵循两种类似的模式之一。 它们采用可枚举序列。 它们返回不同的序列或单个值。 通过形状的一致性,可以通过编写具有类似形状的方法来扩展 LINQ。 事实上,自首次引入 LINQ 以来,.NET 库在许多 .NET 版本中获得了新的方法。 在本文中,你将看到通过编写遵循相同模式的自己的方法来扩展 LINQ 的示例。

为 LINQ 查询添加自定义方法

通过将扩展方法添加到 IEnumerable<T> 接口来扩展用于 LINQ 查询的方法集。 例如,除了标准平均值或最大作之外,还可以创建自定义聚合方法,以从值序列中计算单个值。 还可以创建一个方法,该方法用作自定义筛选器或值序列的特定数据转换,并返回一个新序列。 此类方法的示例包括DistinctSkipReverse

扩展 IEnumerable<T> 接口时,可以将自定义方法应用于任何可枚举集合。 有关详细信息,请参阅扩展方法

聚合方法从一组值计算单个值。 LINQ 提供了多种聚合方法,包括AverageMinMax。 可以通过向 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();

现在便可以为 Medianinteger 类型调用 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# 中,可以使用 lambda 表达式实现此目的。 此外,仅在 Visual Basic 中使用 AggregateGroup 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 自身上声明的。

前面的示例在每个扩展块中声明一个扩展成员。 在大多数情况下,您会为同一接收方创建多个扩展成员。 在这些情况下,您应该在单个扩展模块中声明这些成员的扩展功能。