2012-01-06 1 views
28

У меня есть IEnumerable настраиваемого типа. (Что я получил от SelectMany)Получить предыдущий и следующий элемент в IEnumerable, используя LINQ

У меня также есть элемент (myItem) в этом IEnumerable, который мне нужен предыдущий и следующий элемент из IEnumerable.

В настоящее время я делаю желаемым так:

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

я могу получить следующий элемент просто ommitting в .Reverse.

или, я мог бы:

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

, а затем использовать .ElementAt(index +/- 1), чтобы получить предыдущий или следующий элемент.

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

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

+1

Учитывая читаемость, ваш первый вариант является удивительным! Гораздо лучше в ответах. Будет использовать его! Благодаря! –

+0

Я согласен с @Aleksei, используя этот тоже –

ответ

36

Во-первых

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

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

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

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

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

+5

Чувствует себя немного излишним, но все же отличный ответ. – msarchet

+0

Я ненавижу преобразовывать вещи в «Список <>», чтобы упростить поиск (иногда это непрактично), и даже если вы только когда-нибудь сделаете это, как только вам понадобится код чего-то подобного. ИМХО, вкладывая его в свою собственную функцию, не имеет проблем, что делает его общим, так же просто, как и нет. Единственная реальная проблема с кодом - это имя функции. –

+0

После исправления некоторых незначительных ошибок сборки и проверки их надлежащим образом, я должен сказать, что меня впечатлило. Это выполняется точно так, как требуется, и мне нравится его читаемость. Главный недостаток (для меня) - это вызов '.ToList()' для получения элементов; Ну что ж. Я также переименовал функцию в функцию «FindSurroundingItems », которая достаточно точна для моих целей. – Bob2Chiv

3

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

var myList = myIEnumerable.ToList() 

итерацию над ним по индексу

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

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

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

+0

. Лучший способ сделать это - просто использовать модуль '%' для записей '[i-1]' '[i + 1]' – msarchet

+0

Только если вы хотите, чтобы элемент после последнего элемента, который будет первым элементом, и наоборот –

2

процессора

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

память

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

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 
+0

Мне очень нравится этот ответ для удобочитаемости; но похоже, что это будет битва. – Bob2Chiv

+0

Это буквально суть двусвязного списка, я не понимаю, почему у всех аллергия на очереди и связанные списки здесь. Я хотел бы знать, создает ли создание очереди или LinkedList из другого IEnumerable даже приводит к копированию. Похоже, он мог просто использовать оригинал, который можно перечислить как указатели. – Novaterata

+0

@novaterata: Создание (например) 'Queue' из' IEnumerable' будет ** копировать ** ссылки в 'IEnumerable'. Таким образом, мы не будем копировать объекты, на которые указывает «IEnumerable», но копируют ссылки на эти объекты. Это так же дорого, как и элементы в последовательности. Если бы это было что-то, что мне нужно было делать регулярно в цикле, я бы нашел другой путь. _that сказал, что я ничего не имею против Связанных списков и очередей, но мне не нравятся выполнение ненужных копий. –

1

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

Иногда просто 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)]; 
    } 
} 
+1

Поддерживает ли IEnumerable оператор operator [] или свойство Length? Насколько я знаю. Этот код нарушен. – spender

+0

@spender ah предназначен для преобразования в список в первую очередь. – msarchet

+0

Разве это не будет таким же, как второй метод OP, кроме того, что он использует FindIndex вместо цикла for? – Chris

0

Если вам это нужно для каждого элемента в myIEnumerable, я просто перебираю его, сохраняя ссылки на 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; 
     } 
    } 
} 
2

Я думал, попытался бы ответить на это, используя 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.