C# 使用 LINQ 获取 IEnumerable 中的上一项和下一项

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/8759849/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-09 04:18:07  来源:igfitidea点击:

Get previous and next item in a IEnumerable using LINQ

c#linqienumerable

提问by Bob2Chiv

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

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

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

我在 IEnumerable 中还有一个项目 (myItem),我想要来自 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.

我可以通过简单地省略.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.

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

  1. Which is better between the two options?
  2. Is there an even better option available?
  1. 这两种选择哪个更好?
  2. 有没有更好的选择?

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

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

采纳答案by Binary Worrier

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, FindItemReturningPreviousItemFoundItemAndNextItemis a bit of a mouthful).

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

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.

希望这可以帮助。

回答by Patrick McDonald

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]

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

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

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

回答by Lasse Espeholt

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.

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

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.

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

回答by spender

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

为了可读性,我将加载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

回答by msarchet

You are really over complicating things:

你真的把事情复杂化了:

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

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

回答by Bartosz Wójtowicz

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.

如果 myIEnumerable 中的每个元素都需要它,我只需遍历它,保留对前 2 个元素的引用。在循环体中,我会处理第二个前一个元素,当前元素是它的后代,第一个是它的祖先。

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

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

回答by PHeiberg

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

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

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

回答by HedgeHogFace

I thought I would try to answer this using Zipfrom Linq.

我想我会尝试使用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 );

This will return a anonymous type {previous,current,next}. Unfortunately this will only work for indexes 1,2 and 3.

这将返回一个匿名类型 {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 );

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

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

回答by jwize

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.

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

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

回答by Fidel

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 Previousvalue, and the last Nextvalue.

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

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