目录
介绍
背景
编码此混乱
兴趣点
本文致力于使用各种技术来演示C#中的值相等语义。
背景引用相等和值相等是确定对象相等的两种不同方法。
在引用相等的情况下,通过内存地址比较两个对象。如果两个对象都指向相同的内存地址,则它们是等效的。否则,它们不是。使用引用相等性,不考虑对象保存的数据。两个对象唯一相等的时间是它们是否实际引用同一实例。
通常,我们宁愿使用价值平等。在值相等的情况下,如果两个对象的所有字段都具有相同的数据,则无论它们是否指向相同的内存位置,都将被视为相等。这意味着多个实例可以彼此相等,与引用相等不同。
.NET提供了一些用于实现值相等语义的工具,具体取决于您打算如何使用它。
一种方法是在类本身上重载适当的方法。这样做意味着该类将始终使用值语义。这可能不是您想要的,通常,您不仅可能要区分实例,而且值语义还会占用更多资源。通常,这正是您所需要的。用你最好的判断。
另一种方法是创建一个实现IEqualityComparer的类。这将允许在Dictionary之类的类中使用值语义来比较类,但是正常的比较将使用引用相等。有时候,这正是你所需要的。
我们将在此处探讨这两种机制。
编码此混乱首先,考虑员工类:
public class Employee
{
public int Id;
public string Name;
public string Title;
public DateTime Birthday;
}
如您所见,这是一个非常简单的类,代表一个员工。默认情况下,类使用引用相等语义,因此为了执行值语义,我们需要做其他工作。
我们可以通过创建一个实现IEqualityComparer的类来使用值语义:
// a class for comparing two employees for equality
// this class is used by the framework in classes like
// Dictionary to do key comparisons.
public class EmployeeEqualityComparer : IEqualityComparer
{
// 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(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
{
public int Id;
public string Name;
public string Title;
public DateTime Birthday;
// implementation of
// IEqualityComparer.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。尽管有这些额外的代码,但基本思想与第一种方法相同。
我们实现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();
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(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();
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默认情况下执行一种值相等语义。他们比较每个字段。在字段本身使用引用语义之前,此方法一直有效,因此,如果您需要按值比较这些字段本身,则无论如何会发现自己在结构上实现了值语义。