C# 除运算符和对象相等外的LINQ

C# 除运算符和对象相等外的LINQ,c#,linq,C#,Linq,在使用Except操作符时,我注意到了一个有趣的问题: 我有要从中排除某些用户的用户列表: 用户列表来自一个XML文件: 代码如下所示: interface IUser { int ID { get; set; } string Name { get; set; } } class User: IUser { #region IUser Members public int ID { get; set;

在使用
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

    }
}
我不明白为什么我需要具体化调用
的对象,除非
是在
IEnumerable
上定义的


如果您有任何建议/见解,我将不胜感激。

我想我知道为什么这样做没有达到预期效果。由于初始用户列表是一个LINQ表达式,因此每次迭代时(在
GetMatchingUsers
中使用一次,在执行
操作时再次使用)都会对其重新求值,因此会创建新的用户对象。这将导致不同的引用,因此没有匹配项。使用
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
==>开始
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
开始
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
开始
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
新用户
{
ID=(int)u.Attribute(“ID”),
名称=(字符串)u.Attribute(“名称”)
}
).of type()//仍然是一个查询,对象尚未具体化
用户。输出用户(用户);
var matches=User.GetMatchingUsers(用户);
用户。输出用户(用户);
var excludes=用户。除了(匹配项);//excludes应包含6个用户,但此处包含8个用户
}
}

我认为您应该实现提供自己的Equals和GetHashCode方法

从MSDN():

如果你想比较 某些自定义数据类型的对象 我们必须实施 IEqualityComparer)通用 类中的接口。以下 代码示例显示了如何实现 此接口为自定义数据类型 并提供GetHashCode和Equals 方法

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>
...
接口IUser:IEquatable
...
在这种情况下,用户类需要提供bool Equals(IUser other)方法


就这些。现在它不需要调用.ToList()方法就可以工作。

CMS:我已经在我的产品代码中实现了IEqualtable,这确实有效。我不明白的是,为什么在调用GetMatchingUser之前显式地在query上调用ToList()会产生预期的效果,而不是将Users变量保留为queryJeff:我不是从GetMatchingUser内部创建的本地列表返回IUSER,该方法从原始IEnumerable返回IUSER,因此,引用仍应指向幕后的原始IUser对象,因此引用相等应按预期工作@阿比吉特:是的,我看到了。因此删除了我的答案。我自己也在仔细观察。我复制了您看到的内容。以下是XML文件的内容,以防万一&l这个答案将隐藏多次运行查询的低效性。如果是这样,那么“新”对象不是每次都会被传递到GetMatchingUsers吗?此外,该方法将查询作为结果而不是对象返回。只是我的2美分…不,因为每次使用表达式时都会对其求值。在我的代码中,它显示了这一点,在调用GetMatchingUsers之前,由我的输出对其进行求值,然后在调用GetMatchingUsers时再次求值,重要的是,在Except期间再次求值。因为GetMatchingUsers和Except的求值都会生成它们自己的实例,Except无法像您期望的那样工作。GetMatchingUsers是否返回查询并不重要,用户可枚举项返回的实例在每次计算中仍然不同。我明白了。。因此,似乎对于所有与集合相关的运算符(扩展方法),底层对象都必须具体化,除非IUser接口的IEquatable实现已作为参数传递?
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);
}
interface IUser : IEquatable<IUser>
...