[英]Implementing Equals and GetHashCode - an easier way
我有一棵对象树(DTO),其中一个对象引用了其他对象,依此类推:
class Person
{
public int Id { get; }
public Address Address { get; }
// Several other properties
}
public Address
{
public int Id { get; }
public Location Location { get; }
// Several other properties
}
这些对象可能非常复杂,并具有许多其他属性。
在我的应用程序,一个Person
有相同的Id
可以在两个存储,本地存储的应用程序,并从后端的到来。 我需要以特定的方式将在线Person
与本地Person
合并,因此,我需要首先知道在线Person
是否与本地存储的相同(换句话说,如果应用未更新Local Person
)。 。
为了使用LINQ的Except,我知道我需要实现Equatable<T>
,而我看到的通常方式是这样的:
class Person : IEquatable<Person>
{
public int Id { get; }
public Address Address { get; }
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
public bool Equals(Person other)
{
return other != null &&
Id == other.Id &&
Address.Equals(other.Address);
}
public override int GetHashCode()
{
var hashCode = -306707981;
hashCode = hashCode * -1521134295 + Id.GetHashCode();
hashCode = hashCode * -1521134295 + (Address != null ? Address.GetHashCode() : 0);
return hashCode;
}
对我来说,这听起来很复杂且难以维护,当属性更改时,很容易忘记更新Equals
和GetHashCode
。 根据对象,它在计算上也可能会有点昂贵。
以下内容不是实现Equals
和GethashCode
的更简单有效的方法吗?
class Person : IEquatable<Person>
{
public int Id { get; }
public Address Address { get; private set; }
public DateTime UpdatedAt { get; private set; }
public void SetAdress(Address address)
{
Address = address;
UpdatedAt = DateTime.Now;
}
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
public bool Equals(Person other)
{
return other != null &&
Id == other.Id &&
UpdatedAt.Ticks == other.UpdatedAt.Ticks;
}
public override int GetHashCode()
{
var hashCode = -306707981;
hashCode = hashCode * -1521134295 + Id.GetHashCode();
hashCode = hashCode * -1521134295 + UpdatedAt.Ticks.GetHashCode();
return hashCode;
}
}
我的想法是,只要对象更改,就会有一个时间戳记。 该时间戳记与对象一起保存。 我也在考虑将此字段用作存储中的并发令牌。
由于解析DateTime可能是一个问题,因此与其使用时间,不如说我认为Guid也是替代DateTime的好选择。 不会有太多的对象,因此Guid的唯一性不应该成为问题。
您认为这种方法有问题吗?
就像我在上面说过的那样,与让Equals和GetHashCode遍历所有属性相比,我认为它更容易实现且运行起来更快。
更新 :我考虑得越多,我倾向于觉得在类上实现Equals
和GetHashCode
不是一个好方法。 我认为最好实现专门的IEqualityComparer<Person>
,以特定的方式比较Person
并将其传递给LINQ的方法。
这样做的原因是,就像在评论和答案中一样,可以以不同的方式使用Person
。
如果两个对象具有相同的属性但在不同的时间创建,则将给您带来假的负相等;如果两个对象的属性具有不同的属性却又相继创建,则会给您带来假的正等式(时钟不那么精确) 。
对于LINQ Except
,实际上是您需要实现的GetHashCode
,并且应该使用所有属性的哈希码。
理想情况下,它们也应该是不可变的(删除私有设置器),以便一个对象在整个生命周期中具有相同的哈希码。
您的GetHashCode
也应unchecked
。
另外,您可以将Except
与自定义比较器一起使用。
真正的惰性版本,用于使用值元组(不为此分配值)来实现GetHashCode
/ Equals
:
class Person : IEquatable<Person>
{
public int Id { get; }
public Address Address { get; }
public Person(int id, Address address) => (Id, Address) = (id, address);
public override bool Equals(object obj) => Equals(obj as Person);
public bool Equals(Person other) => other != null
&& (Id, Address).Equals((other.Id,other.Address));
public override int GetHashCode() => (Id, Address).GetHashCode();
}
以下是LinqPad草图,您可以从这里开始。 它具有您可以用来根据需要定制的所有工具。 当然,这仅仅是一个概念,并不是所有方面都得到了详尽阐述。
如您所见,有一个Include
属性可以应用于您要包括在哈希中的后备字段。
void Main()
{
var o1 = new C { Interesting = "Whatever", NotSoInterresting = "Blah.." };
var o2 = new C { Interesting = "Whatever", NotSoInterresting = "Blah-blah.." };
(o1 == o2).Dump("o1 == o2"); // False
(o2 == o1).Dump("o2 == o1"); // False
var o3 = o1.Clone();
(o3 == o1).Dump("o3 == o1"); // True
(object.ReferenceEquals(o1, o3)).Dump("R(o3) == R(o2)"); // False
o3.NotSoInterresting = "Changed!";
(o1 == o3).Dump("o1 == C(o3)"); // True
o3.Interesting = "Changed!";
(o1 == o3).Dump("o1 == C(o3)"); // False
}
[AttributeUsage(AttributeTargets.Field)]
public class IncludeAttribute : Attribute { }
public static class ObjectExtensions
{
public static int GetHash(this object obj) => obj?.GetHashCode() ?? 1;
public static int CalculateHashFromFields(this object obj)
{
var fields = obj.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly /*or not*/)
.Where(f => f.CustomAttributes.Any(x => x.AttributeType.Equals(typeof(IncludeAttribute))));
var result = 1;
unchecked
{
foreach(var f in fields) result *= f.GetValue(obj).GetHash();
}
return result;
}
}
public partial class C
{
[Include]
private int id;
public int Id { get => id; private set { id = value; UpdateHash(); } }
[Include]
private string interesting;
public string Interesting { get => interesting; set { interesting = value; UpdateHash(); } }
public string NotSoInterresting { get; set; }
}
public partial class C: IEquatable<C>
{
public C Clone() => new C { Id = this.Id, Interesting = this.Interesting, NotSoInterresting = this.NotSoInterresting };
private static int _id = 1; // Some persistence is required instead
public C()
{
Id = _id++;
}
private int hash;
private void UpdateHash() => hash = this.CalculateHashFromFields();
public override bool Equals(object obj)
{
return Equals(obj as C);
}
public bool Equals(C other) => this.hash == other.hash;
public override int GetHashCode() => hash;
public static bool operator ==(C obj1, C obj2) => obj1.Equals(obj2);
public static bool operator !=(C obj1, C obj2) => !obj1.Equals(obj2);
}
[更新18.06.17]
更新后的版本:
void Main()
{
var o1 = new C { Interesting = "Whatever", NotSoInterresting = "Blah.." };
var o2 = new C { Interesting = "Whatever", NotSoInterresting = "Blah-blah.." };
(o1 == o2).Dump("o1 == o2"); // False
(o2 == o1).Dump("o2 == o1"); // False
var o3 = o1.Clone();
(o3 == o1).Dump("o3 == o1"); // True
(object.ReferenceEquals(o1, o3)).Dump("R(o3) == R(o2)"); // False
o3.NotSoInterresting = "Changed!";
(o1 == o3).Dump("o1 == C(o3)"); // True
o3.Interesting = "Changed!";
(o1 == o3).Dump("o1 == C(o3)"); // False
C o4 = null;
(null == o4).Dump("o4 == null"); // True
}
[AttributeUsage(AttributeTargets.Field)]
public class IncludeAttribute : Attribute { }
public static class ObjectExtensions
{
public static int GetHash(this object obj) => obj?.GetHashCode() ?? 1;
}
public abstract class EquatableBase : IEquatable<EquatableBase>
{
private static FieldInfo[] fields = null;
private void PrepareFields()
{
fields = this.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly /*or not*/)
.Where(f => f.CustomAttributes.Any(x => x.AttributeType.Equals(typeof(IncludeAttribute))))
.ToArray();
}
private int CalculateHashFromProperties()
{
if (fields == null) PrepareFields();
var result = 1;
unchecked
{
foreach (var f in fields) result ^= f.GetValue(this).GetHash();
}
return result;
}
private bool CheckDeepEqualityTo(EquatableBase other)
{
if (ReferenceEquals(other, null) || other.GetType() != GetType()) return false;
if (fields == null) PrepareFields();
var result = true;
for(int i = 0; i < fields.Length && result; i++)
{
var field = fields[i];
result &= field.GetValue(this).Equals(field.GetValue(other));
}
return result;
}
private int hash;
protected int UpdateHash() => hash = this.CalculateHashFromProperties();
protected void InvalidateHash() => hash = 0;
public override bool Equals(object obj) => Equals(obj as EquatableBase);
public bool Equals(EquatableBase other) => object.ReferenceEquals(this, other) || this.CheckDeepEqualityTo(other);
public override int GetHashCode() => hash == 0 ? UpdateHash() : hash;
public static bool operator ==(EquatableBase obj1, EquatableBase obj2) => ReferenceEquals(obj1, obj2) || obj1?.CheckDeepEqualityTo(obj2) == true;
public static bool operator !=(EquatableBase obj1, EquatableBase obj2) => !(obj1 == obj2);
}
public partial class C: EquatableBase
{
private static int _id = 1; // Some persistence is required instead
public C()
{
Id = _id++;
}
public C Clone() => new C { Id = this.Id, Interesting = this.Interesting, NotSoInterresting = this.NotSoInterresting };
[Include]
private int id;
public int Id { get => id; private set { id = value; InvalidateHash(); } }
[Include]
private string interesting;
public string Interesting { get => interesting; set { interesting = value; InvalidateHash(); } }
public string NotSoInterresting { get; set; }
}
仍然不能摆脱在setter中调用某些东西(当然仍然存在进行优化的地方),但是这些改进非常困难:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.