使用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<IUser> users)
所做的一切都是IEnumerable<IUser>
并且正在返回
其ID与之匹配的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>
上定义了?
非常感谢任何建议/见解。
答案 0 :(得分:13)
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()方法的情况下工作。
答案 1 :(得分:10)
我想我知道为什么这不能按预期工作。因为初始用户列表是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 :(得分:2)
我认为您应该实现IEquatable<T>来提供自己的Equals和GetHashCode方法。
来自MSDN(Enumerable.Except):
如果你想比较的序列 一些自定义数据类型的对象,你 必须实施 IEqualityComparer&lt;(Of&lt;(T&gt;)&gt;)generic 你班上的界面。下列 代码示例显示了如何实现 自定义数据类型中的此接口 并提供GetHashCode和Equals 方法