web-dev-qa-db-fra.com

La méthode C # Distinct () conserve-t-elle l'ordre d'origine de la séquence?

Je souhaite supprimer les doublons de la liste, sans changer l'ordre des éléments uniques dans la liste.

Jon Skeet et d’autres ont suggéré d’utiliser les suivants

list = list.Distinct().ToList();

supprimer les doublons d'une liste C #

Supprimer les doublons d'une liste <T> en C #

Est-il garanti que l'ordre des éléments uniques serait le même qu'avant? Si oui, donnez une référence qui le confirme car je n’ai rien trouvé dans la documentation.

68
Nitesh

Ce n'est pas garanti, mais c'est la mise en œuvre la plus évidente. Il serait difficile de l’implémenter en mode continu (c’est-à-dire qu’il renvoie les résultats dès que possible, ayant lu le moins possible) sans les renvoyant dans l’ordre.

Vous voudrez peut-être lire mon billet de blog sur la mise en œuvre Edulinq de Distinct () .

Notez que même si cela était garanti pour LINQ to Objects (ce que je pense personnellement devrait être), cela ne voudrait rien dire pour les autres fournisseurs LINQ tels que LINQ to SQL.

Le niveau de garantie fourni par LINQ à Objects est parfois un peu incohérent, IMO. Certaines optimisations sont documentées, d’autres pas. Heck, une partie de la documentation est à plat faux .

60
Jon Skeet

Oui, dans l'ordre de la première occurrence dans la liste d'origine. C'est garanti pour .Net Framework 3.5 

J'ai fait une petite enquête avec Reflector. Après avoir désassemblé System.Core.dll, Version = 3.5.0.0, vous pouvez voir que Distinct () est une méthode d’extension qui ressemble à ceci:

public static class Emunmerable
{
    public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        return DistinctIterator<TSource>(source, null);
    }
}

Ainsi, DistinctIterator, qui implémente IEnumerable et IEnumerator, est intéressant. Voici l'implémentation simplifiée (goto et lables enlevés) de cet IEnumerator:

private sealed class DistinctIterator<TSource> : IEnumerable<TSource>, IEnumerable, IEnumerator<TSource>, IEnumerator, IDisposable
{
    private bool _enumeratingStarted;
    private IEnumerator<TSource> _sourceListEnumerator;
    public IEnumerable<TSource> _source;
    private HashSet<TSource> _hashSet;    
    private TSource _current;

    private bool MoveNext()
    {
        if (!_enumeratingStarted)
        {
            _sourceListEnumerator = _source.GetEnumerator();
            _hashSet = new HashSet<TSource>();
            _enumeratingStarted = true;
        }

        while(_sourceListEnumerator.MoveNext())
        {
            TSource element = _sourceListEnumerator.Current;

             if (!_hashSet.Add(element))
                 continue;

             _current = element;
             return true;
        }

        return false;
    }

    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    TSource IEnumerator<TSource>.Current
    {
        get { return _current; }
    }

    object IEnumerator.Current
    {        
        get { return _current; }
    }
}

Comme vous pouvez le voir - l'énumération va dans l'ordre fourni par la source énumérable (liste, sur laquelle nous sommes appelées Distinct). Hashset utilisé uniquement pour déterminer si nous avons déjà renvoyé cet élément ou non. Sinon, nous le renvoyons, sinon, continuez d'énumérer sur la source. 

Ainsi, il est garanti que Distinct () retournera les éléments exactement dans le même ordre , qui sont fournis par la collection à laquelle Distinct a été appliqué.

24
Sergey Berezovskiy

Selon la documentation , la séquence n'est pas ordonnée.

11
mgronber

Oui , Enumerable.Distinct conserve l’ordre. En supposant que la méthode soit paresseuse "donne des valeurs distinctes dès qu'elles sont vues", il s'ensuit automatiquement. Penses-y.

La source de référence .NET confirme. Il retourne une sous-séquence, le premier élément de chaque classe d'équivalence.

foreach (TSource element in source)
    if (set.Add(element)) yield return element;

La mise en œuvre .NET Core est similaire.

Frustrant, la documentation de Enumerable.Distinct est confuse sur ce point:

La séquence de résultat n'est pas ordonnée.

Je peux seulement imaginer qu'ils veulent dire "la séquence de résultats n'est pas triée". Vous pouvez implémenter Distinct en triant puis en comparant chaque élément au précédent, mais cela ne serait pas paresseux comme défini ci-dessus.

4
Colonel Panic

Par défaut, lorsque l'opérateur distinct Linq utilise la méthode Equals, vous pouvez utiliser votre propre objet IEqualityComparer<T> pour spécifier quand deux objets sont égaux avec une logique personnalisée implémentant les méthodes GetHashCode et Equals. N'oubliez pas que:

GetHashCode ne devrait pas utiliser une comparaison de processeur lourde (par exemple, n'utiliser que des contrôles de base évidents) et être utilisé en premier pour indiquer si deux objets sont certainement différents (si un code de hachage différent est renvoyé) ou potentiellement identiques (même code de hachage). Dans ce dernier cas, lorsque deux objets ont le même hashcode, le cadre procédera à la vérification en utilisant la méthode Equals comme décision finale concernant l'égalité des objets donnés.

Une fois que vous avez MyType et que MyTypeEqualityComparer classes suivent le code, assurez-vous que la séquence conserve son ordre:

var cmp = new MyTypeEqualityComparer();
var lst = new List<MyType>();
// add some to lst
var q = lst.Distinct(cmp);

Dans follow sci library j’ai implémenté une méthode d’extension pour garantir que Vector3D conserve l’ordre lorsqu’on utilise une méthode d’extension spécifique DistinctKeepOrder:

code correspondant suit:

/// <summary>
/// support class for DistinctKeepOrder extension
/// </summary>
public class Vector3DWithOrder
{
    public int Order { get; private set; }
    public Vector3D Vector { get; private set; }
    public Vector3DWithOrder(Vector3D v, int order)
    {
        Vector = v;
        Order = order;
    }
}

public class Vector3DWithOrderEqualityComparer : IEqualityComparer<Vector3DWithOrder>
{
    Vector3DEqualityComparer cmp;

    public Vector3DWithOrderEqualityComparer(Vector3DEqualityComparer _cmp)
    {
        cmp = _cmp;
    }

    public bool Equals(Vector3DWithOrder x, Vector3DWithOrder y)
    {
        return cmp.Equals(x.Vector, y.Vector);
    }

    public int GetHashCode(Vector3DWithOrder obj)
    {
        return cmp.GetHashCode(obj.Vector);
    }
}

En bref, Vector3DWithOrder encapsule le type et un entier d'ordre, tandis que Vector3DWithOrderEqualityComparer encapsule le comparateur de type d'origine.

et c'est l'assistant de la méthode pour assurer le maintien de l'ordre

/// <summary>
/// retrieve distinct of given vector set ensuring to maintain given order
/// </summary>        
public static IEnumerable<Vector3D> DistinctKeepOrder(this IEnumerable<Vector3D> vectors, Vector3DEqualityComparer cmp)
{
    var ocmp = new Vector3DWithOrderEqualityComparer(cmp);

    return vectors
        .Select((w, i) => new Vector3DWithOrder(w, i))
        .Distinct(ocmp)
        .OrderBy(w => w.Order)
        .Select(w => w.Vector);
}

Note : des recherches ultérieures pourraient permettre de trouver une manière plus générale (utilisations des interfaces) et optimisée (sans encapsuler l'objet).

1
Lorenzo Delana

Cela dépend fortement de votre fournisseur linq. Sur Linq2Objects, vous pouvez rester sur le code source interne pour Distinct, ce qui laisse supposer que l'ordre d'origine est préservé.

Cependant, pour d'autres fournisseurs qui résolvent en SQL par exemple, ce n'est pas nécessairement le cas, car une instruction ORDER BY- vient généralement après toute agrégation (telle que Distinct). Donc si votre code est ceci:

myArray.OrderBy(x => anothercol).GroupBy(x => y.mycol);

cela se traduit par quelque chose de similaire à ce qui suit en SQL:

SELECT * FROM mytable GROUP BY mycol ORDER BY anothercol;

Cela commence évidemment par regrouper vos données et les trier par la suite. Maintenant, vous êtes bloqué sur la propre logique du SGBD pour l’exécuter. Sur certains SGBD, cela n'est même pas autorisé. Imaginez les données suivantes:

mycol anothercol
1     2
1     1
1     3
2     1
2     3

lors de l'exécution de myArr.OrderBy(x => x.anothercol).GroupBy(x => x.mycol), nous supposons le résultat suivant:

mycol anothercol
1     1
2     1

Mais le SGBD peut agréger la colonne anothercol afin que la valeur de la première ligne soit toujours utilisée, ce qui donne les données suivantes:

mycol anothercol
1    2
2    1

qui après la commande aura comme conséquence:

mycol anothercol
2    1
1    2

Ceci est similaire à ce qui suit:

SELECT mycol, First(anothercol) from mytable group by mycol order by anothercol;

qui est l’ordre complètement inverse de ce que vous attendiez.

Vous voyez que le plan d'exécution peut varier en fonction du fournisseur sous-jacent. C’est pourquoi il n’ya aucune garantie à ce sujet dans la documentation.

0
HimBromBeere