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

发布于 2024-12-25 07:13:40 字数 671 浏览 6 评论 0原文

我有一个自定义类型的 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. 还有更好的选择吗?

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

I have an IEnumerable of a custom type. (That I've gotten from a SelectMany)

I also have an item (myItem) in that IEnumerable that I desire the previous and next item from the IEnumerable.

Currently, I'm doing the desired like this:

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

I can get the next item by simply ommitting the .Reverse.

or, I could:

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

and then use .ElementAt(index +/- 1) to get the previous or next item.

  1. Which is better between the two options?
  2. Is there an even better option available?

"Better" includes a combination of performance (memory and speed) and readability; with readability being my primary concern.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

掐死时间 2025-01-01 07:13:41

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

By creating an extension method for establishing context to the current element you can use a Linq query like this:

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

The extension would be something like this:

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;
        }
    }
}
梦幻的味道 2025-01-01 07:13:41

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

var myList = myIEnumerable.ToList()

通过索引对其进行迭代

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

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

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

You could cache the enumerable in a list

var myList = myIEnumerable.ToList()

iterate over it by index

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

then the current element is myList[i], the previous element is myList[i-1], and the next element is myList[i+1]

(Don't forget about the special cases of the first and last elements in the list.)

酒儿 2025-01-01 07:13:41

你真的把事情复杂化了:

有时,仅仅一个 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)];
   }
} 

You are really over complicating things:

Sometimes just a for loop is going to be better to do something, and I think provide a clearer implementation of what you are trying to do/

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)];
   }
} 
风渺 2025-01-01 07:13:41

下面是一个 LINQ 扩展方法,它返回当前项以及上一项和下一项。它产生 ValueTuple< /code>值以避免分配。源被枚举一次。

/// <summary>
/// Projects each element of a sequence into a tuple that includes the previous
/// and the next element.
/// </summary>
public static IEnumerable<(T Previous, T Current, T Next)> WithPreviousAndNext<T>(
    this IEnumerable<T> source, T firstPrevious = default, T lastNext = default)
{
    ArgumentNullException.ThrowIfNull(source);
    (T Previous, T Current, bool HasPrevious) queue = (default, firstPrevious, false);
    foreach (var item in source)
    {
        if (queue.HasPrevious)
            yield return (queue.Previous, queue.Current, item);
        queue = (queue.Current, item, true);
    }
    if (queue.HasPrevious)
        yield return (queue.Previous, queue.Current, 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)}");

输出:

Source: 1, 2, 3, 4, 5  
Result: (-1, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, -1)  

要获取特定项目的上一个和下一个,您可以使用 元组解构

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

Here is a LINQ extension method that returns the current item, along with the previous and the next. It yields ValueTuple<T, T, T> values to avoid allocations. The source is enumerated once.

/// <summary>
/// Projects each element of a sequence into a tuple that includes the previous
/// and the next element.
/// </summary>
public static IEnumerable<(T Previous, T Current, T Next)> WithPreviousAndNext<T>(
    this IEnumerable<T> source, T firstPrevious = default, T lastNext = default)
{
    ArgumentNullException.ThrowIfNull(source);
    (T Previous, T Current, bool HasPrevious) queue = (default, firstPrevious, false);
    foreach (var item in source)
    {
        if (queue.HasPrevious)
            yield return (queue.Previous, queue.Current, item);
        queue = (queue.Current, item, true);
    }
    if (queue.HasPrevious)
        yield return (queue.Previous, queue.Current, lastNext);
}

Usage example:

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

Output:

Source: 1, 2, 3, 4, 5  
Result: (-1, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, -1)  

To get the previous and the next of a specific item, you could use tuple deconstruction:

var (previous, current, next) = myIEnumerable
    .WithPreviousAndNext()
    .First(e => e.Current.UniqueObjectID == myItem.UniqueObjectID);
扭转时空 2025-01-01 07:13:41

CPU

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

内存

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

CPU

Depends entirely on where the object is in the sequence. If it is located at the end I would expect the second to be faster with more than a factor 2 (but only a constant factor). If it is located in the beginning the first will be faster because you don't traverse the whole list.

Memory

The first is iterating the sequence without saving the sequence so the memory hit will be very small. The second solution will take as much memory as the length of the list * references + objects + overhead.

天荒地未老 2025-01-01 07:13:41

我想我会尝试使用 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 提供的惰性求值。

I thought I would try to answer this using Zip from Linq.

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 );

This will return a anonymous type {previous,current,next}.
Unfortunately this will only work for indexes 1,2 and 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 );

This version will work for all indexes.
Both version use lazy evaluation courtesy of Linq.

小忆控 2025-01-01 07:13:41

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

 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);

Here are some extension methods as promised. The names are generic and reusable with any type simple and there are lookup overloads to get at the item needed to get the next or previous items. I would benchmark the solutions and then see where you could squeeze cycles out.

 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;
    }
}

And you can use them like this.

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);
清泪尽 2025-01-01 07:13:41

我使用以下技术:

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);
}

I use the following technique:

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

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

Which produces this result:

enter image description here

Notice that there are nulls for the first Previous value, and the last Next value.

It uses the following extension method:

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);
}
魂归处 2025-01-01 07:13:41

如果您需要 myIEnumerable 中的每个元素,我只需迭代它并保留对前 2 个元素的引用。在循环体中,我将对第二个前一个元素进行处理,当前元素将是其后代,第一个元素将是其祖先。

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

If you need it for every element in myIEnumerable I’d just iterate through it keeping references to the 2 previous elements. In the body of the loop I'd do the processing for the second previous element and the current would be its descendant and first previous its ancestor.

If you need it for only one element I'd choose your first approach.

夜还是长夜 2025-01-01 07:13:40

首先

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

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

有一个更好的选项,它在内存和速度方面都表现良好,并且可以以可读的方式使用(我对函数名称不满意,但是,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];

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

希望这有帮助。

First off

"Better" includes a combination of performance (memory and speed)

In general you can't have both, the rule of thumb is, if you optimise for speed, it'll cost memory, if you optimise for memory, it'll cost you speed.

There is a better option, that performs well on both memory and speed fronts, and can be used in a readable manner (I'm not delighted with the function name, however, FindItemReturningPreviousItemFoundItemAndNextItem is a bit of a mouthful).

So, it looks like it's time for a custom find extension method, something like . . .

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
}

You can cache the result of this to a list if you need to refer to the items more than once, but it returns the found item, preceded by the previous item, followed by the following item. e.g.

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

The defaults to return if it's the first or last item may need to change depending on your requirements.

Hope this helps.

诗化ㄋ丶相逢 2025-01-01 07:13:40

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

For readability, I'd load the IEnumerable into a linked list:

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文