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