2009-03-28 15 views
11

Aquí es un tema interesante que noté cuando se utiliza el operador Except: tengo lista de usuarios a partir del cual quiero excluir a algunos usuarios:LINQ A excepción del operador y el objeto igualdad

La lista de usuarios está viniendo de un XML archivo:

El código dice así:

interface IUser 
{ 
    int ID { get; set; } 
    string Name { get; set; } 
} 

class User: IUser 
{ 

    #region IUser Members 

    public int ID 
    { 
     get; 
     set; 
    } 

    public string Name 
    { 
     get; 
     set; 
    } 

    #endregion 

    public override string ToString() 
    { 
     return ID + ":" +Name; 
    } 


    public static IEnumerable<IUser> GetMatchingUsers(IEnumerable<IUser> users) 
    { 
     IEnumerable<IUser> localList = new List<User> 
     { 
      new User{ ID=4, Name="James"}, 
      new User{ ID=5, Name="Tom"} 

     }.OfType<IUser>(); 
     var matches = from u in users 
         join lu in localList 
          on u.ID equals lu.ID 
         select u; 
     return matches; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     XDocument doc = XDocument.Load("Users.xml"); 
     IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
       { ID = (int)u.Attribute("id"), 
        Name = (string)u.Attribute("name") 
       } 
      ).OfType<IUser>();  //still a query, objects have not been materialized 


     var matches = User.GetMatchingUsers(users); 
     var excludes = users.Except(matches); // excludes should contain 6 users but here it contains 8 users 

    } 
} 

Cuando llamo User.GetMatchingUsers(users) consigo 2 partidos como se esperaba. El problema es que cuando llamo al users.Except(matches) ¡Los usuarios coincidentes no se excluyen del todo! Estoy esperando 6 usuarios ut "excludes" contiene los 8 usuarios en su lugar.

Dado que todo lo que estoy haciendo en la GetMatchingUsers(IEnumerable<IUser> users) está tomando el IEnumerable<IUser> y que acaban de volver la IUsers cuyo partido de Identificación (2 IUsers en este caso), mi entendimiento es que por defecto Except utilizará la igualdad referencia para comparar los objetos de ser excluido. ¿No es así como se comporta Except?

Lo que es aún más interesante es que si materializo los objetos utilizando .ToList() y luego los usuarios que coinciden, y llamo Except, todo funciona como se esperaba!

así:

IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
       { ID = (int)u.Attribute("id"), 
        Name = (string)u.Attribute("name") 
       } 
      ).OfType<IUser>().ToList(); //explicity materializing all objects by calling ToList() 

var matches = User.GetMatchingUsers(users); 
var excludes = users.Except(matches); // excludes now contains 6 users as expected 

no veo por qué debería necesitar para materializar objetos para llamar Except dado que su definen en IEnumerable<T>?

Cualquier sugerencia/idea sería muy apreciada.

Respuesta

10

Creo que sé por qué esto no funciona como se esperaba. Debido a que la lista de usuarios inicial es una expresión LINQ, se vuelve a evaluar cada vez que se itera (una vez cuando se usa en GetMatchingUsers y nuevamente al hacer la operación Except) y así, se crean nuevos objetos de usuario. Esto llevaría a diferentes referencias y, por lo tanto, no hay coincidencias. El uso de ToList corrige esto porque itera la consulta LINQ una sola vez y, por lo tanto, las referencias son correctas.

He podido reproducir el problema que tienes y haber investigado el código, esta parece ser una explicación muy plausible. Aunque no lo he probado todavía.

actualización
que acaba de ejecutar la prueba, pero la salida de la colección users antes de la llamada a GetMatchingUsers, en esa llamada, y después de ella. Cada vez que se emitía el código hash para el objeto, de hecho tienen diferentes valores cada vez que indican nuevos objetos, como sospechaba.

Aquí está la salida para cada una de las llamadas:

==> Start 
ID=1, Name=Jeff, HashCode=39086322 
ID=2, Name=Alastair, HashCode=36181605 
ID=3, Name=Anthony, HashCode=28068188 
ID=4, Name=James, HashCode=33163964 
ID=5, Name=Tom, HashCode=14421545 
ID=6, Name=David, HashCode=35567111 
<== End 
==> Start 
ID=1, Name=Jeff, HashCode=65066874 
ID=2, Name=Alastair, HashCode=34160229 
ID=3, Name=Anthony, HashCode=63238509 
ID=4, Name=James, HashCode=11679222 
ID=5, Name=Tom, HashCode=35410979 
ID=6, Name=David, HashCode=57416410 
<== End 
==> Start 
ID=1, Name=Jeff, HashCode=61940669 
ID=2, Name=Alastair, HashCode=15193904 
ID=3, Name=Anthony, HashCode=6303833 
ID=4, Name=James, HashCode=40452378 
ID=5, Name=Tom, HashCode=36009496 
ID=6, Name=David, HashCode=19634871 
<== End 

Y, aquí está el código modificado para mostrar el problema:

using System.Xml.Linq; 
using System.Collections.Generic; 
using System.Linq; 
using System; 

interface IUser 
{ 
    int ID 
    { 
     get; 
     set; 
    } 
    string Name 
    { 
     get; 
     set; 
    } 
} 

class User : IUser 
{ 

    #region IUser Members 

    public int ID 
    { 
     get; 
     set; 
    } 

    public string Name 
    { 
     get; 
     set; 
    } 

    #endregion 

    public override string ToString() 
    { 
     return ID + ":" + Name; 
    } 


    public static IEnumerable<IUser> GetMatchingUsers(IEnumerable<IUser> users) 
    { 
     IEnumerable<IUser> localList = new List<User> 
     { 
      new User{ ID=4, Name="James"}, 
      new User{ ID=5, Name="Tom"} 

     }.OfType<IUser>(); 

     OutputUsers(users); 
     var matches = from u in users 
         join lu in localList 
          on u.ID equals lu.ID 
         select u; 
     return matches; 
    } 

    public static void OutputUsers(IEnumerable<IUser> users) 
    { 
     Console.WriteLine("==> Start"); 
     foreach (IUser user in users) 
     { 
      Console.WriteLine("ID=" + user.ID.ToString() + ", Name=" + user.Name + ", HashCode=" + user.GetHashCode().ToString()); 
     } 
     Console.WriteLine("<== End"); 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     XDocument doc = new XDocument(
      new XElement(
       "Users", 
       new XElement("User", new XAttribute("id", "1"), new XAttribute("name", "Jeff")), 
       new XElement("User", new XAttribute("id", "2"), new XAttribute("name", "Alastair")), 
       new XElement("User", new XAttribute("id", "3"), new XAttribute("name", "Anthony")), 
       new XElement("User", new XAttribute("id", "4"), new XAttribute("name", "James")), 
       new XElement("User", new XAttribute("id", "5"), new XAttribute("name", "Tom")), 
       new XElement("User", new XAttribute("id", "6"), new XAttribute("name", "David")))); 
     IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
      { 
       ID = (int)u.Attribute("id"), 
       Name = (string)u.Attribute("name") 
      } 
      ).OfType<IUser>();  //still a query, objects have not been materialized 


     User.OutputUsers(users); 
     var matches = User.GetMatchingUsers(users); 
     User.OutputUsers(users); 
     var excludes = users.Except(matches); // excludes should contain 6 users but here it contains 8 users 

    } 
} 
+0

Si ese es el caso, ¿no pasarían los objetos "nuevos" a GetMatchingUsers cada vez? Además, ese método devuelve una consulta como resultado y no como objetos. Solo mis 2 centavos ... –

+0

No, porque la expresión se evalúa cada vez que se usa. En mi código, que muestra esto, es evaluado por mi salida antes de la llamada a GetMatchingUsers, luego cuando llamo a GetMatchingUSers, y de manera importante, nuevamente durante el Except. –

+0

Como la evaluación de GetMatchingUsers y Except generan sus propias instancias, Except no funciona como esperaba. –

2

Creo que se debe implementar IEquatable<T> proporcionar su propia Métodos iguales y GetHashCode.

De MSDN (Enumerable.Except):

Si desea comparar las secuencias de objetos de algún tipo de datos personalizado, tiene que implementar el IEqualityComparer < (De < (T>)>) genérica interfaz en su clase. El siguiente ejemplo de código muestra cómo implementar esta interfaz en un tipo de datos personalizado y proporciona métodos GetHashCode e Igual .

+0

Pero el código que tiene debería funcionar. ¿Por qué no está funcionando? –

+0

CMS: Implementé IEqualtable en mi código de producción y eso SI FUNCIONA. Lo que no entiendo es por qué es que llamar explícitamente a ToList() en la consulta ANTES de llamar a GetMatching Users produce el efecto deseado en lugar de dejar la variable de usuarios como consulta –

+0

Jeff: No devolveré los IUsers de la lista local I ' creado dentro de GetMatchingUser, el método devuelve IUsers del IEnumerable original , por lo que las referencias deben seguir siendo los objetos IUser originales detrás de las escenas para que la igualdad de referencia haya funcionado como se esperaba. –

12

a) Debe anular la función GetHashCode. DEBE devolver valores iguales para objetos IUser iguales. Por ejemplo:

public override int GetHashCode() 
{ 
    return ID.GetHashCode()^Name.GetHashCode(); 
} 

b) Es necesario reemplazar la función Object.equals (Object obj) en las clases que implementan IUser.

public override bool Equals(object obj) 
{ 
    IUser other = obj as IUser; 
    if (object.ReferenceEquals(obj, null)) // return false if obj is null OR if obj doesn't implement IUser 
     return false; 
    return (this.ID == other.ID) && (this.Name == other.Name); 
} 

c) Como alternativa a (b) IUser puede heredar IEquatable:

interface IUser : IEquatable<IUser> 
... 

clase de usuario tendrá que proporcionar bool método equals (IUser otro) en ese caso.

Eso es todo. Ahora funciona sin llamar al método .ToList().

Cuestiones relacionadas