Подтвердить что ты не робот

Получить предыдущий и следующий элемент в IEnumerable, используя LINQ

У меня есть IEnumerable настраиваемого типа. (Что я получил от SelectMany)

У меня также есть элемент (myItem) в этом IEnumerable, который мне нужен предыдущий и следующий элемент из 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), чтобы получить предыдущий или следующий элемент.

  • Что лучше между двумя вариантами?
  • Есть ли еще лучший вариант?

"Лучше" включает в себя сочетание производительности (памяти и скорости) и читаемости; с читабельностью, являющейся моей главной задачей.

4b9b3361

Ответ 1

Сначала

"Лучше" включает комбинацию производительности (памяти и скорости)

В общем, вы не можете иметь оба, главное правило - если вы оптимизируете скорость, это будет стоить памяти, если вы оптимизируете память, это будет стоить вам скорости.

Существует лучший вариант, который хорошо работает как на фронтах памяти, так и на скорости, и может использоваться в читаемом режиме (я не восхищен именем функции, однако 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];

По умолчанию возвращаются, если это может изменить первый или последний элемент в зависимости от ваших требований.

Надеюсь, что это поможет.

Ответ 2

Для удобства чтения я загрузил 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

Ответ 3

Создав метод расширения для установления контекста для текущего элемента, вы можете использовать запрос 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;
        }
    }
}

Ответ 4

Вы можете кэшировать перечислимое в списке

var myList = myIEnumerable.ToList()

перебираем по индексу

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

тогда текущий элемент myList[i], предыдущий элемент - myList[i-1], а следующий элемент - myList[i+1]

(Не забывайте о специальных случаях первого и последнего элементов в списке.)

Ответ 5

CPU

Зависит полностью от того, где объект находится в последовательности. Если он находится в конце, я ожидаю, что второй будет быстрее с более чем фактором 2 (но только постоянным фактором). Если он находится в начале, первый будет быстрее, потому что вы не пройдете весь список.

Память

Во-первых, итерация последовательности, не сохраняя последовательность, так что поражение памяти будет очень маленьким. Второе решение займет столько же памяти, сколько длина списка * ссылки + объекты + служебные данные.

Ответ 6

Вы действительно усложняете вещи:

Иногда просто цикл 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)];
   }
} 

Ответ 7

Я думал, что постараюсь ответить на это, используя Zip из 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 );

Это приведет к анонимному типу {предыдущий, текущий, следующий}. К сожалению, это будет работать только для индексов 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.

Ответ 8

Если вам это нужно для каждого элемента в myIEnumerable Id, просто перебирайте его, сохраняя ссылки на 2 предыдущих элемента. В теле цикла я сделаю обработку для второго предыдущего элемента, и текущий будет его потомком и первым предыдущим его предком.

Если вам это нужно только для одного элемента, я бы выбрал ваш первый подход.

Ответ 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);