LINQ Seleccione distinto con tipos anónimos

150

Entonces tengo una colección de objetos. El tipo exacto no es importante. De él quiero extraer todos los pares únicos de un par de propiedades particulares, por lo tanto:

myObjectCollection.Select(item=>new
                                {
                                     Alpha = item.propOne,
                                     Bravo = item.propTwo
                                }
                 ).Distinct();

Entonces mi pregunta es: ¿Distinct en este caso usará el objeto predeterminado igual (que será inútil para mí, ya que cada objeto es nuevo) o se le puede pedir que haga un igual diferente (en este caso, valores iguales de Alpha y Bravo => instancias iguales)? ¿Hay alguna forma de lograr ese resultado, si esto no lo hace?

GWLlosa
fuente
¿Es esto LINQ-to-Objects o LINQ-to-SQL? Si solo se trata de objetos, probablemente no tengas suerte. Sin embargo, si es L2S, entonces puede funcionar, ya que DISTINCT se pasaría a la instrucción SQL.
James Curran

Respuestas:

188

Lee aquí la excelente publicación de K. Scott Allen:

E igualdad para todos ... Tipos anónimos

La respuesta corta (y cito):

Resulta que el compilador de C # anula Equals y GetHashCode para los tipos anónimos. La implementación de los dos métodos anulados utiliza todas las propiedades públicas del tipo para calcular el código hash de un objeto y probar la igualdad. Si dos objetos del mismo tipo anónimo tienen los mismos valores para sus propiedades, los objetos son iguales.

Por lo tanto, es totalmente seguro usar el método Distinct () en una consulta que devuelve tipos anónimos.

Matt Hamilton
fuente
2
Creo que esto solo es cierto si las propiedades en sí mismas son tipos de valores o implementan la igualdad de valores, vea mi respuesta.
tvanfosson
Sí, dado que usa GetHashCode en cada propiedad, entonces solo funcionaría si cada propiedad tuviera su propia implementación única. Creo que la mayoría de los casos de uso solo involucrarían tipos simples como propiedades, por lo que generalmente es seguro.
Matt Hamilton
44
Resulta que significa que la igualdad de dos de los tipos anónimos depende de la igualdad de los miembros, lo cual está bien para mí, ya que los miembros están definidos en algún lugar al que puedo llegar y anular la igualdad si es necesario. Simplemente no quería tener que crear una clase para esto solo para anular iguales.
GWLlosa
3
Podría valer la pena solicitar a MS que introduzca la sintaxis de "clave" en C # que tiene VB (donde puede especificar ciertas propiedades de un tipo anónimo para que sea la "clave principal"; consulte la publicación de blog a la que me vinculé).
Matt Hamilton
1
Artículo muy interesante ¡Gracias!
Alexander Prokofyev
14
public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();

Perdón por el mal formato anterior


fuente
Estas extensiones no pueden manejar de tipo objecty object. Si los dos objectes stringaún devolver las filas duplicadas. Pruebe el FirstNameis typeof objecty asigne con el mismo stringallí.
CallMeLaNN
Esta es una gran respuesta para los objetos escritos, pero no es necesaria para los tipos anónimos.
crokusek
5

Interesante que funcione en C # pero no en VB

Devuelve las 26 letras:

var MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ";
MyBet.ToCharArray()
.Select(x => new {lower = x.ToString().ToLower(), upper = x.ToString().ToUpper()})
.Distinct()
.Dump();

Devuelve 52 ...

Dim MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
MyBet.ToCharArray() _
.Select(Function(x) New With {.lower = x.ToString.ToLower(), .upper = x.ToString.ToUpper()}) _
.Distinct() _
.Dump()
GeorgeBarker
fuente
11
Si agrega la Keypalabra clave al tipo anónimo .Distinct(), funcionará según lo previsto (por ejemplo New With { Key .lower = x.ToString.ToLower(), Key .upper = x.ToString.ToUpper()}).
Cᴏʀʏ
3
Cory tiene razón. La traducción correcta del código C # new {A = b}es New {Key .A = b}. Las propiedades no clave en las clases anónimas de VB son mutables, por lo que se comparan por referencia. En C #, todas las propiedades de las clases anónimas son inmutables.
Heinzi
4

Ejecuté una pequeña prueba y descubrí que si las propiedades son tipos de valor, parece funcionar bien. Si no son tipos de valor, entonces el tipo necesita proporcionar sus propias implementaciones Equals y GetHashCode para que funcione. Las cuerdas, creo, funcionarían.

tvanfosson
fuente
2

Puede crear su propio método de extensión distinta que toma la expresión lambda. Aquí hay un ejemplo

Cree una clase que se derive de la interfaz IEqualityComparer

public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

Luego cree su método de extensión distinta

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

y puedes usar este método para encontrar elementos distintos

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();
Buildstarted
fuente
Estas extensiones no pueden manejar de tipo objecty object. Si los dos objectes stringaún devolver las filas duplicadas. Pruebe el FirstNameis typeof objecty asigne con el mismo stringallí.
CallMeLaNN
0

Si Alphay Bravotanto heredar de una clase común, usted será capaz de dictar la comprobación de la igualdad en la clase padre mediante la implementación IEquatable<T>.

Por ejemplo:

public class CommonClass : IEquatable<CommonClass>
{
    // needed for Distinct()
    public override int GetHashCode() 
    {
        return base.GetHashCode();
    }

    public bool Equals(CommonClass other)
    {
        if (other == null) return false;
        return [equality test];
    }
}
ern
fuente
así que si usa como propiedades de sus clases de tipos anónimos que implementa IEquatable <T>, se llama a Equals en lugar del comportamiento predeterminado (¿se comprueban todas las propiedades públicas a través de la reflexión?)
D_Guidi
0

Hola, tuve el mismo problema y encontré una solución. Debe implementar la interfaz IEquatable o simplemente anular los métodos (Equals & GetHashCode). Pero este no es el truco, el truco que viene en el Método GetHashCode. No debe devolver el código hash del objeto de su clase, pero debe devolver el hash de la propiedad que desea comparar de esa manera.

public override bool Equals(object obj)
    {
        Person p = obj as Person;
        if ( obj == null )
            return false;
        if ( object.ReferenceEquals( p , this ) )
            return true;
        if ( p.Age == this.Age && p.Name == this.Name && p.IsEgyptian == this.IsEgyptian )
            return true;
        return false;
        //return base.Equals( obj );
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

Como puede ver, obtuve una clase llamada persona que obtuvo 3 propiedades (Nombre, Edad, IsEgyptian "Porque lo soy") En el GetHashCode devolví el hash de la propiedad Nombre, no el objeto Persona.

Pruébalo y funcionará ISA. Gracias Modather Sadik.

Modather Sadik
fuente
1
GetHashCode debería usar todos los mismos campos y propiedades que se usan en la comparación para la igualdad, no solo uno de ellos. es decirpublic override int GetHashCode() { return this.Name.GetHashCode() ^ this.Age.GetHashCode() ^ this.IsEgyptian.GetHashCode(); }
JG en SD
Para obtener información sobre cómo generar un buen algoritmo hash: stackoverflow.com/questions/263400/…
JG en SD
0

Para que funcione en VB.NET, debe especificar la Keypalabra clave antes de cada propiedad en el tipo anónimo, así:

myObjectCollection.Select(Function(item) New With
{
    Key .Alpha = item.propOne,
    Key .Bravo = item.propTwo
}).Distinct()

Estaba luchando con esto, pensé que VB.NET no era compatible con este tipo de característica, pero en realidad lo hace.

Alisson
fuente