为什么ICollection<> .Contains会忽略我重写的Equals和IEquatable<>接口?

时间:2016-08-23 13:30:37

标签: c# entity-framework entity-framework-6 equals c#-6.0

我在实体框架项目中遇到导航属性问题。

这是课程MobileUser

[DataContract]
[Table("MobileUser")]
public class MobileUser: IEquatable<MobileUser>
{
    // constructors omitted....

    /// <summary>
    /// The primary-key of MobileUser.
    /// This is not the VwdId which is stored in a separate column
    /// </summary>
    [DataMember, Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }

    [DataMember, Required, Index(IsUnique = true), MinLength(VwdIdMinLength), MaxLength(VwdIdMaxLength)]
    public string VwdId { get; set; }

    // other properties omitted ...

    [DataMember]
    public virtual ICollection<MobileDeviceInfo> DeviceInfos { get; private set; }

    public bool Equals(MobileUser other)
    {
        return this.UserId == other?.UserId || this.VwdId == other?.VwdId;
    }

    public override bool Equals(object obj)
    {
        if(object.ReferenceEquals(this, obj))return true;
        MobileUser other = obj as MobileUser;
        if (other == null) return false;
        return this.Equals(other);
    }

    public override int GetHashCode()
    {
        // ReSharper disable once NonReadonlyMemberInGetHashCode
        return VwdId.GetHashCode();
    }

    public override string ToString()
    {
        return "foo"; // omitted actual implementation
    }

    #region constants
    // irrelevant
    #endregion
}

相关部分是此导航属性:

public virtual ICollection<MobileDeviceInfo> DeviceInfos { get; private set; }

这是班级MobileDeviceInfo

[DataContract]
[Table("MobileDeviceInfo")]
public class MobileDeviceInfo : IEquatable<MobileDeviceInfo>
{
    [DataContract]
    public enum MobilePlatform
    {
        [EnumMember]
        // ReSharper disable once InconsistentNaming because correct spelling is iOS
        iOS = 1,
        [EnumMember] Android = 2,
        [EnumMember] WindowsPhone = 3,
        [EnumMember] Blackberry = 4
    }

    // constructors omitted ...

    [DataMember, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int DeviceInfoId { get; private set; }

    [DataMember, Required, Index(IsUnique = true), MinLength(DeviceTokenMinLength), MaxLength(DeviceTokenMaxLength)]
    public string DeviceToken { get; set; }

    [DataMember, Required, MinLength(DeviceNameMinLength), MaxLength(DeviceNameMaxLength)]
    public string DeviceName { get; set; }

    [DataMember, Required]
    public MobilePlatform Platform { get; set; }

    // other properties ...

    [DataMember]
    public virtual MobileUser MobileUser { get; private set; }

    /// <summary>
    ///     The foreign-key to the MobileUser.
    ///     This is not the VwdId which is stored in MobileUser
    /// </summary>
    [DataMember, ForeignKey("MobileUser")]
    public int UserId { get; set; }

    public bool Equals(MobileDeviceInfo other)
    {
        if (other == null) return false;
        return DeviceToken == other.DeviceToken;
    }

    public override string ToString()
    {
        return "Bah"; // implementation omitted

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj)) return true;
        MobileDeviceInfo other = obj as MobileDeviceInfo;
        if (other == null) return false;
        return Equals(other);
    }

    public override int GetHashCode()
    {
        // ReSharper disable once NonReadonlyMemberInGetHashCode
        return DeviceToken.GetHashCode();
    }

    #region constants
    // irrelevant
    #endregion
}

如您所见,它实现IEquatable<MobileDeviceInfo>并覆盖Equals中的GetHashCodeSystem.Object

我有以下测试,我预计Contains会调用我的Equals,但事实并非如此。它似乎使用了Object.ReferenceEquals,因此无法找到我的设备,因为它是一个不同的参考:

var userRepo = new MobileUserRepository((ILog)null);
var deviceRepo = new MobileDeviceRepository((ILog)null);

IReadOnlyList<MobileUser> allUser = userRepo.GetAllMobileUsersWithDevices();
MobileUser user = allUser.First();

IReadOnlyList<MobileDeviceInfo> allDevices = deviceRepo.GetMobileDeviceInfos(user.VwdId, true);
MobileDeviceInfo device = allDevices.First();
bool contains = user.DeviceInfos.Contains(device);
bool anyEqual = user.DeviceInfos.Any(x => x.DeviceToken == device.DeviceToken);
Assert.IsTrue(contains); // no, it's false

使用LINQ&#39; Enumerable.Any的第二种方法会返回预期的true

如果我不使用user.DeviceInfos.Contains(device)user.DeviceInfos.ToList().Contains(device)它也可以正常工作,因为List<>.Contains使用了Equals

ICollection<>的实际类型似乎是System.Collections.Generic.HashSet<MobileDeviceInfo>,但如果我使用下面同时使用HashSet<>的代码,它又会按预期运行:

bool contains = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device); // true

那么为什么只比较引用,我的自定义Equals会被忽略?

更新

更令人困惑的是结果是false即使我把它投射到了。{1}} HashSet<MobileDeviceInfo>

 // still false
bool contains2 = ((HashSet<MobileDeviceInfo>)user.DeviceInfos).Contains(device);
// but this is true as already mentioned
bool contains3 = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device); 

更新2::原因似乎是两个HashSets都使用不同的比较器。 entity-framework-HashSet使用:

System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer

,标准HashSet<>使用:

GenericEqualityComparer<T>

这解释了这个问题,虽然我不明白为什么实体框架在某些情况下使用忽略自定义Equals实现的实现。这不是一个令人讨厌的陷阱,不是吗?

结论:如果您不知道将使用什么比较器,请使用Contains或使用带有自定义比较器的重载Enumerable.Contains

bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default);  // true

2 个答案:

答案 0 :(得分:8)

从EF来源,您可能偶然发现CreateCollectionCreateDelegate,这似乎是挂钩导航属性的一部分。

如果与属性兼容,则会调用EntityUtil.DetermineCollectionType并返回HashSet<T>作为类型。

然后,在HashSet<T>的帮助下,它会调用DelegateFactory.GetNewExpressionForCollectionType,根据代码和说明,将HashSet<T>作为一个特例进行处理并将ObjectReferenceEqualityComparer传递给enter image description here在构造函数中。

所以:HashSet<T> EF为你创建的不是使用你的相等实现,而是使用引用相等。

答案 1 :(得分:3)

  

为什么ICollection&lt;&gt; .Contains会忽略我重写的Equals和IEquatable&lt;&gt;接口

因为接口的实现者没有要求这样做。

ICollection<T>.Contains方法MSDN documentation声明:

  

确定ICollection&lt; T&gt;是否为&lt; T&gt;。包含特定值。

然后

  

备注

     

实施可以改变,确定对象是否相等;例如,List&lt; T&gt;使用Comparer&lt; T&gt; .Default,而Dictionary&lt; TKey,TValue&gt;允许用户指定IComparer&lt; T&gt;。用于比较密钥的实现。

旁注:看起来他们与IComparer<T>混淆了IEqualityComparer<T>,但您明白了这一点:)

  

结论:如果您不知道将使用什么比较器,请永远使用Contains或使用带有自定义比较器的重载的Enumerable.Contains

根据Enumerable.Contains<T>(IEnumerable<T>, T)方法重载(即没有自定义比较器)documentation

  

使用默认相等比较器确定序列是否包含指定元素。

听起来像你的覆盖将被调用。但接下来是:

  

<强>说明
  如果源的类型实现ICollection&lt; T&gt;,则调用该实现中的Contains方法以获得结果。否则,此方法确定source是否包含指定的元素。

与初始陈述冲突。

这真的很乱。我只能说完全同意这个结论!