簡體   English   中英

實現Equals和GetHashCode-一種更簡單的方法

[英]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;
    }

對我來說,這聽起來很復雜且難以維護,當屬性更改時,很容易忘記更新EqualsGetHashCode 根據對象,它在計算上也可能會有點昂貴。

以下內容不是實現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;
    }
}

我的想法是,只要對象更改,就會有一個時間戳記。 該時間戳記與對象一起保存。 我也在考慮將此字段用作存儲中的並發令牌。

由於解析DateTime可能是一個問題,因此與其使用時間,不如說我認為Guid也是替代DateTime的好選擇。 不會有太多的對象,因此Guid的唯一性不應該成為問題。

您認為這種方法有問題嗎?

就像我在上面說過的那樣,與讓Equals和GetHashCode遍歷所有屬性相比,我認為它更容易實現且運行起來更快。

更新 :我考慮得越多,我傾向於覺得在類上實現EqualsGetHashCode不是一個好方法。 我認為最好實現專門的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM