为什么 ICollection<>.Contains 会忽略我重写的 Equals 和 IEquatable<> 接口?
Why ICollection<>.Contains ignores my overridden Equals and the IEquatable<> interface?
我在 entity framework 项目中遇到导航 属性 的问题。
这里是 class 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; }
这是class 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>
并且还覆盖了 System.Object
中的 Equals
和 GetHashCode
。
我有以下测试,我预计 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
的 Enumerable.Any
returns 预期的 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
即使我将它投射到
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::原因似乎是两个 HashSet 使用不同的比较器。 entity-framework-HashSet 使用:
System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer
标准 HashSet<>
使用:
GenericEqualityComparer<T>
这就解释了这个问题,尽管我不明白为什么 entity framework 在某些情况下使用忽略自定义 Equals
实现的实现。这是一个令人讨厌的陷阱,不是吗?
结论:如果您不知道将使用什么比较器,请不要使用 Contains
或将 Enumerable.Contains
与采用自定义比较器的重载一起使用:
bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default); // true
从 EF 源代码中,您可能会偶然发现 CreateCollectionCreateDelegate
,这似乎是连接导航属性的一部分。
这调用 EntityUtil.DetermineCollectionType
和 returns 一个 HashSet<T>
作为与 属性 兼容的类型。
然后,使用 HashSet<T>
,它在构造函数中调用 DelegateFactory.GetNewExpressionForCollectionType
which, per the code and the description, handles HashSet<T>
as a special case and passes it an ObjectReferenceEqualityComparer
。
因此:HashSet<T>
EF 为您创建的不是使用您的相等性实现,而是使用引用相等性。
Why ICollection<>.Contains ignores my overridden Equals and the IEquatable<> interface?
因为接口的实现者没有要求这样做。
ICollection<T>.Contains
方法 MSDN documentation 指出:
Determines whether the ICollection<T> contains a specific value.
然后
Remarks
Implementations can vary in how they determine equality of objects; for example, List<T> uses Comparer<T>.Default, whereas Dictionary<TKey, TValue> allows the user to specify the IComparer<T> implementation to use for comparing keys.
旁注:看起来他们把 IComparer<T>
和 IEqualityComparer<T>
搞砸了,但你明白了:)
Conclusion: never use Contains if you don't know what comparer will be used or use Enumerable.Contains with the overload that takes a custom comparer
根据 Enumerable.Contains<T>(IEnumerable<T>, T)
方法重载(即没有自定义比较器)documentation:
Determines whether a sequence contains a specified element by using the default equality comparer.
听起来您的覆盖将被调用。但随后出现以下内容:
Remarks
If the type of source implements ICollection<T>, the Contains method in that implementation is invoked to obtain the result. Otherwise, this method determines whether source contains the specified element.
与初始语句冲突。
真是乱七八糟。我只能说我完全同意这个结论!
我在 entity framework 项目中遇到导航 属性 的问题。
这里是 class 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; }
这是class 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>
并且还覆盖了 System.Object
中的 Equals
和 GetHashCode
。
我有以下测试,我预计 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
的 Enumerable.Any
returns 预期的 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
即使我将它投射到
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::原因似乎是两个 HashSet 使用不同的比较器。 entity-framework-HashSet 使用:
System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer
标准 HashSet<>
使用:
GenericEqualityComparer<T>
这就解释了这个问题,尽管我不明白为什么 entity framework 在某些情况下使用忽略自定义 Equals
实现的实现。这是一个令人讨厌的陷阱,不是吗?
结论:如果您不知道将使用什么比较器,请不要使用 Contains
或将 Enumerable.Contains
与采用自定义比较器的重载一起使用:
bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default); // true
从 EF 源代码中,您可能会偶然发现 CreateCollectionCreateDelegate
,这似乎是连接导航属性的一部分。
这调用 EntityUtil.DetermineCollectionType
和 returns 一个 HashSet<T>
作为与 属性 兼容的类型。
然后,使用 HashSet<T>
,它在构造函数中调用 DelegateFactory.GetNewExpressionForCollectionType
which, per the code and the description, handles HashSet<T>
as a special case and passes it an ObjectReferenceEqualityComparer
。
因此:HashSet<T>
EF 为您创建的不是使用您的相等性实现,而是使用引用相等性。
Why ICollection<>.Contains ignores my overridden Equals and the IEquatable<> interface?
因为接口的实现者没有要求这样做。
ICollection<T>.Contains
方法 MSDN documentation 指出:
Determines whether the ICollection<T> contains a specific value.
然后
Remarks
Implementations can vary in how they determine equality of objects; for example, List<T> uses Comparer<T>.Default, whereas Dictionary<TKey, TValue> allows the user to specify the IComparer<T> implementation to use for comparing keys.
旁注:看起来他们把 IComparer<T>
和 IEqualityComparer<T>
搞砸了,但你明白了:)
Conclusion: never use Contains if you don't know what comparer will be used or use Enumerable.Contains with the overload that takes a custom comparer
根据 Enumerable.Contains<T>(IEnumerable<T>, T)
方法重载(即没有自定义比较器)documentation:
Determines whether a sequence contains a specified element by using the default equality comparer.
听起来您的覆盖将被调用。但随后出现以下内容:
Remarks
If the type of source implements ICollection<T>, the Contains method in that implementation is invoked to obtain the result. Otherwise, this method determines whether source contains the specified element.
与初始语句冲突。
真是乱七八糟。我只能说我完全同意这个结论!