LINQ Кроме выражения оператора и объекта

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

Список пользователей поступает из файла 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, чье совпадение ID (в этом случае два 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>?

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

Ответ 1

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

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

Обновление
Я только что проверил тест, но вывел коллекцию 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

    }
}

Ответ 2

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

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

b) Вы должны переопределить функцию object.Equals(object 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);
}

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

interface IUser : IEquatable<IUser>
...

В этом случае пользовательский класс должен будет предоставить метод bool Equals (IUser other).

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

Ответ 3

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

Из MSDN (Enumerable.Except):

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