在C#中实现值相等

目录

介绍

背景

编码此混乱

兴趣点


介绍

本文致力于使用各种技术来演示C#中的值相等语义。

背景

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

在引用相等的情况下,通过内存地址比较两个对象。如果两个对象都指向相同的内存地址,则它们是等效的。否则,它们不是。使用引用相等性,不考虑对象保存的数据。两个对象唯一相等的时间是它们是否实际引用同一实例。

通常,我们宁愿使用价值平等。在值相等的情况下,如果两个对象的所有字段都具有相同的数据,则无论它们是否指向相同的内存位置,都将被视为相等。这意味着多个实例可以彼此相等,与引用相等不同。

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

一种方法是在类本身上重载适当的方法。这样做意味着该类将始终使用值语义。这可能不是您想要的,通常,您不仅可能要区分实例,而且值语义还会占用更多资源。通常,这正是您所需要的。用你最好的判断。

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

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

编码此混乱

首先,考虑员工类:

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默认情况下执行一种值相等语义。他们比较每个字段。在字段本身使用引用语义之前,此方法一直有效,因此,如果您需要按值比较这些字段本身,则无论如何会发现自己在结构上实现了值语义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值