【问题标题】:Get previous and next item in a IEnumerable using LINQ使用 LINQ 获取 IEnumerable 中的上一项和下一项
【发布时间】:2012-02-04 07:13:58
【问题描述】:

我有一个自定义类型的 IEnumerable。 (我从 SelectMany 那里得到的)

我在该 IEnumerable 中还有一个项目 (myItem),我希望 IEnumerable 中的上一个和下一个项目。

目前,我正在做这样的事情:

var previousItem = myIEnumerable.Reverse().SkipWhile( 
    i => i.UniqueObjectID != myItem.UniqueObjectID).Skip(1).FirstOrDefault();

我可以通过简单地省略.Reverse来获得下一个项目。

或者,我可以:

int index = myIEnumerable.ToList().FindIndex( 
    i => i.UniqueObjectID == myItem.UniqueObjectID)

然后使用.ElementAt(index +/- 1) 获取上一项或下一项。

  1. 这两个选项哪个更好?
  2. 还有更好的选择吗?

“更好”包括性能(内存和速度)和可读性的组合;可读性是我最关心的问题。

【问题讨论】:

  • 考虑到可读性,您的第一个选项很棒!比答案要好得多。会用!谢谢!
  • 我同意@Aleksei,也使用那个
  • 我的回答确实晚了,但是如果您需要书尾和功能查找,我添加了 NextOrLast、PreviousOrNext 的扩展方法,以便您可以使用 LINQ。

标签: c# linq ienumerable


【解决方案1】:

您可以将枚举缓存在列表中

var myList = myIEnumerable.ToList()

按索引遍历它

for (int i = 0; i < myList.Count; i++)

那么当前元素为myList[i],上一个元素为myList[i-1],下一个元素为myList[i+1]

(不要忘记列表中第一个和最后一个元素的特殊情况。)

【讨论】:

  • 最好的方法就是在 [i-1] [i+1] 条目上使用 Modulus %
  • 仅当您希望最后一个元素之后的元素成为第一个元素时,反之亦然
  • 我认为.ToArray().ToList() 稍快,并立即证明无法修改,这适用于此处-可读性。
【解决方案2】:

CPU

完全取决于对象在序列中的位置。如果它位于最后,我预计第二个会更快,超过一个因子 2(但只是一个常数因子)。如果它位于开头,则第一个会更快,因为您不会遍历整个列表。

内存

第一个是迭代序列而不保存序列,因此内存命中将非常小。第二种解决方案将占用与列表长度 * 引用 + 对象 + 开销一样多的内存。

【讨论】:

    【解决方案3】:

    为了便于阅读,我会将IEnumerable 加载到链接列表中:

    var e = Enumerable.Range(0,100);
    var itemIKnow = 50;
    var linkedList = new LinkedList<int>(e);
    var listNode = linkedList.Find(itemIKnow);
    var next = listNode.Next.Value; //probably a good idea to check for null
    var prev = listNode.Previous.Value; //ditto
    

    【讨论】:

    • 我真的很喜欢这个答案的可读性;但它似乎有点记忆力。
    • 这就是双向链表的重点,我不明白为什么每个人都对这里的队列和链表过敏。我很想知道从另一个 IEnumerable 创建 Queue 或 LinkedList 是否会产生副本。似乎它可以只使用原始可枚举作为指针。
    • @novaterata:从IEnumerable 创建(例如)Queue复制 IEnumerable 中的引用。所以不会复制IEnumerable 指向的对象,但会复制对这些对象的引用。它与序列中的项目一样昂贵。如果这是我需要定期循环执行的操作,我会找到另一种方法。 这表示我不反对链接列表和队列,但我确实不喜欢执行不必要的复制
    【解决方案4】:

    你真的把事情复杂化了:

    有时只是一个 for 循环会更好地做某事,我认为可以更清晰地实现您正在尝试做的事情/

    var myList = myIEnumerable.ToList();
    
    for(i = 0; i < myList.Length; i++)
    {
       if(myList[i].UniqueObjectID == myItem.UniqueObjectID) 
       {
          previousItem = myList[(i - 1) % (myList.Length - 1)];
          nextItem = myList[(i + 1) % (myList.Length - 1)];
       }
    } 
    

    【讨论】:

    • IEnumerable 是否支持运算符 [] 或属性长度?据我所知不是。此代码已损坏。
    • @spender 啊,意思是先转换成列表。
    • 除了他使用 FindIndex 而不是 for 循环之外,这与 OP 的第二种方法不是很相似吗?
    • @Chris,是的,只是指出很可能不需要 linq。
    • 是的,你说得对。我总是假设像 FindIndex 这样的东西会得到足够的优化,就像你上面的一样,除了可能更快。尽管我认为实际上最大的区别在于您是在一个循环中执行此操作,而 ElementAt 可能需要至少再运行一次,并且可能需要再次运行两次……我想这种事情是始终使用 Linq 的陷阱...
    【解决方案5】:

    首先

    “更好”包括性能(内存和速度)的组合

    一般来说,你不能两者兼得,经验法则是,如果你优化速度,它会消耗内存,如果你优化内存,它就会消耗你的速度。

    有一个更好的选择,它在内存和速度方面都表现良好,并且可以以可读的方式使用(我对函数名称不满意,但是,FindItemReturningPreviousItemFoundItemAndNextItem 有点拗口) .

    所以,看起来是时候使用自定义查找扩展方法了,比如 . . .

    public static IEnumerable<T> FindSandwichedItem<T>(this IEnumerable<T> items, Predicate<T> matchFilling)
    {
        if (items == null)
            throw new ArgumentNullException("items");
        if (matchFilling == null)
            throw new ArgumentNullException("matchFilling");
    
        return FindSandwichedItemImpl(items, matchFilling);
    }
    
    private static IEnumerable<T> FindSandwichedItemImpl<T>(IEnumerable<T> items, Predicate<T> matchFilling)
    {
        using(var iter = items.GetEnumerator())
        {
            T previous = default(T);
            while(iter.MoveNext())
            {
                if(matchFilling(iter.Current))
                {
                    yield return previous;
                    yield return iter.Current;
                    if (iter.MoveNext())
                        yield return iter.Current;
                    else
                        yield return default(T);
                    yield break;
                }
                previous = iter.Current;
            }
        }
        // If we get here nothing has been found so return three default values
        yield return default(T); // Previous
        yield return default(T); // Current
        yield return default(T); // Next
    }
    

    如果您需要多次引用项目,您可以将结果缓存到列表中,但它会返回找到的项目,前面是上一个项目,然后是下一个项目。例如

    var sandwichedItems = myIEnumerable.FindSandwichedItem(item => item.objectId == "MyObjectId").ToList();
    var previousItem = sandwichedItems[0];
    var myItem = sandwichedItems[1];
    var nextItem = sandwichedItems[2];
    

    如果它是第一个或最后一个项目,则默认返回可能需要根据您的要求进行更改。

    希望这会有所帮助。

    【讨论】:

    • 感觉有点矫枉过正,但仍然是一个很好的答案。
    • 很抱歉构建错误和缺乏测试,这只是为了让您了解该方法应该做什么我还限制了我在 SO 答案上“工作”的时间:) i> 你总是可以让它返回一个包含三个项目的元组,而不是产生项目。
    • @BinaryWorrier:是的,现在可以了。 :) +1 以获得良好而完整的解决方案。顺便说一句,我已将此方法命名为 FindTripple()
    • @Dimitre:既然你说我想叫它FindExcentraGallumbits()
    • @BinaryWorrier:更好的名字:FindTriad。
    【解决方案6】:

    如果您对 myIEnumerable 中的每个元素都需要它,我只需遍历它,保持对前两个元素的引用。在循环的主体中,我将对前第二个元素进行处理,当前元素将是它的后代,而第一个元素是它的祖先。

    如果您只需要一个元素,我会选择您的第一种方法。

    【讨论】:

      【解决方案7】:

      通过创建用于为当前元素建立上下文的扩展方法,您可以像这样使用 Linq 查询:

      var result = myIEnumerable.WithContext()
          .Single(i => i.Current.UniqueObjectID == myItem.UniqueObjectID);
      var previous = result.Previous;
      var next = result.Next;
      

      扩展名是这样的:

      public class ElementWithContext<T>
      {
          public T Previous { get; private set; }
          public T Next { get; private set; }
          public T Current { get; private set; }
      
          public ElementWithContext(T current, T previous, T next)
          {
              Current = current;
              Previous = previous;
              Next = next;
          }
      }
      
      public static class LinqExtensions
      {
          public static IEnumerable<ElementWithContext<T>> 
              WithContext<T>(this IEnumerable<T> source)
          {
              T previous = default(T);
              T current = source.FirstOrDefault();
      
              foreach (T next in source.Union(new[] { default(T) }).Skip(1))
              {
                  yield return new ElementWithContext<T>(current, previous, next);
                  previous = current;
                  current = next;
              }
          }
      }
      

      【讨论】:

      • 我认为您打算使用 Concat 而不是 Union
      • 这个解决方案很危险。应避免多次枚举未知来源的IEnumerable。它可能导致对数据库、文件系统或 Web 资源的多次命中。如果你必须这样做,作为第一步,使用ToList 创建一个缓冲区。
      【解决方案8】:

      我想我会尝试使用来自 Linq 的 Zip 来回答这个问题。

      string[] items = {"nought","one","two","three","four"};
      
      var item = items[2];
      
      var sandwiched =
          items
              .Zip( items.Skip(1), (previous,current) => new { previous, current } )
              .Zip( items.Skip(2), (pair,next) => new { pair.previous, pair.current, next } )     
              .FirstOrDefault( triplet => triplet.current == item );
      

      这将返回一个匿名类型 {previous,current,next}。 不幸的是,这只适用于索引 1,2 和 3。

      string[] items = {"nought","one","two","three","four"};
      
      var item = items[4]; 
      
      var pad1 = Enumerable.Repeat( "", 1 );
      var pad2 = Enumerable.Repeat( "", 2 );
      
      var padded = pad1.Concat( items );
      var next1 = items.Concat( pad1 );
      var next2 = items.Skip(1).Concat( pad2 );
      
      var sandwiched =
          padded
              .Zip( next1, (previous,current) => new { previous, current } )
              .Zip( next2, (pair,next) => new { pair.previous, pair.current, next } )
              .FirstOrDefault( triplet => triplet.current == item );
      

      此版本适用于所有索引。 两个版本都使用 Linq 提供的惰性求值。

      【讨论】:

        【解决方案9】:

        这里有一些承诺的扩展方法。这些名称是通用的并且可以与任何简单类型重用,并且有查找重载来获取获取下一个或上一个项目所需的项目。我会对解决方案进行基准测试,然后看看你可以在哪里挤出周期。

         public static class ExtensionMethods  
        {
            public static T Previous<T>(this List<T> list, T item) { 
                var index = list.IndexOf(item) - 1;
                return index > -1 ? list[index] : default(T);
            }
            public static T Next<T>(this List<T> list, T item) {
                var index = list.IndexOf(item) + 1;
                return index < list.Count() ? list[index] : default(T);
            }
            public static T Previous<T>(this List<T> list, Func<T, Boolean> lookup) { 
                var item = list.SingleOrDefault(lookup);
                var index = list.IndexOf(item) - 1;
                return index > -1 ? list[index] : default(T);
            }
            public static T Next<T>(this List<T> list, Func<T,Boolean> lookup) {
                var item = list.SingleOrDefault(lookup);
                var index = list.IndexOf(item) + 1;
                return index < list.Count() ? list[index] : default(T);
            }
            public static T PreviousOrFirst<T>(this List<T> list, T item) { 
                if(list.Count() < 1) 
                    throw new Exception("No array items!");
        
                var previous = list.Previous(item);
                return previous == null ? list.First() : previous;
            }
            public static T NextOrLast<T>(this List<T> list, T item) { 
                if(list.Count() < 1) 
                    throw new Exception("No array items!");
                var next = list.Next(item);
                return next == null ? list.Last() : next;
            }
            public static T PreviousOrFirst<T>(this List<T> list, Func<T,Boolean> lookup) { 
                if(list.Count() < 1) 
                    throw new Exception("No array items!");
                var previous = list.Previous(lookup);
                return previous == null ? list.First() : previous;
            }
            public static T NextOrLast<T>(this List<T> list, Func<T,Boolean> lookup) { 
                if(list.Count() < 1) 
                    throw new Exception("No array items!");
                var next = list.Next(lookup);
                return next == null ? list.Last() : next;
            }
        }
        

        你可以像这样使用它们。

        var previous = list.Previous(obj);
        var next = list.Next(obj);
        var previousWithLookup = list.Previous((o) => o.LookupProperty == otherObj.LookupProperty);
        var nextWithLookup = list.Next((o) => o.LookupProperty == otherObj.LookupProperty);
        var previousOrFirst = list.PreviousOrFirst(obj);
        var nextOrLast = list.NextOrLast(ob);
        var previousOrFirstWithLookup = list.PreviousOrFirst((o) => o.LookupProperty == otherObj.LookupProperty);
        var nextOrLastWithLookup = list.NextOrLast((o) => o.LookupProperty == otherObj.LookupProperty);
        

        【讨论】:

          【解决方案10】:

          我使用以下技术:

          var items = new[] { "Bob", "Jon", "Zac" };
          
          var sandwiches = items
                              .Sandwich()
                              .ToList();
          

          产生这个结果:

          请注意,第一个 Previous 值和最后一个 Next 值都有空值。

          它使用以下扩展方法:

          public static IEnumerable<(T Previous, T Current, T Next)> Sandwich<T>(this IEnumerable<T> source, T beforeFirst = default, T afterLast = default)
          {
              var sourceList = source.ToList();
          
              T previous = beforeFirst;
              T current = sourceList.FirstOrDefault();
          
              foreach (var next in sourceList.Skip(1))
              {
                  yield return (previous, current, next);
          
                  previous = current;
                  current = next;
              }
          
              yield return (previous, current, afterLast);
          }
          

          【讨论】:

          • 感谢西奥多,感谢您的帮助
          • IEnumerables 来源不明的不应该被枚举多次!
          • 这就是我所说的壮丽!谢谢,亲爱的!
          【解决方案11】:

          这是一个 LINQ 扩展方法,它返回当前项以及上一项和下一项。它产生ValueTuple 类型以避免分配。源被枚举一次。

          public static IEnumerable<(T Previous, T Current, T Next)> WithPreviousAndNext<T>(
              this IEnumerable<T> source, T firstPrevious = default, T lastNext = default)
          {
              Queue<T> queue = new Queue<T>(2);
              queue.Enqueue(firstPrevious);
              foreach (var item in source)
              {
                  if (queue.Count > 1)
                  {
                      yield return (queue.Dequeue(), queue.Peek(), item);
                  }
                  queue.Enqueue(item);
              }
              if (queue.Count > 1) yield return (queue.Dequeue(), queue.Peek(), lastNext);
          }
          

          使用示例:

          var source = Enumerable.Range(1, 5);
          Console.WriteLine($"Source: {String.Join(", ", source)}");
          var result = source.WithPreviousAndNext(firstPrevious: -1, lastNext: -1);
          Console.WriteLine($"Result: {String.Join(", ", result)}");
          

          输出:

          来源:1、2、3、4、5
          结果:(-1, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, -1)

          获取特定项目的上一个和下一个(使用tuple deconstruction):

          var (previous, current, next) = myIEnumerable
              .WithPreviousAndNext()
              .First(e => e.Current.UniqueObjectID == myItem.UniqueObjectID);
          

          【讨论】:

          • 不错的西奥多,很整洁
          猜你喜欢
          • 2012-02-27
          • 2020-10-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-09-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多