【问题标题】:LINQ Except operator and object equalityLINQ 除了运算符和对象相等
【发布时间】:2009-03-28 19:09:31
【问题描述】:

这是我在使用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 个用户,但“排除”包含所有 8 个用户。

因为我在GetMatchingUsers(IEnumerable&lt;IUser&gt; users) 所做的一切就是使用IEnumerable&lt;IUser&gt; 并返回 IUsers 的 ID 匹配(在这种情况下为 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

鉴于在IEnumerable&lt;T&gt; 上定义,我不明白为什么我需要物化对象来调用Except

任何建议/见解将不胜感激。

【问题讨论】:

    标签: c# linq


    【解决方案1】:

    a) 您需要重写 GetHashCode 函数。 它必须为相等的 IUser 对象返回相等的值。例如:

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

    b) 您需要在实现 IUser 的类中重写 object.Equals(object obj) 函数。

    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() 方法即可工作。

    【讨论】:

      【解决方案2】:

      我想我知道为什么这不能按预期工作。因为初始用户列表是一个 LINQ 表达式,所以每次迭代时都会重新计算它(一次在GetMatchingUsers 中使用,一次在执行Except 操作时),因此,创建了新的用户对象。这将导致不同的引用,因此没有匹配项。使用 ToList 可以解决此问题,因为它只迭代 LINQ 查询一次,因此引用是固定的。

      我已经能够重现您遇到的问题并调查了代码,这似乎是一个非常合理的解释。不过,我还没有证明这一点。

      更新
      我刚刚运行了测试,但在调用GetMatchingUsers 之前、在该调用中以及之后输出了users 集合。每次输出对象的哈希码时,它们确实每次都有不同的值指示新对象,正如我所怀疑的那样。

      这是每个调用的输出:

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

      【讨论】:

      • 如果是这样,那么“新”对象不会每次都传递给 GetMatchingUsers 吗?该方法还返回一个查询作为结果而不是对象。只是我的 2 美分...
      • 否,因为表达式在每次使用时都会被计算。在我的代码中,显示了这一点,它在调用 GetMatchingUsers 之前由我的输出评估,然后在调用 GetMatchingUSers 时再次评估,重要的是,在例外期间再次评估。
      • 因为对 GetMatchingUsers 和 except 的评估都生成了它们自己的实例,所以 except 无法按预期工作。
      • GetMatchingUsers 是否返回查询无关紧要,用户可枚举返回的实例在每次评估时仍然不同。
      • 我明白了.. 所以似乎对于所有与集合相关的运算符(扩展方法),底层对象都必须实现,除非 IUser 接口的 IEquatable 的实现已被传递为参数?
      【解决方案3】:

      我认为你应该实现 IEquatable<T> 来提供你自己的 Equals 和 GetHashCode 方法。

      来自 MSDN (Enumerable.Except):

      如果你想比较序列 一些自定义数据类型的对象,你 必须实施 IEqualityComparer)>) 泛型 类中的接口。以下 代码示例展示了如何实现 自定义数据类型中的此接口 并提供 GetHashCode 和 Equals 方法。

      【讨论】:

      • CMS:我已经在我的生产代码中实现了 IEqualtable 并且确实有效。我不明白的是,为什么在调用 GetMatching Users 之前在查询上显式调用 ToList() 会产生所需的效果,而不是将 users 变量保留为查询
      • Jeff:我没有从我在 GetMatchingUser 中创建的本地列表中返回 IUsers,该方法从原始 IEnumerable 返回 IUsers,因此引用仍应为原始 IUser幕后的对象,所以引用相等应该按预期工作!
      • @Abhijeet:是的,我看到了。因此删除我的答案。我自己仔细看看。我已经复制了你所看到的。
      • 这里是 XML 文件的内容,以防万一.. &l
      • 这个答案会隐藏多次运行查询的低效率。
      猜你喜欢
      • 2017-03-31
      • 2013-01-28
      • 1970-01-01
      • 1970-01-01
      • 2011-10-24
      • 2011-02-07
      • 1970-01-01
      • 1970-01-01
      • 2015-06-05
      相关资源
      最近更新 更多