Получить предыдущий и следующий элемент в 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), чтобы получить предыдущий или следующий элемент.




  1. Что лучше между этими двумя варианты?

  2. есть ли еще лучший вариант?


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

768   8  

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

    Ничего не найдено.