【问题标题】:Linq optimisation within a foreachforeach 中的 Linq 优化
【发布时间】:2013-06-30 20:00:48
【问题描述】:

我一直在寻找一种将foreach 循环拆分为多个部分的方法,并遇到了以下代码:

foreach(var item in items.Skip(currentPage * itemsPerPage).Take(itemsPerPage))
{
    //Do stuff
}

items.Skip(currentPage * itemsPerPage).Take(itemsPerPage) 是在每次迭代中处理,还是只处理一次,然后编译器会自动将临时结果用于 foreach 循环?

【问题讨论】:

  • 打个断点看看。
  • 这只是一个拆分。你也是从循环中调用它吗?

标签: c# linq optimization foreach


【解决方案1】:

不,它会被处理一次。

同理:

public IEnumerable<Something> GetData() {
    return someData; 
}


foreach(var d in GetData()) {
   //do something with [d]
}

【讨论】:

  • 不确定这是否正确。我的意思是您的 GetData 函数类似于仅 getter 属性,并且每次循环增加其步骤时,都会调用该 get 访问器,或者在您的情况下,您的方法,基本上在每个步骤上调用 Skip / Take 构造。
  • @PotecaruTudor:在 foreach 循环中,它将被称为 one。为了证明这一点,只需做一个简单的测试。
  • 是的,刚刚调试了一个测试示例,你是对的。谢谢。
【解决方案2】:

foreach 构造等价于:

IEnumerator enumerator = myCollection.GetEnumerator();
try
{
   while (enumerator.MoveNext())
   {
       object current = enumerator.Current;
       Console.WriteLine(current);
   }
}
finally
{
   IDisposable e = enumerator as IDisposable;
   if (e != null)
   {
       e.Dispose();
   }
}

所以,不,myCollection 只会被处理一次。

更新:

请注意,这取决于IEnumerable 使用的IEnumerator 的实现。

在这个(邪恶的)例子中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;


namespace TestStack
{
    class EvilEnumerator<T> : IEnumerator<T> {

        private IEnumerable<T> enumerable;
        private int index = -1;

        public EvilEnumerator(IEnumerable<T> e) 
        {
            enumerable = e;
        }


        #region IEnumerator<T> Membres

        public T Current
        {
            get { return enumerable.ElementAt(index); }
        }

        #endregion

        #region IDisposable Membres

        public void Dispose()
        {

        }

        #endregion

        #region IEnumerator Membres

        object IEnumerator.Current
        {
            get { return enumerable.ElementAt(index); }
        }

        public bool MoveNext()
        {
            index++;
            if (index >= enumerable.Count())
                return false;
            return true;
        }

        public void Reset()
        {

        }

        #endregion
    }
    class DemoEnumerable<T> : IEnumerable<T>
    {

        private IEnumerable<T> enumerable;

        public DemoEnumerable(IEnumerable<T> e)
        {
            enumerable = e; 
        }


        #region IEnumerable<T> Membres

        public IEnumerator<T> GetEnumerator()
        {
            return new EvilEnumerator<T>(enumerable);
        }

        #endregion

        #region IEnumerable Membres

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }

    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<int> numbers = Enumerable.Range(0,100);
            DemoEnumerable<int> enumerable = new DemoEnumerable<int>(numbers);
            foreach (var item in enumerable)
            {
                Console.WriteLine(item);
            }
        }
    }
}

enumerable 的每次迭代都会计算numbers 两次。

【讨论】:

    【解决方案3】:

    问题:

    items.Skip(currentPage * itemsPerPage).Take(itemsPerPage) 是 每次迭代都处理一次,或者它会被处理一次,并且有一个 由 foreach 循环自动使用的临时结果 编译器?

    答案:

    它将被处理一次,而不是每次迭代。您可以将集合放入变量中以使 foreach 更具可读性。如下图所示。

    foreach(var item in items.Skip(currentPage * itemsPerPage).Take(itemsPerPage))
    {
        //Do stuff
    }
    

    对比

    List<MyClass> query = items.Skip(currentPage * itemsPerPage).Take(itemsPerPage).ToList();
    
    foreach(var item in query)
    {
        //Do stuff
    }
    

    对比

    IEnumerable<MyClass> query = items.Skip(currentPage * itemsPerPage).Take(itemsPerPage);
    
    foreach(var item in query)
    {
        //Do stuff
    }
    

    【讨论】:

      【解决方案4】:

      正如其他人指出的那样,您提供的代码只会迭代列表中的项目一次。

      但是,这只会为您提供一页的项目。如果您要处理多个页面,则必须为每个页面调用一次该代码(因为您必须在某处递增 currentPage,对吧?)。

      我的意思是你必须做这样的事情:

      for (int currentPage = 0; currentPage < numPages; ++currentPage)
      {
          foreach (var item in items.Skip(currentPage*itemsPerPage).Take(itemsPerPage))
          {
              //Do stuff
          }
      }
      

      现在如果你这样做那个,那么你重复序列多次 - 每个页面一次。第一次迭代只会到第一页的结尾,但下一次将从第二页的开头迭代到结尾(通过Skip()Take()) - 下一次将从第三页的开头到结尾。以此类推。

      为避免这种情况,您可以为IEnumerable&lt;T&gt; 编写一个扩展方法,将数据分成批次(您也可以将其描述为将数据“分页”成“页面”)。

      而不是仅仅呈现 IEnumerable 的 IEnumerable,将每个批次包装在一个类中以提供批次索引以及批次中的项目会更有用,如下所示:

      public sealed class Batch<T>
      {
          public readonly int Index;
          public readonly IEnumerable<T> Items;
      
          public Batch(int index, IEnumerable<T> items)
          {
              Index = index;
              Items = items;
          }
      }
      
      public static class EnumerableExt
      {
          // Note: Not threadsafe, so not suitable for use with Parallel.Foreach() or IEnumerable.AsParallel()
      
          public static IEnumerable<Batch<T>> Partition<T>(this IEnumerable<T> input, int batchSize)
          {
              var enumerator = input.GetEnumerator();
              int index = 0;
      
              while (enumerator.MoveNext())
                  yield return new Batch<T>(index++, nextBatch(enumerator, batchSize));
          }
      
          private static IEnumerable<T> nextBatch<T>(IEnumerator<T> enumerator, int blockSize)
          {
              do { yield return enumerator.Current; }
              while (--blockSize > 0 && enumerator.MoveNext());
          }
      }
      

      这个扩展方法不缓存数据,只遍历一次。

      使用这种扩展方法,批量处理项目变得更具可读性。请注意,此示例枚举所有页面的所有项目,这与 OP 的示例不同,它仅遍历一页的项目:

      var items = Enumerable.Range(10, 50); // Pretend we have 50 items.
      int itemsPerPage = 20;
      
      foreach (var page in items.Partition(itemsPerPage))
      {
          Console.Write("Page " + page.Index + " items: ");
      
          foreach (var i in page.Items)
              Console.Write(i + " ");
      
          Console.WriteLine();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-06-12
        • 2017-08-22
        • 1970-01-01
        • 1970-01-01
        • 2021-01-10
        • 2011-02-26
        • 1970-01-01
        • 2014-06-24
        相关资源
        最近更新 更多