¿El método C # Distinct () mantiene intacto el orden original de la secuencia?

82

Quiero eliminar los duplicados de la lista, sin cambiar el orden de los elementos únicos en la lista.

Jon Skeet y otros han sugerido usar lo siguiente

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

eliminar duplicados de una lista C #

Eliminar duplicados de una List <T> en C #

¿Está garantizado que el orden de los elementos únicos sea el mismo que antes? En caso afirmativo, proporcione una referencia que confirme esto, ya que no pude encontrar nada en la documentación.

Nitesh
fuente
5
@ColonelPanic - documentación oficial aquí msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx declara explícitamente "El método Distinct () devuelve una secuencia desordenada que no contiene valores duplicados".
Evk
@Evk 'Secuencia desordenada' no es lo mismo que 'orden original de secuencia'.
Nitesh
3
Considero que "no ordenado" significa "sin ningún orden en particular", lo que también implica "no es necesario en el orden original de secuencia".
Evk
Acabo de tener un problema con respecto a distinto con oracle12 Entity Framework 6. En mi caso, tuve orderby antes de desinfectar en mi cláusula linq y el pedido desapareció. select (). OrderBy (). Distinct (). ToList () no funcionó mientras que select (). OrderBy (). Distinct (). ToList () funcionó.
Karl
2
@Karl, estas expresiones son las mismas. :)
pvgoran

Respuestas:

75

No está garantizado, pero es la implementación más obvia. Sería difícil de implementar en forma de transmisión (es decir, de modo que devolviera resultados tan pronto como pudiera, habiendo leído tan poco como pudo) sin devolverlos en orden.

Es posible que desee leer la publicación de mi blog sobre la implementación de Edulinq de Distinct () .

Tenga en cuenta que incluso si esto estuviera garantizado para LINQ to Objects (que personalmente creo que debería ser) eso no significaría nada para otros proveedores de LINQ como LINQ to SQL.

El nivel de garantías proporcionado dentro de LINQ to Objects es un poco inconsistente a veces, en mi opinión. Algunas optimizaciones están documentadas, otras no. Diablos, parte de la documentación es completamente incorrecta .

Jon Skeet
fuente
Lo acepto porque 1) Responde claramente a mi preocupación si está garantizado o no 2) La publicación vinculada profundiza en los aspectos indocumentados de Distinct 3) La publicación vinculada también tiene una implementación de muestra que puede usarse como referencia para implementar una Distinct en Listas con esa garantía.
Nitesh
26

En .NET Framework 3.5, desensamblar el CIL de la implementación de Linq-to-Objects de Distinct()muestra que se conserva el orden de los elementos; sin embargo, esto no es un comportamiento documentado.

Hice una pequeña investigación con Reflector. Después de desensamblar System.Core.dll, Version = 3.5.0.0, puede ver que Distinct () es un método de extensión, que se ve así:

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

Entonces, interesante aquí es DistinctIterator, que implementa IEnumerable e IEnumerator. Aquí está la implementación simplificada (goto y lables eliminados) de este 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; }
    }
}

Como puede ver, la enumeración va en el orden proporcionado por la fuente enumerable (lista, a la que estamos llamando Distinct). Hashsetse usa solo para determinar si ya devolvimos dicho elemento o no. Si no, lo devolveremos, de lo contrario, continúe enumerando en la fuente.

Por lo tanto, se garantiza que Distinct()devolverá los elementos exactamente en el mismo orden , que son proporcionados por la colección a la que se aplicó Distinct.

Sergey Berezovskiy
fuente
8
¿Es un comportamiento bien documentado?
abatishchev
4
La respuesta vinculada contiene una referencia a la documentación que dice: "La secuencia de resultados no está ordenada".
mgronber
4
@lazyberezovsky: La pregunta es sobre garantías , no sobre implementación común . (Como ya dije, me sorprendería si la implementación cambia alguna vez entre plataformas / versiones, pero eso no equivale a una garantía).
LukeH
5
@lazyberezovsky: Soy de C \ C ++ donde muchas cosas no están definidas y es muy común preguntar si algo está garantizado. También estoy usando Distinct () en una aplicación Silverlight, que está tanto en Mac como en Windows, por eso no podemos conformarnos con una 'implementación común', debe garantizarse.
Nitesh
42
@lazyberezovsky: Cuando la gente habla de garantías, normalmente se refiere a un comportamiento documentado en el que es razonable confiar. Por ejemplo, la documentación para GroupBy no especifican el comportamiento, pero la documentación para Distinto no lo hacen .
Jon Skeet
14

Según la documentación, la secuencia está desordenada.

mgronber
fuente
2
Información adicional para encontrarlo: En el enlace, consulte la sección "Comentarios". "La secuencia de resultados no está ordenada".
Curtis Yallop
6

, Enumerable. Distinct conserva el orden. Suponiendo que el método es perezoso "produce valores distintos tan pronto como se ven", sigue automáticamente. Piénsalo.

La fuente de .NET Reference confirma. Devuelve una subsecuencia, el primer elemento de cada clase de equivalencia.

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

La implementación de .NET Core es similar.

Es frustrante que la documentación de Enumerable.Distinct se confunda en este punto:

La secuencia de resultados no está ordenada.

Solo puedo imaginar que quieren decir "la secuencia de resultados no está ordenada". Usted podría implementar Distinto de clasificación previa luego comparar cada elemento a la anterior, pero esto no sería perezosa como se definió anteriormente.

Coronel Panic
fuente
6
La fuente no es la especificación. Lo que encontró es una coincidencia y podría no ser válido después de la próxima actualización.
Henk Holterman
@HenkHolterman En general, estoy de acuerdo, las implementaciones pueden cambiar. Por ejemplo, .NET 4.5 cambió el algoritmo de clasificación detrás de Array.Sort. Sin embargo, en este caso particular, cualquier implementación sensata de Enumerable.Distinct seguramente será perezosa ("produce valores distintos tan pronto como se vean"), y la propiedad de preservación del orden se deriva de eso. La evaluación perezosa es un principio básico de LINQ to Objects; rescindirlo sería impensable.
Colonel Panic
1
He visto implementaciones que usan .net 4.6 donde las llamadas dbQuery.OrderBy(...).Distinct().ToList()no devuelven una lista en el orden especificado por el orden por predicado; eliminar Distinct (que resultó ser redundante) solucionó el error en mi caso
Rowland Shaw
1

De forma predeterminada, cuando se usa Distinct, el operador linq usa el método Equals, pero puede usar su propio IEqualityComparer<T>objeto para especificar cuándo dos objetos son iguales con una implementación lógica personalizada GetHashCodey un Equalsmétodo. Recuérdalo:

GetHashCodeno debe usar una comparación de CPU pesada (por ejemplo, use solo algunas comprobaciones básicas obvias) y se usa como el primero para indicar si dos objetos son seguramente diferentes (si se devuelven diferentes códigos hash) o potencialmente iguales (el mismo código hash). En este último caso, cuando dos objetos tienen el mismo código hash, el marco pasará a verificar utilizando el método Equals como una decisión final sobre la igualdad de los objetos dados.

Después de que tenga MyTypey una MyTypeEqualityComparerclase siga el código, no asegúrese de que la secuencia mantenga su orden:

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

En la siguiente biblioteca de ciencia , implementé un método de extensión para asegurar que el conjunto de Vector3D mantenga el orden cuando se usa un método de extensión específico DistinctKeepOrder:

sigue el código relevante:

/// <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 resumen, Vector3DWithOrderencapsula el tipo y un entero de orden, mientras que Vector3DWithOrderEqualityComparerencapsula el comparador de tipo original.

y este es el método auxiliar para garantizar que se mantenga el orden

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

Nota : una mayor investigación podría permitir encontrar una forma más general (usos de interfaces) y optimizada (sin encapsular el objeto).

Lorenzo Delana
fuente
1

Esto depende en gran medida de su proveedor de linq. En Linq2Objects puede permanecer en el código fuente interno Distinct, lo que hace suponer que se conserva el orden original.

Sin embargo, para otros proveedores que resuelven algún tipo de SQL, por ejemplo, ese no es necesariamente el caso, ya que una ORDER BYdeclaración -por lo general viene después de cualquier agregación (como Distinct). Entonces, si su código es este:

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

esto se traduce a algo similar a lo siguiente en SQL:

SELECT * FROM mytable GROUP BY mycol ORDER BY anothercol;

Obviamente, esto primero agrupa sus datos y luego los ordena. Ahora estás atascado en la propia lógica del DBMS de cómo ejecutar eso. En algunos DBMS esto ni siquiera está permitido. Imagina los siguientes datos:

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

al ejecutar myArr.OrderBy(x => x.anothercol).GroupBy(x => x.mycol)asumimos el siguiente resultado:

mycol anothercol
1     1
2     1

Pero el DBMS puede agregar la otra columna de modo que siempre se use el valor de la primera fila, dando como resultado los siguientes datos:

mycol anothercol
1    2
2    1

que después de realizar el pedido resultará en esto:

mycol anothercol
2    1
1    2

Esto es similar a lo siguiente:

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

que es el orden completamente inverso al esperado.

Verá que el plan de ejecución puede variar según el proveedor subyacente. Es por eso que no hay garantía al respecto en los documentos.

HimBromBeere
fuente