C# Linq增强扩展MoreLinq之Aggregate(对序列应用累加器)

如项目所说,LINQ to Objects 缺失了一些理想的功能。但MoreLinq将强大的linq进一步增强了,扩展出了将近100+的功能,使编写代码效率提高。

MoreLINQ项目的主要目标是为LINQ提供更多的功能和灵活性,以满足不同场景下的需求。该项目由一些微软的工程师创建和维护,他们利用自己的业余时间开发并分享这个开源项目。

本系列文章将逐个介绍MoreLinq的使用方法。

Aggregate 扩展

Aggregate可实现对序列应用累加器,其原生方法是Linq的Aggregate,MoreLinq在其基础上扩展了更多功能。
原生方法是这样的:

public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func);
public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func);
public static TResult Aggregate<TSource, TAccumulate, TResult>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector);

MoreLinq中Aggregate 的方法:

/// <summary>
        /// Applies two accumulators sequentially in a single pass over a
        /// sequence.
        /// </summary>
        /// <typeparam name="T">The type of elements in <paramref name="source"/>.</typeparam>
        /// <typeparam name="TAccumulate1">The type of first accumulator value.</typeparam>
        /// <typeparam name="TAccumulate2">The type of second accumulator value.</typeparam>
        /// <typeparam name="TResult">The type of the accumulated result.</typeparam>
        /// <param name="source">The source sequence</param>
        /// <param name="seed1">The seed value for the first accumulator.</param>
        /// <param name="accumulator1">The first accumulator.</param>
        /// <param name="seed2">The seed value for the second accumulator.</param>
        /// <param name="accumulator2">The second accumulator.</param>
        /// <param name="resultSelector">
        /// A function that projects a single result given the result of each
        /// accumulator.</param>
        /// <returns>The value returned by <paramref name="resultSelector"/>.</returns>
        /// <remarks>
        /// This operator executes immediately.
        /// </remarks>

        public static TResult Aggregate<T, TAccumulate1, TAccumulate2, TResult>(
            this IEnumerable<T> source,
            TAccumulate1 seed1, Func<TAccumulate1, T, TAccumulate1> accumulator1,
            TAccumulate2 seed2, Func<TAccumulate2, T, TAccumulate2> accumulator2,
            Func<TAccumulate1, TAccumulate2, TResult> resultSelector)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (accumulator1 == null) throw new ArgumentNullException(nameof(accumulator1));
            if (accumulator2 == null) throw new ArgumentNullException(nameof(accumulator2));
            if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));

            var a1 = seed1;
            var a2 = seed2;

            foreach (var item in source)
            {
                a1 = accumulator1(a1, item);
                a2 = accumulator2(a2, item);
            }

            return resultSelector(a1, a2);
        }

该扩展方法在一次遍历序列中连续应用累加器,上面的代码是对两个初始值和两个累加器的扩展,且sead1只应用accumulator1,同理sead2只应用accumulator2,然后对结果应用resultSelector(a1,a2)方法。
该扩展方法有多个重载,最多可以在一次遍历中依次应用八个累加器序列。

/// <summary>
        /// Applies two accumulator queries sequentially in a single
        /// pass over a sequence.
        /// </summary>
        /// <typeparam name="T">The type of elements in <paramref name="source"/>.</typeparam>
        /// <typeparam name="TResult1">The type of the result of the first accumulator.</typeparam>
        /// <typeparam name="TResult2">The type of the result of the second accumulator.</typeparam>
        /// <typeparam name="TResult">The type of the accumulated result.</typeparam>
        /// <param name="source">The source sequence</param>
        /// <param name="accumulator1">The first accumulator.</param>
        /// <param name="accumulator2">The second accumulator.</param>
        /// <param name="resultSelector">
        /// A function that projects a single result given the result of each
        /// accumulator.</param>
        /// <returns>The value returned by <paramref name="resultSelector"/>.</returns>
        /// <exception cref="InvalidOperationException">
        /// An <see cref="IObservable{T}"/> returned by an accumulator function
        /// produced zero or more than a single aggregate result.
        /// </exception>
        /// <remarks>
        /// <para>This operator executes immediately.</para>
        /// <para>
        /// Each accumulator argument is a function that receives an
        /// <see cref="IObservable{T}"/>, which when subscribed to, produces the
        /// items in the <paramref name="source"/> sequence and in original
        /// order; the function must then return an <see cref="IObservable{T}"/>
        /// that produces a single aggregate on completion (when
        /// <see cref="IObserver{T}.OnCompleted"/> is called. An error is raised
        /// at run-time if the <see cref="IObserver{T}"/> returned by an
        /// accumulator function produces no result or produces more than a
        /// single result.
        /// </para>
        /// </remarks>

        public static TResult Aggregate<T, TResult1, TResult2, TResult>(
            this IEnumerable<T> source,
            Func<IObservable<T>, IObservable<TResult1>> accumulator1,
            Func<IObservable<T>, IObservable<TResult2>> accumulator2,
            Func<TResult1, TResult2, TResult> resultSelector)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (accumulator1 == null) throw new ArgumentNullException(nameof(accumulator1));
            if (accumulator2 == null) throw new ArgumentNullException(nameof(accumulator2));
            if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));

            var r1 = new (bool, TResult1)[1];
            var r2 = new (bool, TResult2)[1];

            var subject = new Subject<T>();

            using (SubscribeSingle(accumulator1, subject, r1, nameof(accumulator1)))
            using (SubscribeSingle(accumulator2, subject, r2, nameof(accumulator2)))
            {
                foreach (var item in source)
                    subject.OnNext(item);

                subject.OnCompleted();
            }

            return resultSelector            (
                GetAggregateResult(r1[0], nameof(accumulator1)),
                GetAggregateResult(r2[0], nameof(accumulator2))
            );
        }

该方法在一个序列上的一次传递中按顺序应用累加器,上面的代码会分别订阅流数据,将执行结果应用resultSelector方法。

用两个示例演示Aggregate 的基本用法

运行以下七个累加器:

  • 所有数字的总和

  • 偶数的总和

  • 数字的个数

  • 最小的数字

  • 最大的数字

  • 所有数字中不同数字的个数

  • 数字列表

原生

Enumerable
    .Range(1, 10)
    .Shuffle()
    .Select(n => new { Num = n, Str = n.ToString(CultureInfo.InvariantCulture) })
    .Aggregate(
        0, (s, e) => s + e.Num,
        0, (s, e) => e.Num % 2 == 0 ? s + e.Num : s,
        0, (s, _) => s + 1,
        (int?)null, (s, e) => s is int n ? Math.Min(n, e.Num) : e.Num,
        (int?)null, (s, e) => s is int n ? Math.Max(n, e.Num) : e.Num,
        new HashSet<int>(), (s, e) => { s.Add(e.Str.Length); return s; },
        new List<(int, string)>(), (s, e) => { s.Add((e.Num, e.Str)); return s; },
        (sum, esum, count, min, max, lengths, items) => new
        {
            Sum           = sum,
            EvenSum       = esum,
            Count         = count,
            Average       = (double)sum / count,
            Min           = min is int mn ? mn : throw new InvalidOperationException(),
            Max           = max is int mx ? mx : throw new InvalidOperationException(),
            UniqueLengths = "[" + string.Join(", ", lengths) + "]",
            Items         = "[" + string.Join(", ", items)   + "]",
        })

但用这种方式编写每个聚合器可能很繁琐、重复且容易出错,因为不能重复使用 Enumerable.Sum。Aggregate 扩展允许使用响应性推导式编写聚合器。将上边的代码改一下。

Enumerable    
    .Range(1, 10)
    .Shuffle()
    .Select(n => new { Num = n, Str = n.ToString(CultureInfo.InvariantCulture) })
    .Aggregate(
        s => s.Sum(e => e.Num),
        s => s.Select(e => e.Num).Where(n => n % 2 == 0).Sum(),
        s => s.Count(),
        s => s.Min(e => e.Num),
        s => s.Max(e => e.Num),
        s => s.Select(e => e.Str.Length).Distinct().ToArray(),
        s => s.ToList(),
        (sum, esum, count, min, max, lengths, items) => new
        {
            Sum           = sum,
            EvenSum       = esum,
            Count         = count,
            Average       = (double)sum / count,
            Min           = min,
            Max           = max,
            UniqueLengths = "[" + string.Join(", ", lengths) + "]",
            Items         = "[" + string.Join(", ", items)   + "]",
        })

这样是不是就好看很多了呢?