【问题标题】:Implementing Equals and GetHashCode - an easier way实现 Equals 和 GetHashCode - 更简单的方法
【发布时间】:2018-06-16 07:02:04
【问题描述】:

我有一棵对象树 (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
}

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

在我的应用中,具有相同 IdPerson 可能位于两个存储中,即应用中的本地存储和来自后端的存储。我需要以特定方式将在线Person 与本地Person 合并,所以为此我需要首先知道在线Person 是否与本地存储的相同(换句话说,如果本地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&lt;Person&gt;,它以特定的方式比较Persons,并将其传递给LINQ的方法。

这样做的原因是,就像在 cmets 和 answer 中一样,Person 可以以不同的方式使用。

【问题讨论】:

  • GUID 或时间戳对您拥有的属性及其值一无所知。您已经拥有的 Id 字段也没有。如果你能保证它们是独一无二的,那它们会比这更好吗?
  • Person 具有属性Id。这还不足以区分不同的实例吗?为什么该类的多个实例具有相同的Id? -- 如果您想跟踪实例数据的变化,那么还有其他方法可以做到这一点。例如使用property change
  • 另一方面,您可以拥有一个仅在实体更新时计算的私有哈希字段,因此您无需每次都重新计算。
  • IEquatable 分两步工作。 1)为每个项目创建一个哈希,并为哈希值创建一个二叉树。 2)然后当两个值具有相等的哈希值时,它使用 Equal 方法进行第二次比较。您可以让哈希对所有值都返回零,然后将 Equal 方法作为唯一的比较标准。您可以在返回 ID 时进行哈希处理,然后让 Equal 检查地址。
  • @DonBox - 所以你有一个类的两个实例(希望是相同的Id),你想决定哪一个“更好”(更新)。 ——嗯,这不是那么容易,不是吗?想象一个只有NameSurname 属性的Person。在时间点 A,server 和 app 是相等的。在 B 点,应用程序(仅)更改了名称。在 C 点服务器更改(仅)姓氏。在 D 点,您要合并。服务器实例是否比应用程序实例“更好”?您想丢失任何一项更改吗?

标签: c# linq gethashcode iequatable


【解决方案1】:

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

对于LINQExcept,你需要实现的是真正的GetHashCode,这应该是使用所有属性的哈希码。

理想情况下,它们也应该是不可变的(删除私有设置器),以便一个对象在其整个生命周期内都具有相同的哈希码。

您的GetHashCode 也应该是unchecked

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

【讨论】:

  • 我认为“平等”可以根据您需要的平等类型而有所不同。如果您从一般 \ abstract 的角度来看,那么是的,您是对的,如果两个 Person 对象是在不同时间创建的,但它们具有相同的属性,它们会有所不同。但这在我的情况下是不可能的,因为UpdatedAt 的工作方式请看一下这个问题,我将其更新为更加清晰和准确。
  • 越想越觉得在类上实现 Equals 和 GetHashCode 并不是一个好办法。我觉得实现 IEqualityComparer&lt;Person&gt; 并将其传递给 LINQ 的方法要好得多。不确定...
  • 如何定义相等决定了你的实现应该是什么。如果您不关心属性值,只关心是否已更新,那么不必在每次更新对象时都更新 GUID,您可以将其设为不可变并使用默认的引用相等性。如果您确实关心属性值,则应仅使用它们来比较相等性。大多数时候Equals 可以使用GetHashCode,如果你真的想偷懒的话。
  • 关于不变性的好点,但它在我的场景中不起作用。我提到我有一个功能,我需要合并存储在不同存储中的两组 Person 对象。另外,我已经在问题中提到我不想使用属性的原因是因为它可能会变得计算量很大,因为 Person 具有复杂的属性,而这些属性又具有复杂的属性等等。我已经提过,请在我更新后再次阅读我的问题。
  • 感谢您的帮助。同样,如果您以抽象的方式看待问题,而不知道实现实际在做什么,那么您是正确的。但请阅读问题
【解决方案2】:

使用值元组(不为此分配)实现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();
}

【讨论】:

  • “不为此分配”是什么意思?
  • @DonBox 我只是说“值元组”代码不涉及任何对象分配,因此开销很小
  • 感谢您的回答。我已经提到我不是想知道如何实现 IEquatable,而是如何以特定方式实现。
【解决方案3】:

下面是一个 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 中调用某些东西(当然还有优化的地方),但这些改进非常困难:

  • 可重用基类而不是部分基类
  • 感兴趣的字段按类型缓存
  • 哈希失效后,只有在第一次请求时才会重新计算,而且失效很便宜
  • 基于感兴趣的字段进行深度相等检查,而不仅仅是比较哈希值

【讨论】:

  • ... 然后每次修改属性时都要支付反射成本(假设您记得将 UpdateHash 调用添加到 setter 中)。根据使用情况,这可能会更糟。如果有的话,为每种类型构建和缓存一个委托/表达式树,并在 CalculateHashFromFields 方法中使用它......但我仍然不知道我是否真的喜欢这种方法。该代码可以在自定义、通用、相等比较器中。然后很容易缓存每个类型的委托,仍然查询您的属性,并且只在必要时调用(当您需要相等检查时,而不是总是)。
  • 是的。我从未说过这是一个完整的解决方案。缓存部分反射是一种可能的改进。另一方面,“成本”是相对的:如果实例的更新频率远低于比较,您仍然可以通过这种方式获得。可能还有另一种可能性,以某种方式拦截更新实例完成的位置,然后才计算哈希。
猜你喜欢
  • 1970-01-01
  • 2012-01-26
  • 1970-01-01
  • 1970-01-01
  • 2021-04-18
  • 2018-03-03
  • 2012-03-08
  • 2016-11-28
相关资源
最近更新 更多