【问题标题】:Aggregate values until a limit is reached聚合值直到达到限制
【发布时间】:2015-02-10 08:38:24
【问题描述】:

我需要类似于AggregateWhile 方法的东西。标准的 System.Linq.Enumerable 类不提供它。到目前为止,我一直能够利用标准的 LINQ 方法来解决我遇到的每一个问题。所以我想知道在这种情况下这是否仍然可行,或者我是否真的需要使用非标准方法扩展 LINQ。

假设的AggregateWhile 方法将遍历一个序列并应用累加器。一旦谓词返回 false,聚合将完成。结果是元素的聚合,但包括谓词失败的元素。

这是一个例子。我们有一个List { 1, 2, 3, 4, 5 },带有一个累加器,将两个输入数字相加,还有一个谓词表明累加必须小于 12。AggregateWhile 将返回 10,因为这是 1 + 2 + 3 + 4 和加法的结果最后的 5 个将使总数超过限制。在代码中:

var list = new List<int> { 1, 2, 3, 4, 5 };
int total = list.AggregateWhile( (x, y) => x + y, a => a < 12 ); // returns 10

我需要一个纯粹的函数式解决方案,因此关闭临时变量不是一种选择。

【问题讨论】:

  • 你总是可以只使用循环而不是 Linq...
  • 或者看一下Aggregate的引用源,然后修改它以包含谓词。
  • @MatthewWatson 我需要使用 LINQ,它必须是纯功能的。如果您想知道原因,请查看 my blog 上的最后一个条目。
  • @Dirk 自己写方法没问题。

标签: c# linq linq-to-objects


【解决方案1】:

您可以自己编写函数,也可以在累加器中携带一个标志:

int total = list.Aggregate(new { value = 0, valid = true }, 
                          (acc, v) => acc.value + v < 12 && acc.valid ?
                                      new { value = acc.value + v, valid = true } :
                                      new { value = acc.value, valid = false },
                            acc => acc.value); 

这很丑,所以写一个新的AggregateWhile会更好:

public static TSource AggregateWhile<TSource>(this IEnumerable<TSource> source, 
                                         Func<TSource, TSource, TSource> func,
                                         Func<TSource, bool> predicate)
{
   using (IEnumerator<TSource> e = source.GetEnumerator()) {
       TSource result = e.Current;
       TSource tmp = default(TSource);
       while (e.MoveNext() && predicate(tmp = func(result, e.Current))) 
            result = tmp;
       return result;
   }
}

(为简洁起见,没有错误检查)

【讨论】:

  • 第一个示例将不必要地遍历所有元素。
【解决方案2】:

您可以编写自己的扩展方法。这不像普通的 Linq 方法那么完美,我作弊是因为我已经知道您的要求以使其更简单。实际上,您可能需要 a 的可选起始值以及 T 或其他内容的不同 In 和输出类型:

public static class Linq
{
  public static T AggregateWhile<T>(this IEnumerable<T> sequence, Func<T, T, T> aggregate, Func<T, bool> predicate)
  {
     T a;
     foreach(var value in sequence)
     {
        T temp = aggregate(a, value);
        if(!predicate(temp)) break;
        a = temp;
     }
     return a;
  }
}

【讨论】:

    【解决方案3】:

    这不行吗?

    int total = list.Aggregate(0, (a, x) => (a + x) > 12 ? a : a + x);
    

    使用Tuple&lt;bool, int&gt; 作为累加器类型,在第一次溢出时中断:

    int total = list.Aggregate(new Tuple<bool, int>(false, 0),
        (a, x) => a.Item1 || (a.Item2 + x) > 12
        ? new Tuple<bool, int>(true, a.Item2)
        : new Tuple<bool, int>(false, a.Item2 + x)
    ).Item2;
    

    但不幸的是,它不是那么好。


    开始使用 F#。 ;)

    let list = [ 1; 2; 3; 4; 5; 1 ]
    let predicate = fun a -> a > 12 
    let total = list |> List.fold (fun (aval, astate) x ->
        if astate || predicate (aval + x)
        then (aval, true)
        else (aval + x, false)) (0, false)
    

    Tuple 拆包,没有new 膨胀。当您编写代码时,类型推断变得轻而易举。

    【讨论】:

    • 这对于像new List&lt;int&gt; { 1, 2, 3, 4, 5, 1 };这样的输入会失败
    • 然后使用 Tuple 作为累加器类型。
    • @rkrahl 如果您建议可以提供 F# 示例吗? :-)
    • @Grundy 你在这里。
    • 希望我可以选择两个答案,但只能投赞成票。
    【解决方案4】:

    我在遇到一个问题时问了这个问题,后来我重新定义为不需要AggregateWhile。但现在我遇到了一个稍微不同的问题,无疑需要AggregateWhile 或直接替代它。

    @sloth 和 @rkrahl 提出的解决方案很有帮助。但它们的不足之处在于聚合逻辑(在这种情况下是加法)重复了两次。对于这个问题的微不足道的例子来说,这似乎没什么大不了的。但是对于我真正的问题,计算很复杂,所以写两次是不可接受的。

    这是我更喜欢的解决方案(缺少实际的 AggregateWhile 方法):

    class Program
    {
        static void Main( string[] args ) { new Program(); }
    
        public Program()
        {
            var list = new int[] { 1, 2, 3, 4, 5 };
            int total = list
                .Aggregate( new Accumulator( 0 ), ( a, i ) => a.Next( i ), a => a.Total );
        }
    }
    
    class Accumulator
    {
        public Accumulator( int total )
        {
            this.total = total;
        }
    
        public Accumulator Next( int i )
        {
            if ( isDone )
                return this;
            else {
                int total = this.total + i;
                if ( total < 12 )
                    return new Accumulator( total );
                else {
                    isDone = true;
                    return this;
                }
            }
        }
        bool isDone;
    
        public int Total
        {
            get { return total; }
        }
        readonly int total;
    }
    

    理想的解决方案是完全实现和测试的AggregateWhile 方法,它们对应于三个Aggregate 重载。除此之外,上述模式的优势在于它可以利用 .NET 框架中已经存在的(有些缺乏的)功能。

    【讨论】:

      【解决方案5】:

      这是一个AggregateWhile 和一个seed

      public static TAccumulate AggregateWhile<TSource, TAccumulate>(
          this IEnumerable<TSource> source,
          TAccumulate seed,
          Func<TAccumulate, TSource, TAccumulate> func,
          Func<TAccumulate, bool> predicate)
      {
          if (source == null)
              throw new ArgumentNullException(nameof(source));
      
          if (func == null)
              throw new ArgumentNullException(nameof(func));
      
          if (predicate == null)
              throw new ArgumentNullException(nameof(predicate));
      
          var accumulate = seed;
          foreach (var item in source)
          {
              var tmp = func(accumulate, item);
              if (!predicate(tmp)) break;
              accumulate = tmp;
          }
          return accumulate;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-10-03
        • 2012-11-18
        • 2018-02-24
        • 2010-09-28
        • 2018-10-27
        • 2018-10-17
        • 1970-01-01
        • 2019-04-23
        相关资源
        最近更新 更多