简体   繁体   English

实现Equals和GetHashCode-一种更简单的方法

[英]Implementing Equals and GetHashCode - an easier way

I have a tree of objects (DTOs), where one object references other objects and so on: 我有一棵对象树(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
}

These objects can be quite complex and have many other properties. 这些对象可能非常复杂,并具有许多其他属性。

In my app, a Person with same Id could be in two storages, local storage in the app and coming from backend. 在我的应用程序,一个Person有相同的Id可以在两个存储,本地存储的应用程序,并从后端的到来。 I need to merge the online Person with local Person in a specific way, so for this I need to first know if the online Person is same with the one stored locally (in other words if local Person hasn't been updated by the app). 我需要以特定的方式将在线Person与本地Person合并,因此,我需要首先知道在线Person是否与本地存储的相同(换句话说,如果应用未更新Local Person )。 。

In order to use LINQ's Except, I know I need to implement Equatable<T> and the usual way I've seen it is like this: 为了使用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;
    }

To me this sounds complicated and hard to maintain, it's easy to forget to update Equals and GetHashCode when properties change. 对我来说,这听起来很复杂且难以维护,当属性更改时,很容易忘记更新EqualsGetHashCode Depending on the objects, it can also be a bit computational expensive. 根据对象,它在计算上也可能会有点昂贵。

Wouldn't the following be a simpler and much effective way of implementing Equals and GethashCode ? 以下内容不是实现EqualsGethashCode的更简单有效的方法吗?

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;
    }
}

My idea is whenever the object changes, there's a timestamp. 我的想法是,只要对象更改,就会有一个时间戳记。 This timestamp gets saved along with the object. 该时间戳记与对象一起保存。 I am thinking to use this field as a concurrency token in storage too. 我也在考虑将此字段用作存储中的并发令牌。

Since resolution of DateTime could be an issue, instead of using time, I'm thinking a Guid is also a good option instead of DateTime. 由于解析DateTime可能是一个问题,因此与其使用时间,不如说我认为Guid也是替代DateTime的好选择。 There wouldn't be too many objects, so the uniqueness of Guid shouldn't be an issue. 不会有太多的对象,因此Guid的唯一性不应该成为问题。

Do you see a problem with this approach? 您认为这种方法有问题吗?

Like I said above, I think it would be much easier to implement and faster to run than having Equals and GetHashCode go over all the properties. 就像我在上面说过的那样,与让Equals和GetHashCode遍历所有属性相比,我认为它更容易实现且运行起来更快。

Update : The more I think about it, I tend to feel that having Equals and GetHashCode implemented on the class is not a good approach. 更新 :我考虑得越多,我倾向于觉得在类上实现EqualsGetHashCode不是一个好方法。 I think it would be better to implement a specialized IEqualityComparer<Person> which compares Person s in a specific way and pass it to LINQ's methods. 我认为最好实现专门的IEqualityComparer<Person> ,以特定的方式比较Person并将其传递给LINQ的方法。

The reason for this is because, like in the comments and answer, a Person could be used in different ways. 这样做的原因是,就像在评论和答案中一样,可以以不同的方式使用Person

This would give you false negative equality if two objects have the same properties but were created at different times, and it would give you false positive equality if two objects were created with different properties but right after each other (the clock is not that accurate). 如果两个对象具有相同的属性但在不同的时间创建,则将给您带来假的负相等;如果两个对象的属性具有不同的属性却又相继创建,则会给您带来假的正等式(时钟不那么精确) 。

For LINQ Except , it's really GetHashCode you need to implement, and this should be using the hash code of all of the properties. 对于LINQ Except ,实际上是您需要实现的GetHashCode ,并且应该使用所有属性的哈希码。

Ideally, they should also be immutable (remove the private setter) so that one object has the same hash code for its whole life. 理想情况下,它们也应该是不可变的(删除私有设置器),以便一个对象在整个生命周期中具有相同的哈希码。

Your GetHashCode should also be unchecked . 您的GetHashCode也应unchecked

Alternatively, you could use Except with a custom comparer. 另外,您可以将Except与自定义比较器一起使用。

Really lazy version for implementing GetHashCode / Equals using value-tuples (which don't allocate for this): 真正的惰性版本,用于使用值元组(不为此分配值)来实现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();
}

Following is a LinqPad sketch, you could start from. 以下是LinqPad草图,您可以从这里开始。 It has all the tools you could use to tailor it to your needs. 它具有您可以用来根据需要定制的所有工具。 Of course, this is just a concept, and not all aspects are completely elaborated. 当然,这仅仅是一个概念,并不是所有方面都得到了详尽阐述。

As you can see, there is an Include attribute that can be applied to the backing fields you want to include in the hash. 如您所见,有一个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);
}

[Update 18.06.17] [更新18.06.17]

Updated version: 更新后的版本:

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; }
}

One still can't get rid of calling something in the setter (and there certainly is still place for optimization), but these are the improvements so har: 仍然不能摆脱在setter中调用某些东西(当然仍然存在进行优化的地方),但是这些改进非常困难:

  • Reusable base class instead of partial 可重用的基类,而不是局部的
  • The fields of interest are cached per type 每种类型都会缓存感兴趣的字段
  • Hash is recalculated only at first request after it was invalidated, and invalidation is cheep 散列仅在无效后才在第一个请求时重新计算,并且无效
  • Deep equality check based on the fields of interest, instead of just comparing the hashes 根据感兴趣的字段进行深度相等检查,而不仅仅是比较散列

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM