2010-11-29 12 views
7

Estoy tratando de escribir una consulta Linq que devuelve una matriz de objetos, con valores únicos en sus constructores. Para los tipos enteros, Distinct devuelve solo una copia de cada valor, pero cuando intento crear mi lista de objetos, las cosas se desmoronan. Sospecho que es un problema con el operador de igualdad para mi clase, pero cuando configuro un punto de interrupción, nunca se golpea.Distinct() devuelve duplicados con un tipo definido por el usuario

Al filtrar el duplicado int en una subexpresión resuelve el problema, y ​​también me ahorra la construcción de objetos que se descartarán inmediatamente, pero me llama la atención por qué esta versión no funciona.

ACTUALIZACIÓN: 11:04 PM Varias personas han señalado que MyType no anula GetHashCode(). Me temo que simplifiqué demasiado el ejemplo. El MyType original sí lo implementa. Lo he agregado a continuación, modificado solo para poner el código hash en una variable temp antes de devolverlo.

Ejecutando a través del depurador, veo que las cinco invocaciones de GetHashCode devuelven un valor diferente. Y como MyType solo hereda de Object, es probable que este sea el mismo comportamiento que Object exhibiría.

¿Sería correcto concluir que el hash debería basarse en los contenidos de Value? Este fue mi primer intento de anular a los operadores, y en ese momento, no parecía que GetHashCode tuviera que ser particularmente elegante. (Esta es la primera vez que uno de mis cheques de igualdad no parecía funcionar correctamente.)

class Program 
{ 
    static void Main(string[] args) 
    { 
     int[] list = { 1, 3, 4, 4, 5 }; 
     int[] list2 = 
      (from value in list 
      select value).Distinct().ToArray(); // One copy of each value. 
     MyType[] distinct = 
      (from value in list 
      select new MyType(value)).Distinct().ToArray(); // Two objects created with 4. 

     Array.ForEach(distinct, value => Console.WriteLine(value)); 
    } 
} 

class MyType 
{ 
    public int Value { get; private set; } 

    public MyType(int arg) 
    { 
     Value = arg; 
    } 

    public override int GetHashCode() 
    { 
     int retval = base.GetHashCode(); 
     return retval; 
    } 

    public override bool Equals(object obj) 
    { 
     if (obj == null) 
      return false; 

     MyType rhs = obj as MyType; 
     if ((Object)rhs == null) 
      return false; 

     return this == rhs; 
    } 

    public static bool operator ==(MyType lhs, MyType rhs) 
    { 
     bool result; 

     if ((Object)lhs != null && (Object)rhs != null) 
      result = lhs.Value == rhs.Value; 
     else 
      result = (Object)lhs == (Object)rhs; 

     return result; 
    } 

    public static bool operator !=(MyType lhs, MyType rhs) 
    { 
     return !(lhs == rhs); 
    } 
} 
+0

¿Ha implementado GetHashCode()? Parece que podría devolver Value.HashCode() – cordialgerm

+0

Está implementando esencialmente un tipo de valor en su tipo de referencia MyClass. No hay nada de malo en eso, pero debe pensar en términos del valor de MyClass como la identidad de la instancia en lugar de que la ubicación del objeto en la memoria sea su identidad (la predeterminada para los tipos de referencia). Por lo tanto, anule GetHashCode() para devolver Value.GetHashCode() para reflejar esa identidad. – dthorpe

+0

@dthorpe - Creo que mi principal problema fue no haber reconocido cómo GetHashCode probablemente se implementaría en Object. Mi pensamiento en ese momento era esencialmente: "No necesito nada sofisticado, la implementación predeterminada debería ser lo suficientemente buena". No cometeré ese error otra vez. La próxima vez que vaya a anular el operador ==, encontraré una forma completamente diferente de arruinarlo. – ThatBlairGuy

Respuesta

8

Debe anular GetHashCode() en su clase. GetHashCode debe implementarse en tándem con sobrecargas iguales. Es común que el código verifique la igualdad del código hash antes de llamar a Equals. Es por eso que su implementación Equals no recibe una llamada.

+0

Creo que esto es incorrecto: de acuerdo con los documentos, Distinct() usa el comparador de igualdad predeterminado. http://msdn.microsoft.com/en-us/library/bb348436.aspx –

+2

Reformulado. El punto es que el cartel original señaló que su implementación Equals no se llama en absoluto. ¿Razón? El sistema que realiza la comparación de igualdad (Comparador de igualdad predeterminado, lo que sea) verifica el código hash antes de llamar a Equals. No ha reemplazado a GetHashCode, por lo que los valores hash nunca serán los mismos, por lo que nunca se llamará a su implementación Equals. Si soluciona el problema de GetHashCode(), probablemente sepa por sí mismo que también tiene errores en su implementación de Equals. – dthorpe

+3

'Distinct' utiliza un' Set 'para realizar un seguimiento de los elementos que se han visto antes, y' Set 'utiliza' GetHashCode() 'internamente. – Gabe

2

Su sospecha es correcta, es la igualdad que en la actualidad se limita a comprobar las referencias a objetos. Incluso su aplicación no hace nada extra, cambiarlo a esto:

public override bool Equals(object obj) 
{ 
    if (obj == null) 
     return false; 

    MyType rhs = obj as MyType; 
    if ((Object)rhs == null) 
     return false; 

    return this.Value == rhs.Value; 
} 
+0

Equals difiere al reemplazado == que compara los valores. –

+0

¿Qué piensas de @Henk? El punto es que esto debe agregarse this.Value == rhs.Value. – Aliostad

+1

Cambiar la última línea para comparar la propiedad Value hace que el filtrado funcione, pero MyType real tiene propiedades múltiples para comparar. ¿Realmente necesito compararlos a todos? Es tentador escribirlo como return (MyType) this == (MyType) rhs, pero eso se dirigiría a la recursión ... – ThatBlairGuy

0

creo MyType necesita implementar IEquatable para que esto funcione.

+2

No, solo una implementación correcta de Equals y GetHashCode –

+1

Lo siento, es malo, acabo de implementar IEtabletable antes de solucionar esto. – cristobalito

2

En ti método de la igualdad todavía están probando para la igualdad de referencia, en lugar de la igualdad semántica, por ejemplo en esta línea:

result = (Object)lhs == (Object)rhs 

que sólo está comparando dos referencias a objetos que, aunque se mantienen exactamente los mismos datos , todavía no son el mismo objeto. En cambio, su prueba de igualdad necesita comparar una o más propiedades de su objeto. Por ejemplo, si el objeto tenía una propiedad ID y objetos con el mismo ID debe considerarse semánticamente equivalentes, entonces se podría hacer esto:

result = lhs.ID == rhs.ID 

Tenga en cuenta que anulando equals() significa que también debe reemplazar GetHashCode() , que es otra olla de pescado, y puede ser bastante difícil de hacer correctamente.

+0

Como otros señalaron, esto no explica por qué nunca se invoca la anulación de Equals de OP, y la solución es anular también GetHashCode(). Esto está plagado de dificultades, así que hice otra sugerencia en una nueva respuesta. –

+0

En este punto, creo que parte del problema es mi implementación de GetHashCode(). En lugar de simplemente devolver base.GetHashCode(), parece que tengo que devolver algo en función de Value. – ThatBlairGuy

1

Debe implementar GetHashCode().

+0

Esto es cierto solo en el sentido de que siempre debe hacerse cuando se anula Igual(), pero por sí solo no resolverá el problema del que pregunta. –

+1

Mikey Cee: Si 'GetHashCode()' no devuelve el mismo valor para dos objetos, 'Equals()' nunca se llamará. – Gabe

+1

Mikey Cee: Pero responde la pregunta del que pregunta por qué su punto crítico en Equals nunca es golpeado. – dthorpe

0

Las otras respuestas han cubierto más o menos el hecho de que es necesario implementar correctamente iguales y GetHashCode, pero como un lado Nota Es posible que le interese saber que los tipos anónimos han estos valores en práctica de forma automática:

var distinct = 
     (from value in list 
     select new {Value = value}).Distinct().ToArray(); 

Entonces, sin tener que definir esta clase, automáticamente obtienes el comportamiento Equals y GetHashCode que estás buscando. Genial, ¿eh?

1

Parece que una simple operación de Distinct puede ser implementado más elegante como sigue:

var distinct = items.GroupBy(x => x.ID).Select(x => x.First()); 

donde ID es la propiedad que determina si dos objetos son semánticamente equivalente. A partir de la confusión aquí (incluida la de mí mismo), la implementación predeterminada de Distinct() parece ser un poco intrincada.

+0

En el código del mundo real, he usado una consulta anidada para filtrar el int. Duplicado. Esto definitivamente ha sido un ejercicio interesante. – ThatBlairGuy

Cuestiones relacionadas