重载Entity Framework实体的Equals和GetHashCode的最佳实践

3

我想要检查两个实体之间的one-to-many关系是否相等。

因此,我重写了Object.Equals方法,但是随后收到了编译器警告CS0659:“‘class’覆盖了Object.Equals(object o),但没有覆盖Object.GetHashCode()。”

我已经重写了Object.GetHashCode,但是Resharper告诉我GetHashCode方法应该为所有对象生命周期返回相同的结果,并且将在可变对象中使用。(文档

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

我不知道应该选择哪一个:

  • 在不覆盖GetHashCode的情况下重写Equals方法, 还是
  • 使用不可变数据覆盖GetHashCode?

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

6
Entity Framework使用自己的智能方法来检测对象的相等性。例如,如果您调用SaveChanges,则会使用获取对象的值与更新对象的值进行匹配,以检测是否需要SQL更新。
我不确定您对相等性的定义是否会影响此相等性检查,导致某些未更改的项在数据库中被更新,甚至更糟的是,有些更改后的数据没有被更新到数据库中。
注意,您的实体类(即放置在DbSet <...>中的类)代表数据库中的表和表之间的关系。
从数据库提取的两个项目应何时视为代表同一对象?是当它们具有相同的值时吗?在一个数据库中,我们不能有两个名为“John Doe”,出生于7月4日的人吗?
你唯一能够用来检测从数据库提取的两个Persons是否代表同一个Person的方法是通过检查Id。一些非主键值不同只能告诉您已更改的数据未更新到数据库,并不能说明这是不同的Person。
我的建议是尽可能让您的表格表示简单:只包含表的列(非虚拟属性)和表之间的关系(虚拟属性)。没有成员,没有方法,什么都没有。
如果您需要额外的功能,请创建该类的扩展函数。如果您需要非标准的相等比较方法,请创建单独的相等比较器。该类的用户可以决定是否要使用默认比较方法或您的特殊比较方法。
这与各种String Comparers非常类似:StringComparer.OrdinalIgnorCase,StringComparer.InvariantCulture等。
回到您的问题,我觉得您想要一个Gpu比较器,它不检查Id的值:具有不同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 和类型之外,我在您想要进行等式检查的方式中看到了一些其他问题:
  • 您的 Gpus 集合可能为空。您必须解决这个问题,以便它不会引发异常。具有 null Gpus 的计算机是否等于具有零 Gpus 的计算机?
  • 显然,Gpus 的顺序对您来说并不重要: [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);
    }
}

如果你要使用大量的GPU,请考虑使用更智能的GetHashCode。


1
重写实体的Equals方法确实会影响EF的更改跟踪。我赞同您的建议,为每个用例实现一个特定的IEqualityComparer<T> - Georg Patscheider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接