首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >实现Equals和GetHashCode -一种更简单的方法

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

Stack Overflow用户
提问于 2018-06-16 15:02:05
回答 3查看 249关注 0票数 1

我有一个对象树(DTO),其中一个对象引用其他对象,依此类推:

代码语言:javascript
复制
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
}

这些对象可能非常复杂,并且具有许多其他属性。

在我的应用程序中,具有相同IdPerson可能在两个存储中,应用程序中的本地存储和来自后端的存储。我需要以一种特定的方式合并在线Person和本地Person,因此我需要首先知道在线Person是否与本地存储的Person相同(换句话说,如果本地Person没有被应用程序更新)。

为了使用LINQ的Except,我知道我需要实现Equatable<T>,通常我看到的方式是这样的:

代码语言:javascript
复制
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的更简单、更有效的方法吗

代码语言:javascript
复制
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可以以不同的方式使用。

EN

回答 3

Stack Overflow用户

发布于 2018-06-16 15:11:31

如果两个对象具有相同的属性,但创建时间不同,这将给出错误的否定相等;如果创建的两个对象具有不同的属性,但彼此紧随其后,则会给出错误的否定相等(时钟不是那么准确)。

对于LINQ Except,它实际上是你需要实现的GetHashCode,这应该使用所有属性的哈希码。

理想情况下,它们也应该是不可变的(删除私有setter),这样一个对象在其整个生命周期中都有相同的哈希码。

您的GetHashCode也应该是unchecked

或者,您可以将Except与自定义比较器一起使用。

票数 1
EN

Stack Overflow用户

发布于 2018-06-16 17:08:42

使用值元组(不会为此分配)实现GetHashCode / Equals的真正懒惰版本:

代码语言:javascript
复制
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();
}
票数 1
EN

Stack Overflow用户

发布于 2018-06-16 16:12:44

下面是一个LinqPad草图,你可以从这里开始。它有您可以使用的所有工具来根据您的需求进行定制。当然,这只是一个概念,并不是所有方面都完全阐述。

正如您所看到的,有一个Include属性可以应用于您希望包含在散列中的支持字段。

代码语言:javascript
复制
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

更新版本:

代码语言:javascript
复制
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中调用某些东西(当然仍然有优化的空间),但这些改进是如此之大:

  • 可重用基类而不是部分
  • 感兴趣的字段按类型缓存
  • 散列仅在无效后的第一次请求时重新计算,无效是基于感兴趣的字段的
  • 深度相等检查,而不是仅比较散列

<代码>F214

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50885715

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档