2009-03-28 6 views
11

est ici une question intéressante que j'ai remarqué lors de l'utilisation du Except opérateur: Je liste des utilisateurs dont je veux exclure certains utilisateurs:LINQ Sauf opérateur et objet égalité

La liste des utilisateurs provient d'un XML fichier:

Le code va comme ceci:

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 

    } 
} 

Quand j'appelle User.GetMatchingUsers(users) je reçois 2 matchs comme prévu. Le problème est que lorsque j'appelle users.Except(matches) Les utilisateurs correspondants ne sont pas exclus du tout! Je m'attends à 6 utilisateurs ut "excludes" contient tous les 8 utilisateurs à la place.

Depuis tout ce que je fais dans GetMatchingUsers(IEnumerable<IUser> users) prend les IEnumerable<IUser> et juste retour le IUsers dont le match ID (2 IUsers dans ce cas), je crois comprendre que, par défaut Except utilisera l'égalité de référence pour comparer les objets être exclu. N'est-ce pas le comportement de Except?

Ce qui est encore plus intéressant est que si je matérialise les objets à l'aide .ToList() puis obtenir les utilisateurs correspondants, et appelle Except, tout fonctionne comme prévu!

Comme si:

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 

Je ne vois pas pourquoi je besoin de matérialiser des objets pour appeler Except étant donné que son défini sur IEnumerable<T>?

Toutes les suggestions/idées seraient très appréciées.

Répondre

10

Je pense que je sais pourquoi cela ne fonctionne pas comme prévu. Étant donné que la liste initiale des utilisateurs est une expression LINQ, elle est réévaluée chaque fois qu'elle est répétée (une fois utilisée dans GetMatchingUsers et à nouveau lors de l'opération Except). De nouveaux objets utilisateur sont créés. Cela conduirait à des références différentes et donc pas de correspondances. L'utilisation de ToList résout ce problème car il itère la requête LINQ une seule fois et les références sont donc corrigées.

J'ai été capable de reproduire le problème que vous avez et d'avoir étudié le code, cela semble être une explication très plausible. Je ne l'ai pas encore prouvé.

Mise à jour
J'ai juste couru le test, mais la sortie de la collection users avant l'appel à GetMatchingUsers, dans cet appel, et après. Chaque fois que le code de hachage de l'objet était sorti, ils avaient en effet des valeurs différentes indiquant chaque fois de nouveaux objets, comme je le soupçonnais.

Voici la sortie pour chacun des appels:

==> 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 

Et, voici le code modifié pour montrer le problème:

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 tel est le cas, alors les "nouveaux" objets ne seront-ils pas passés dans GetMatchingUsers à chaque fois? Cette méthode renvoie également une requête en tant que résultat et non des objets. Juste mes 2 cents ... –

+0

Non, car l'expression est évaluée chaque fois qu'elle est utilisée. Dans mon code, qui montre cela, il est évalué par ma sortie avant l'appel à GetMatchingUsers, puis à nouveau lors de l'appel GetMatchingUSers, et surtout, encore une fois pendant l'Except. –

+0

Étant donné que l'évaluation de GetMatchingUsers et l'exception génèrent leurs propres instances, l'exception ne fonctionne pas comme prévu. –

2

Je pense que vous devriez mettre en œuvre IEquatable<T> fournir votre propre Equals et méthodes GetHashCode.

De MSDN (Enumerable.Except):

Si vous voulez comparer les séquences de objets d'un certain type de données personnalisé, vous devez mettre en œuvre le IEqualityComparer < (de < (T>)>) générique interface dans votre classe. L'exemple de code suivant montre comment implémenter cette interface dans un type de données personnalisé et fournir des méthodes GetHashCode et Equals . A) Vous devez remplacer la fonction GetHashCode.

+0

Mais le code qu'il a devrait fonctionner. Pourquoi ça ne marche pas? –

+0

CMS: J'ai implémenté IEqualtable dans mon code de production et cela fonctionne. Qu'est-ce que je ne comprends pas, c'est pourquoi appeler explicitement ToList() sur la requête avant d'appeler GetMatching Users produit l'effet désiré au lieu de laisser la variable users comme requête –

+0

Jeff: Je ne retourne pas les IUsers de la liste locale I ' La méthode retourne IUsers à partir de l'original IEnumerable , donc les références doivent toujours être aux objets IUser d'origine derrière les scènes afin que l'égalité de référence ait fonctionné comme prévu! –

12

a) Vous devez remplacer la fonction GetHashCode. Il DOIT retourner des valeurs égales pour des objets IUser égaux. Par exemple:

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

b) Vous devez remplacer la fonction Object.equals (obj d'objet) dans les classes qui mettent en œuvre 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) Comme alternative à (b) IUser peut hériter IEquatable:

interface IUser : IEquatable<IUser> 
... 

classe utilisateur devra fournir la méthode bool Equals (IUser autre) dans ce cas.

C'est tout. Maintenant cela fonctionne sans appeler la méthode .ToList().