Получить предыдущий и следующий элемент в 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), чтобы получить предыдущий или следующий элемент.
- Что лучше между этими двумя варианты?
- есть ли еще лучший вариант?
"лучше" включает в себя сочетание производительности (памяти и скорости) и читабельности; при этом читабельность является моей главной заботой.
8 ответов:
Во-первых
В общем, вы не можете иметь и то, и другое, эмпирическое правило таково: если вы оптимизируете скорость, это будет стоить памяти, если вы оптимизируете память, это будет стоить вам скорости."лучше" включает комбинацию производительности (памяти и скорости)
Есть лучший вариант, который хорошо работает как на фронтах памяти, так и на фронтах скорости, и может быть использован в удобочитаемой манере (я не в восторге от имени функции, однако,
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];Значения по умолчанию для возврата, если это первый или последний элемент, возможно, потребуется изменить в зависимости от ваших требований.
Надеюсь, это поможет.
Для удобства чтения я бы загрузил
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
Создавая метод расширения для установления контекста к текущему элементу, вы можете использовать запрос 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; } } }
Вы можете кэшировать перечисляемое в списке
var myList = myIEnumerable.ToList()Перебираем его по индексу
for (int i = 0; i < myList.Count; i++)Тогда текущий элемент -
myList[i], предыдущий элемент -myList[i-1], а следующий элемент -myList[i+1](Не забывайте о частных случаях первого и последнего элементов в списке.)
Процессор
Полностью зависит от того, где находится объект в последовательности. Если он расположен в конце, я ожидал бы, что второй будет быстрее с более чем фактором 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)]; } }
Я подумал, что попробую ответить на этот вопрос, используя 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.
Если вам это нужно для каждого элемента в myIEnumerable, я бы просто перебирал его, сохраняя ссылки на 2 предыдущих элемента. В теле цикла я бы сделал обработку для второго предыдущего элемента, и ток был бы его потомком, а первый предыдущий-его предком.
Если вам это нужно только для одного элемента, я бы выбрал ваш первый подход.
Comments