2009-03-28 3 views
11

Вот интересный вопрос, который я заметил при использовании Except Оператора: У меня есть список пользователей, из которых я хочу, чтобы исключить некоторые пользователей:LINQ Кроме оператор и объект равенства

Список пользователей приходят из XML файл:

код выглядит следующим образом:

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 

    } 
} 

Когда я звоню User.GetMatchingUsers(users) я получаю 2 матча, как и ожидалось. Проблема в том, что когда я звоню users.Except(matches) Совпадают пользователи не исключаются вообще! Я ожидаю, что 6 пользователей ut «исключает» содержит всего 8 пользователей.

Поскольку все, что я делаю в GetMatchingUsers(IEnumerable<IUser> users) принимает IEnumerable<IUser> и только возвращение IUsers, чей матч удостоверения личности (2 IUsers в данном случае), я понимаю, что по умолчанию Except будет использовать равенство ссылок для сравнения объектов в исключить. Это не так, как ведет себя Except?

Что еще более интересно то, что если я материализовать объекты с помощью .ToList() и затем получить соответствующие пользователей и вызвать Except, все работает, как ожидалось!

Как так:

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 

Я не понимаю, почему я должен нужно материализовать объекты для вызова Except при условии, что его определенная на IEnumerable<T>?

Любые предложения или идеи были бы высоко оценены.

ответ

10

Я думаю, что знаю, почему это не работает должным образом. Поскольку исходный список пользователей является выражением LINQ, он переоценивается каждый раз, когда он итерируется (один раз при использовании в GetMatchingUsers и снова при выполнении операции Except), и поэтому создаются новые пользовательские объекты. Это приведет к разным ссылкам, и поэтому нет совпадений. Использование ToList исправляет это, потому что оно выполняет итерацию только запроса LINQ, и поэтому ссылки исправлены.

Я смог воспроизвести возникшую у вас проблему и изучив этот код, это кажется очень правдоподобным объяснением. Я еще не доказал это.

Update
Я просто побежал испытание, но выводя users коллекции перед вызовом GetMatchingUsers, в этом призыве, и после него. Каждый раз, когда был выведен хэш-код для объекта, и они действительно имеют разные значения каждый раз, указывая на новые объекты, как я подозревал.

Вот выход для каждого из вызовов:

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

И вот модифицированный код, чтобы показать проблему:

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

Если это так, то не будут ли «новые» объекты передаваться в GetMatchingUsers каждый раз? Также этот метод возвращает запрос как результат, а не объекты. Только мои 2 цента ... –

+0

Нет, потому что выражение оценивается каждый раз, когда оно используется. В моем коде, который показывает это, он оценивается моим выходом до вызова GetMatchingUsers, затем снова при вызове GetMatchingUSers и, что еще важнее, снова во время Except. –

+0

Поскольку оценка для GetMatchingUsers и Exception генерирует их собственные экземпляры, Except не работает так, как вы ожидаете. –

2

Я думаю, что вы должны реализовать IEquatable<T> предоставить свои собственные Equals и GetHashCode.

Из MSDN (Enumerable.Except):

Если вы хотите сравнить последовательности объектов некоторого типа данных, вы должны реализовать IEqualityComparer < (Of < (T>)>) общий интерфейс в вашем классе. Следующий пример кода показывает, как реализовать этот интерфейс в пользовательском типе данных и предоставить методы GetHashCode и Equals .

+0

Но код у него должен работать. Почему он не работает? –

+0

CMS: Я реализовал IEqualtable в своем производственном коде и работает. То, что я не понимаю, заключается в том, почему он явно вызывает ToList() в запросе, прежде чем вызвать GetMatching Users производит желаемый эффект вместо того, чтобы оставить переменную пользователя в качестве запроса. –

+0

Jeff: Я не возвращаю IUsers из локального списка. ve, созданный внутри GetMatchingUser. Метод возвращает IUsers из исходного IEnumerable , поэтому ссылки все равно должны быть в исходных объектах IUser за кулисами, поэтому ссылочное равенство должно работать должным образом! –

12

a) Вам необходимо переопределить функцию GetHashCode. Он ДОЛЖЕН возвращать равные значения для равных объектов IUser. Например:

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

б) Вы должны переопределить Object.equals (объект OBJ) функции в классах, реализующих 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); 
} 

с) В качестве альтернативы (б) IUser может наследовать IEquatable:

interface IUser : IEquatable<IUser> 
... 

класса пользователю необходимо будет обеспечить логическое значение Равно другой метод (IUser) в этом случае.

Это все. Теперь он работает без вызова метода .ToList().

Смежные вопросы