web-dev-qa-db-ja.com

LINQを使用してIEnumerableの前のアイテムと次のアイテムを取得する

カスタムタイプのIEnumerableがあります。 (SelectManyから取得したこと)

また、そのIEnumerableにアイテム(myItem)があり、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つのオプションのどちらが良いですか?
  2. さらに良いオプションがありますか?

「より良い」には、パフォーマンス(メモリと速度)と読みやすさの組み合わせが含まれます。読みやすさは私の最大の関心事です。

31
Bob2Chiv

最初に

「より良い」には、パフォーマンス(メモリと速度)の組み合わせが含まれます

一般に、両方を使用することはできません。経験則では、速度を最適化するとメモリが消費され、メモリを最適化すると速度が低下します。

メモリとスピードの両方の面で良好に機能し、読みやすい方法で使用できる、より良いオプションがあります(ただし、関数名には満足していませんが、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];

デフォルトは、要件に応じて最初または最後のアイテムを変更する必要がある場合に返されます。

お役に立てれば。

39
Binary Worrier

読みやすくするために、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
20
spender

現在の要素へのコンテキストを確立するための拡張メソッドを作成することにより、次のような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;
        }
    }
}
13
PHeiberg

[〜#〜] cpu [〜#〜]

オブジェクトがシーケンス内のどこにあるかに完全に依存します。最後に配置されている場合、2番目の要素が2倍以上(ただし、一定の要素のみ)で高速になると予想されます。最初にある場合、リスト全体をたどらないため、最初のほうが速くなります。

メモリ

1つ目は、メモリヒットが非常に小さくなるように、シーケンスを保存せずにシーケンスを繰り返すことです。 2番目のソリューションは、リストの長さ*参照+オブジェクト+オーバーヘッドと同じ量のメモリを消費します。

2
Lasse Espeholt

あなたは本当に物事を複雑にしています:

時々、単に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)];
   }
} 
2
msarchet

Linqの Zip を使用してこれに答えようと思いました。

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 );

これは、匿名型{previous、current、next}を返します。残念ながら、これはインデックス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の好意による遅延評価を使用しています。

2
HedgeHogFace

約束されたいくつかの拡張メソッドがあります。名前は汎用であり、任意のタイプのシンプルで再利用できます。また、次または前のアイテムを取得するために必要なアイテムを取得するためのルックアップのオーバーロードがあります。ソリューションのベンチマークを行い、サイクルを絞り出すことができる場所を確認します。

 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);
0
jwize

MyIEnumerableのすべての要素に必要な場合は、前の2つの要素への参照を維持しながら繰り返します。ループの本体では、2番目の前の要素の処理を行います。現在の要素はその子孫で、最初の要素はその祖先です。

1つの要素にのみ必要な場合は、最初のアプローチを選択します。

0