65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中实现值相等性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (16投票s)

2019 年 11 月 18 日

MIT

3分钟阅读

viewsIcon

15879

downloadIcon

219

在 C# 类中稳健地实现值相等性语义

screenshot

引言

本文旨在演示在 C# 中使用各种技术实现值相等性语义。

背景

引用相等性和值相等性是确定对象相等性的两种不同方法。

对于引用相等性,通过内存地址比较两个对象。如果两个对象指向相同的内存地址,则它们是等效的。否则,它们不是。使用引用相等性时,不考虑对象保存的数据。只有当两个对象实际引用同一个实例时,它们才相等。

通常,我们更喜欢使用值相等性。对于值相等性,如果两个对象的所有字段都具有相同的数据,则认为它们相等,无论它们是否指向相同的内存位置。这意味着多个实例可以彼此相等,这与引用相等性不同。

.NET 提供了一些工具来实现值相等性语义,具体取决于您打算如何使用它。

一种方法是重载类本身上的适当方法。这样做意味着该类将始终使用值语义。这可能不是您想要的,因为通常您不仅可能希望区分实例,而且值语义也更消耗资源。然而,通常情况下,这正是您所需要的。使用您最好的判断力。

另一种方法是创建一个实现 IEqualityComparer<T> 的类。这将允许您的类在使用像 Dictionary<TKey,TValue> 这样的类时使用值语义进行比较,但正常的比较将使用引用相等性。有时,这正是您所需要的。

我们将在这里探讨这两种机制。

编写这个混乱的程序

首先,考虑 employee 类

public class Employee
{
    public int Id;
    public string Name;
    public string Title;
    public DateTime Birthday;
}

正如您所看到的,这是一个非常简单的类,代表一个员工。默认情况下,类使用引用相等性语义,因此为了实现值语义,我们需要做额外的工作。

我们可以通过创建一个实现 IEqualityComparer<T> 的类,对该类或任何类使用值语义。

// a class for comparing two employees for equality
// this class is used by the framework in classes like
// Dictionary<TKey,TValue> to do key comparisons.
public class EmployeeEqualityComparer : IEqualityComparer<Employee>
{
    // static singleton field
    public static readonly EmployeeEqualityComparer Default = new EmployeeEqualityComparer();
    // compare two employee instances for equality
    public bool Equals(Employee lhs,Employee rhs)
    {
        // always check this first to avoid unnecessary work
        if (ReferenceEquals(lhs, rhs)) return true;
        // short circuit for nulls
        if (ReferenceEquals(lhs, null) || ReferenceEquals(rhs, null))
            return false;
        // compare each of the fields
        return lhs.Id == rhs.Id &&
            0 == string.Compare(lhs.Name, rhs.Name) &&
            0 == string.Compare(lhs.Title, rhs.Title) &&
            lhs.Birthday == rhs.Birthday;
    }
    // gets the hashcode for the employee
    // this value must be the same as long
    // as the fields are the same.
    public int GetHashCode(Employee lhs)
    {
        // short circuit for null
        if (null == lhs) return 0;
        // get the hashcode for each field
        // taking care to check for nulls
        // we XOR the hashcodes for the 
        // result
        var result = lhs.Id.GetHashCode();
        if (null != lhs.Name)
            result ^= lhs.Name.GetHashCode();
        if (null != lhs.Title)
            result ^= lhs.Title.GetHashCode();
        result ^= lhs.Birthday.GetHashCode();
        return result;
    }
}

完成之后,您可以将此类传递给,例如,一个字典

var d = new Dictionary<Employee, int>(EmployeeEqualityComparer.Default);

这样做允许字典使用值语义进行键比较。这意味着键的考虑是基于其字段的值,而不是它们的实例标识/内存位置。请注意,上面我们将 Employee 用作字典键。当我需要在字典中使用集合作为键时,我经常使用相等性比较器类。这是一个合理的应用,因为即使您在特定情况下需要它们,通常也不希望集合具有值语义。

接下来是第二种方法,在类本身上实现值语义

// represents a basic employee
// with value equality 
// semantics
public class Employee2 : 
    // implementing this interface tells the .NET
    // framework classes that we can compare based on 
    // value equality.
    IEquatable<Employee2>
{
    public int Id;
    public string Name;
    public string Title;
    public DateTime Birthday;

    // implementation of 
    // IEqualityComparer<Employee2>.Equals()
    public bool Equals(Employee2 rhs)
    {
        // short circuit if rhs and this
        // refer to the same memory location
        // (reference equality)
        if (ReferenceEquals(rhs, this))
            return true;
        // short circuit for nulls
        if (ReferenceEquals(rhs, null))
            return false;
        // compare each of the fields
        return Id == rhs.Id &&
            0 == string.Compare(Name, rhs.Name) &&
            0 == string.Compare(Title, rhs.Title) &&
            Birthday == rhs.Birthday;
    }
    // basic .NET value equality support
    public override bool Equals(object obj)
        => Equals(obj as Employee2);
    // gets the hashcode based on the value
    // of Employee2. The hashcodes MUST be
    // the same for any Employee2 that
    // equals another Employee2!
    public override int GetHashCode()
    {
        // go through each of the fields,
        // getting the hashcode, taking
        // care to check for null strings
        // we XOR the hashcodes together
        // to get a result
        var result = Id.GetHashCode();
        if (null != Name)
            result ^= Name.GetHashCode();
        if (null != Title)
            result ^= Title.GetHashCode();
        result ^= Birthday.GetHashCode();
        return result;
    }
    // enable == support in C#
    public static bool operator==(Employee2 lhs,Employee2 rhs)
    {
        // short circuit for reference equality
        if (ReferenceEquals(lhs, rhs))
            return true;
        // short circuit for null
        if (ReferenceEquals(lhs, null) || ReferenceEquals(rhs, null))
            return false;
        return lhs.Equals(rhs);
    }
    // enable != support in C#
    public static bool operator !=(Employee2 lhs, Employee2 rhs)
    {
        // essentially the reverse of ==
        if (ReferenceEquals(lhs, rhs))
            return false;
        if (ReferenceEquals(lhs, null) || ReferenceEquals(rhs, null))
            return true;
        return !lhs.Equals(rhs);
    }
}

正如您所看到的,这有点复杂。我们有熟悉的 Equals()GetHashCode() 方法,但我们还有一个 Equals() 重载和两个运算符重载,而且我们实现了 IEquatable<Employee2>。尽管有这些额外的代码,但基本思想与第一种方法相同。

我们几乎以与第一种方法相同的方式实现 Equals(Employee2 rhs)GetHashCode(),但是我们需要重载另一个 Equals() 方法并转发调用。此外,我们为 ==!= 创建了两个运算符重载,复制了引用相等性和 null 检查,然后转发到 Equals()

一旦我们以这种方式实现了一个对象,进行引用相等性比较的唯一方法是使用 ReferenceEquals()。任何其他机制都会给我们值相等性语义,这正是我们想要的。

使用示例可以在演示项目的 Program 类的 Main() 方法中找到

static void Main(string[] args)
{
    // prepare 2 employee instances
    // with the same data
    var e1a = new Employee()
    {
        Id = 1,
        Name = "John Smith",
        Title = "Software Design Engineer in Test",
        Birthday = new DateTime(1981, 11, 19)
    };
    var e1b = new Employee()
    {
        Id = 1,
        Name = "John Smith",
        Title = "Software Design Engineer in Test",
        Birthday = new DateTime(1981, 11, 19)
    };
    // these will return false, since the 2 instances are different
    // this is reference equality:
    Console.WriteLine("e1a.Equals(e1b): {0}", e1a.Equals(e1b));
    Console.WriteLine("e1a==e1b: {0}", e1a==e1b);
    // this will return true since this class is designed
    // to compare the data in the fields:
    Console.WriteLine("EmployeeEqualityComparer.Equals(e1a,e1b): {0}",
        EmployeeEqualityComparer.Default.Equals(e1a, e1b));
    // prepare a dictionary:
    var d1 = new Dictionary<Employee, int>();
    d1.Add(e1a,0);
    // will return true since the dictionary has a key with this instance
    Console.WriteLine("Dictionary.ContainsKey(e1a): {0}", d1.ContainsKey(e1a));
    // will return false since the dictionary has no key with this instance
    Console.WriteLine("Dictionary.ContainsKey(e1b): {0}", d1.ContainsKey(e1b));
    // prepare a dictionary with our custom equality comparer:
    d1 = new Dictionary<Employee, int>(EmployeeEqualityComparer.Default);
    d1.Add(e1a, 0);
    // will return true since the instance is the same
    Console.WriteLine("Dictionary(EC).ContainsKey(e1a): {0}", d1.ContainsKey(e1a));
    // will return true since the fields are the same
    Console.WriteLine("Dictionary(EC).ContainsKey(e1b): {0}", d1.ContainsKey(e1b));

    // prepare 2 Employee2 instances
    // with the same data:
    var e2a = new Employee2()
    {
        Id = 1,
        Name = "John Smith",
        Title = "Software Design Engineer in Test",
        Birthday = new DateTime(1981, 11, 19)
    };
    var e2b = new Employee2()
    {
        Id = 1,
        Name = "John Smith",
        Title = "Software Design Engineer in Test",
        Birthday = new DateTime(1981, 11, 19)
    };
    // these will return true because they are overloaded
    // in Employee2 to compare the fields
    Console.WriteLine("e2a.Equals(e2b): {0}", e2a.Equals(e2b));
    Console.WriteLine("e2a==e2b: {0}", e2a == e2b);
    // prepare a dictionary:
    var d2 = new Dictionary<Employee2, int>();
    d2.Add(e2a, 0);
    // these will return true, since Employee2 implements
    // Equals():
    Console.WriteLine("Dictionary.ContainsKey(e2a): {0}", d2.ContainsKey(e2a));
    Console.WriteLine("Dictionary.ContainsKey(e2b): {0}", d2.ContainsKey(e2b));
}

关注点

Structs 默认执行一种值相等性语义。它们比较每个字段。这在字段本身使用引用语义之前有效,因此如果您需要按值比较这些字段本身,您可能会发现自己仍然需要在结构上实现值语义。

历史

  • 2019 年 11 月 17 日 - 首次提交
© . All rights reserved.