2009-03-28 15 views
11

Qui è una questione interessante che ho notato quando si utilizza il Except Operatore: ho lista di utenti con cui voglio escludere alcuni utenti:LINQ operatore Tranne ed oggetto l'uguaglianza

L'elenco degli utenti proviene da un XML File:

Il codice va in questo modo:

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 

    } 
} 

Quando chiamo User.GetMatchingUsers(users) ottengo 2 partite come previsto. Il problema è che quando chiamo users.Except(matches) Gli utenti corrispondenti non vengono affatto esclusi! Mi aspetto 6 utenti "esclus" contiene invece 8 utenti.

Poiché tutti che sto facendo in GetMatchingUsers(IEnumerable<IUser> users) sta prendendo il IEnumerable<IUser> e proprio ritorno il IUsers cui la partita di ID (2 IUsers in questo caso), la mia comprensione è che di default Except userà l'uguaglianza di riferimento per confrontare gli oggetti da essere escluso Non è così che si comporta Except?

Ciò che è ancora più interessante è che se io materializzare l'oggetti utilizzando .ToList() e quindi ottenere gli utenti corrispondenti, e chiamo Except, tutto funziona come previsto!

Come così:

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 

non vedo perché dovrei bisogno di materializzare oggetti per chiamare Except dato che il suo definito sulla IEnumerable<T>?

Qualsiasi suggerimento/approfondimento sarebbe molto apprezzato.

risposta

10

Penso di sapere perché questo non funziona come previsto. Poiché l'elenco di utenti iniziale è un'espressione LINQ, viene rivalutato ogni volta che viene iterato (una volta quando viene utilizzato in) e così, vengono creati nuovi oggetti utente. Ciò porterebbe a riferimenti diversi e quindi nessuna corrispondenza. L'utilizzo di ToList risolve questo perché itera solo una volta la query LINQ e quindi i riferimenti sono corretti.

Sono stato in grado di riprodurre il problema riscontrato e dopo aver esaminato il codice, questa sembra una spiegazione molto plausibile. Non ho ancora provato, però.

Aggiornamento
Ho appena eseguito il test, ma l'output della collezione users prima della chiamata a GetMatchingUsers, in quella chiamata, e dopo di esso. Ogni volta veniva emesso il codice hash per l'oggetto e in effetti ogni volta hanno valori diversi che indicano nuovi oggetti, come sospettavo.

Ecco l'output per ciascuna delle chiamate:

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

Ed, ecco il codice modificato per mostrare il 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

In questo caso, gli oggetti "nuovi" non verranno passati in GetMatchingUsers ogni volta? Anche questo metodo restituisce una query come risultato e non come oggetto. Solo i miei 2 centesimi ... –

+0

No, perché l'espressione viene valutata ogni volta che viene utilizzata. Nel mio codice, che mostra questo, viene valutato dal mio output prima della chiamata a GetMatchingUsers, poi di nuovo quando si chiama GetMatchingUSers, e, soprattutto, di nuovo durante l'eccezione. –

+0

Poiché la valutazione per GetMatchingUsers e Except generano entrambe le proprie istanze, l'eccezione non funziona come previsto. –

2

penso che si dovrebbe implementare IEquatable<T> per fornire il proprio Metodi uguali e GetHashCode.

Da MSDN (Enumerable.Except):

Se si desidera confrontare sequenze di oggetti di un certo tipo di dati personalizzato, è necessario implementare la IEqualityComparer < (Of < (T>)>) generico Interfaccia nella tua classe. Il seguente esempio di codice mostra come implementare l'interfaccia in un tipo di dati personalizzato e fornire i metodi GetHashCode ed Equals .

+0

Ma il codice che ha dovrebbe funzionare. Perché non funziona? –

+0

CMS: Ho implementato IEqualtable nel mio codice di produzione e che funziona. Quello che non riesco a capire è che perché chiama esplicitamente ToList() sulla query PRIMA di chiamare GetMatching Users produce l'effetto desiderato invece di lasciare la variabile user come query –

+0

Jeff: Non sto restituendo i IUser dalla lista locale I ' creato all'interno di GetMatchingUser, Il metodo restituisce IUser dall'Inumerable originale , quindi i riferimenti dovrebbero essere ancora agli oggetti IUser originali dietro le quinte, quindi l'uguaglianza di riferimento avrebbe dovuto funzionare come previsto! –

12

a) È necessario eseguire l'override della funzione GetHashCode. DEVE restituire valori uguali per oggetti uguali IUser. Per esempio:

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

b) È necessario eseguire l'override funzione Object.equalsQ (oggetto obj) in classi che implementano 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) In alternativa al (b) IUSER può ereditare IEquatable:

interface IUser : IEquatable<IUser> 
... 

classe utente dovrà fornire bool Equals metodo (IUSER altro) in questo caso.

Questo è tutto. Ora funziona senza chiamare il metodo .ToList().

Problemi correlati