web-dev-qa-db-fra.com

Récupérer l'élément précédent et suivant dans un IEnumerable à l'aide de LINQ

J'ai un IEnumerable d'un type personnalisé. (Que j'ai obtenu d'un SelectMany)

J'ai également un élément (myItem) dans cet IEnumerable que je désire l'élément précédent et suivant de l'IEnumerable.

Actuellement, je fais le souhaité comme ceci:

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

Je peux obtenir l'article suivant en omettant simplement le .Reverse.

o, je pourrais:

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

puis utilisez .ElementAt(index +/- 1) pour obtenir l'élément précédent ou suivant.

  1. Quel est le meilleur entre les deux options?
  2. Existe-t-il une option encore meilleure disponible?

"Mieux" comprend une combinaison de performances (mémoire et vitesse) et de lisibilité; la lisibilité étant ma principale préoccupation.

31
Bob2Chiv

Tout d'abord

"Mieux" comprend une combinaison de performances (mémoire et vitesse)

En général, vous ne pouvez pas avoir les deux, la règle de base est que si vous optimisez pour la vitesse, cela coûtera de la mémoire, si vous optimisez pour la mémoire, cela vous coûtera de la vitesse.

Il existe une meilleure option, qui fonctionne bien à la fois sur la mémoire et sur les fronts de vitesse, et peut être utilisée de manière lisible (je ne suis pas ravi du nom de la fonction, cependant, FindItemReturningPreviousItemFoundItemAndNextItem est un peu gorgée) .

Il semble donc qu'il soit temps pour une méthode d'extension de recherche personnalisée, quelque chose comme. . .

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
}

Vous pouvez mettre en cache le résultat de cette opération dans une liste si vous devez faire référence aux éléments plusieurs fois, mais il renvoie l'élément trouvé, précédé de l'élément précédent, suivi de l'élément suivant. par exemple.

var sandwichedItems = myIEnumerable.FindSandwichedItem(item => item.objectId == "MyObjectId").ToList();
var previousItem = sandwichedItems[0];
var myItem = sandwichedItems[1];
var nextItem = sandwichedItems[2];

Les valeurs par défaut à renvoyer s'il s'agit du premier ou du dernier élément peuvent devoir être modifiées en fonction de vos besoins.

J'espère que cela t'aides.

39
Binary Worrier

Pour plus de lisibilité, je chargerais le IEnumerable dans une liste chaînée:

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

En créant une méthode d'extension pour établir le contexte de l'élément actuel, vous pouvez utiliser une requête Linq comme celle-ci:

var result = myIEnumerable.WithContext()
    .Single(i => i.Current.UniqueObjectID == myItem.UniqueObjectID);
var previous = result.Previous;
var next = result.Next;

L'extension ressemblerait à ceci:

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

Vous pouvez mettre en cache l'énumérable dans une liste

var myList = myIEnumerable.ToList()

itérer dessus par index

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

alors l'élément courant est myList[i], l'élément précédent est myList[i-1], et l'élément suivant est myList[i+1]

(N'oubliez pas les cas particuliers des premier et dernier éléments de la liste.)

4
Patrick McDonald

CPU

Dépend entièrement de l'emplacement de l'objet dans la séquence. S'il est situé à la fin, je m'attendrais à ce que le second soit plus rapide avec plus d'un facteur 2 (mais seulement un facteur constant). S'il se trouve au début, le premier sera plus rapide car vous ne parcourez pas toute la liste.

Mémoire

La première consiste à itérer la séquence sans l'enregistrer, de sorte que le hit mémoire sera très petit. La deuxième solution prendra autant de mémoire que la longueur de la liste * références + objets + surcharge.

2
Lasse Espeholt

Vous compliquez vraiment les choses:

Parfois, une simple boucle for vaudra mieux faire quelque chose, et je pense fournir une mise en œuvre plus claire de ce que vous essayez de faire /

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

J'ai pensé que j'essaierais de répondre à cela en utilisant Zip de 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 );

Cela retournera un type anonyme {précédent, actuel, suivant}. Malheureusement, cela ne fonctionnera que pour les index 1,2 et 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 );

Cette version fonctionnera pour tous les index. Les deux versions utilisent l'évaluation paresseuse avec l'aimable autorisation de Linq.

2
HedgeHogFace

Voici quelques méthodes d'extension promises. Les noms sont génériques et réutilisables avec n'importe quel type simple et il existe des surcharges de recherche pour obtenir l'élément nécessaire pour obtenir les éléments suivants ou précédents. Je comparerais les solutions et verrais ensuite où vous pourriez éliminer les cycles.

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

Et vous pouvez les utiliser comme ça.

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

Si vous en avez besoin pour chaque élément de myIEnumerable, je voudrais simplement le parcourir en gardant les références aux 2 éléments précédents. Dans le corps de la boucle, je ferais le traitement du deuxième élément précédent et le courant serait son descendant et le premier précédent son ancêtre.

Si vous en avez besoin pour un seul élément, je choisirais votre première approche.

0
Bartosz Wójtowicz