【问题标题】:Best practices for Entity Framework entities override Equals and GetHashCodeEntity Framework 实体的最佳实践覆盖 Equals 和 GetHashCode
【发布时间】:2018-12-30 13:38:10
【问题描述】:

我想检查两个具有one-to-many 关系的实体之间的相等性。

显然我覆盖了Object.Equals 方法,但随后我收到CS0659 编译器警告:'class' overrides Object.Equals(object o) but does not override Object.GetHashCode()

我覆盖了Object.GetHashCode,但后来,Resharper 告诉我GetHashCode 方法应该在所有对象生命周期中返回相同的结果,并将用于可变对象。 (docs)

public class Computer
{
    public long Id { get; set; }
    public ICollection<GPU> GPUs { get; set; } = new List<GPU>();

    public override bool Equals(object obj)
    {
        return obj is Computer computer &&
               GPUs.All(computer.GPUs.Contains);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(GPUs);
    }
}

public class GPU
{
    public long Id { get; set; }
    public int? Cores { get; set; } = null;

    public override bool Equals(object obj)
    {
        return obj is GPU gpu &&
               Cores == gpu.Cores;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Cores);
    }
}

我不知道我应该更喜欢什么:

  • 覆盖Equals 方法而不覆盖GetHashCode,或
  • 用不可变数据覆盖GetHashCode

【问题讨论】:

  • 在您的示例中,您在两个类中都没有任何不可变字段,因此您将无法正确计算哈希码(您无法确保哈希码不会改变,而该对象包含在依赖于其哈希码的集合中)。也许比较这两个对象的最好方法是将其序列化为字符串并比较输出?
  • Entity Framework 将 LINQ 转换为 SQL,完全绕过 Equals 和 GetHashCode。这些函数仅在内存比较中起作用,即您从数据库中提取数据之后。但是看到您的努力,我认为您甚至应该避免存储“相等”的对象。存储相同的计算机/GPU 有什么意义?

标签: c# entity-framework object-equality


【解决方案1】:

Entity Framework 使用自己的智能方法来检测对象是否相等。例如,如果您调用 SaveChanges,则会使用此方法:获取对象的值与更新对象的值相匹配,以检测是否需要 SQL 更新。

我不确定你的相等定义是否会与这种相等检查混淆,导致数据库中一些未更改的项目被更新,或者更糟糕的是,一些更改的数据不会在数据库中更新。

数据库相等

请记住,您的实体类(您放在DbSet&lt;...&gt; 中的类)代表数据库中的表以及表之间的关系。

什么时候从你的数据库中提取的两个项目被认为代表同一个对象?是不是当它们具有相同的值时?我们不能在一个数据库中拥有两个名为“John Doe”、出生于 7 月 4 日的人吗?

您可以用来检测从数据库中提取的两个Persons 代表相同Person 的唯一方法是检查Id。一些非主键值不同的事实只是告诉您更改的数据没有在数据库中更新,而不是它是不同的Person

覆盖 Equals 与创建 EqualityComparer

我的建议是,让您的表格表示尽可能简单:只有表格的列(非虚拟属性)和表格之间的关系(虚拟属性)。没有成员,没有方法,什么都没有。

如果您需要额外的功能,请创建类的扩展功能。如果您需要非标准的相等比较方法,请创建一个单独的相等比较器。您的类的用户可以决定是要使用默认比较方法还是您的特殊比较方法。

这都可以与各种字符串比较器相媲美:StringComparer.OrdinalIgnorCaseStringComparer.InvariantCulture 等。

回到你的问题

在我看来,您需要一个不检查 Id 值的 Gpu 比较器:具有不同 Id 的两个项目,但其他属性的相同值被认为是相等的。

class GpuComparer : EqualityComparer<Gpu>
{
    public static IEqualityComparer<Gpu> IgnoreIdComparer {get;} = new GpuComparer()

    public override bool Equals(Gpu x, Gpu y)
    {
        if (x == null) return y == null; // true if both null, false if x null but y not
        if (y == null) return false;     // because x not null
        if (Object.ReferenceEquals(x, y)) return true;
        if (x.GetType() != y.GetType()) return false;

        // if here, we know x and y both not null, and of same type.
        // compare all properties for equality
        return x.Cores == y.Cores;
    }
    public override int GetHasCode(Gpu x)
    {
        if (x == null) throw new ArgumentNullException(nameof(x));

         // note: I want a different Hash for x.Cores == null than x.Cores == 0!

         return (x.Cores.HasValue) ? return x.Cores.Value.GetHashCode() : -78546;
         // -78546 is just a value I expect that is not used often as Cores;
    }
}

请注意,我添加了相同类型的测试,因为如果 y 是 Gpu 的派生类,并且您会忽略它们不是同一类型,那么可能是 Equals(x, y),但不是 Equals(y, x),这是相等函数的先决条件之一

用法:

IEqualityComparer<Gpu> gpuIgnoreIdComparer = GpuComparer.IgnoreIdComparer;
Gpu x = new Gpu {Id = 0, Cores = null}
Gpu y = new Gpu {Id = 1, Cores = null}

bool sameExceptForId = gpuIgnoreIdComparer.Equals(x, y);

x 和 y 将被视为相等

HashSet<Gpu> hashSetIgnoringIds = new HashSet<Gpu>(GpuComparer.IgnoreIdComparer);
hashSetIgnoringIds.Add(x);
bool containsY = hashSetIgnoringIds.Contains(y); // expect true

计算机的比较器将是类似的。除了您忘记检查 null 和类型之外,我还发现您希望进行相等性检查的方式还存在一些其他问题:

  • 可以将 null 分配给您的 Gpus 集合。你必须解决这个问题,它不会引发异常。具有零 Gpus 的计算机是否等于具有零 Gpus 的计算机?
  • 显然 GPU 的顺序对您来说并不重要:[1, 3] 等于 [3, 1]
  • 显然某个GPU出现的次数并不重要:[1,1,3]等于[1,3,3]?

.

class IgnoreIdComputerComparer : EqualityComparer<Computer>
{
    public static IEqualityComparer NoIdComparer {get} = new IgnoreIdComputerCompare();


    public override bool (Computer x, Computer y)
    {
        if (x == null) return y == null;not null
        if (y == null) return false;
        if (Object.ReferenceEquals(x, y)) return true;
        if (x.GetType() != y.GetType())  return false;

        // equal if both GPU collections null or empty,
        // or any element in X.Gpu is also in Y.Gpu ignoring duplicates
        // using the Gpu IgnoreIdComparer
        if (x.Gpus == null || x.Gpus.Count == 0)
            return y.Gpus == null || y.Gpus.Count == 0;

        // equal if same elements, ignoring duplicates:
        HashSet<Gpu> xGpus = new HashSet<Gpu>(x, GpuComparer.IgnoreIdComparer);
        return xGpush.EqualSet(y);
    }

    public override int GetHashCode(Computer x)
    {
        if (x == null) throw new ArgumentNullException(nameof(x));

        if (x.Gpus == null || x.Gpus.Count == 0) return -784120;

         HashSet<Gpu> xGpus = new HashSet<Gpu>(x, GpuComparer.IgnoreIdComparer);
         return xGpus.Sum(gpu => gpu);
    }
}

TODO:如果您将使用大量 Gpus,请考虑使用更智能的 GetHashCode

【讨论】:

  • 在实体上覆盖 Equals 确实会干扰 EF 的更改跟踪。我赞同您的建议,即为每个用例实现一个特定的 IEqualityComparer&lt;T&gt;
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-09
  • 2022-01-11
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 2011-03-02
相关资源
最近更新 更多