使用LINQ获取IEnumerable中的上一个和下一个项目

时间:2012-01-06 15:05:27

标签: c# linq ienumerable

我有一个自定义类型的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. 是否有更好的选择?
  3. “更好”包括性能(内存和速度)和可读性的组合;可读性是我最关心的问题。

11 个答案:

答案 0 :(得分:39)

首先关闭

  

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

一般来说,你不能同时拥有这两者,经验法则是,如果你优化速度,它会花费内存,如果你优化内存,它会花费你的速度。

有一个更好的选择,在内存和速度前端都表现良好,并且可以以可读的方式使用(我对功能名称不满意,但是,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
}

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

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

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

希望这有帮助。

答案 1 :(得分:20)

为了便于阅读,我将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

答案 2 :(得分:13)

通过创建用于为当前元素建立上下文的扩展方法,您可以使用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;
        }
    }
}

答案 3 :(得分:4)

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

var myList = myIEnumerable.ToList()

通过索引

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

然后当前元素为myList[i],前一个元素为myList[i-1],下一个元素为myList[i+1]

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

答案 4 :(得分:2)

<强> CPU

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

<强>内存

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

答案 5 :(得分:2)

你真的过于复杂化了:

有时候只需要一个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)];
   }
} 

答案 6 :(得分:2)

我想我会尝试使用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的惰性评估。

答案 7 :(得分:1)

这里是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);

答案 8 :(得分:0)

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

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

答案 9 :(得分:0)

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

 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 :(得分:0)

我使用以下技术:

var items = new[] { "Bob", "Jon", "Zac" };

var sandwiches = items
                    .Sandwich()
                    .ToList();

哪个会产生以下结果:

enter image description here

请注意,第一个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);
}