在 C# 中实现值相等性






4.93/5 (16投票s)
在 C# 类中稳健地实现值相等性语义
引言
本文旨在演示在 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 日 - 首次提交