装箱类型相等和字典键

Boxing type equality and dictionary keys

当涉及到盒装类型时,我对字典如何比较键有点困惑。

using System;
using System.Collections.Generic;
                
public class Program
{
     public static void Main()
     {
           int i = 5;
           int n = 5;
           
           object boxedI = i;
           object boxedN = n;
           
           Console.WriteLine("i == n ? " + (i == n) ); //true
           Console.WriteLine("bI == bN ? " + (boxedI == boxedN) ); //false
           
           Dictionary<object,int> _dict = new Dictionary<object,int> ();
           _dict.Add(boxedI,5);
           
           Console.WriteLine("_dict contains boxedI? " + _dict.ContainsKey(boxedI) ); //true
           Console.WriteLine("_dict contains boxedN? " + _dict.ContainsKey(boxedN) ); //!! also true, surprise me
           
           _dict.Add(boxedN,5);//exception
     }
}

我预计,由于相等运算符“失败”(据我所知,它基于方法 GetHashCode,字典使用相同的方法来构建它的内部哈希表形式对象),那么字典也应该“失败”盒装 I 和 N 的比较,但事实并非如此。

这里是我用的fiddle:https://dotnetfiddle.net/DW54nN

所以我想问是否有人可以向我解释这里添加了什么以及我的心智模型中缺少什么。

这是引用与值类型的东西:

int i = 5;
int n = 5;

这些是值类型并被放入堆栈,所以当我们比较它们时,我们进入堆栈并且可以说 i 和 n 的值是 5,这使它们“相等”。

object boxedI = i;
object boxedN = n;

当你把这些值放在一个 object 中时,你创建了一个“引用”类型,这意味着一个值被放入堆中,一个引用被放入堆栈中,所以你可以想象在您拥有的堆栈:

#0005 -> boxedI
#0006 -> boxedN

现在当你做 equals 时,你是在比较 #0005 == #0006 哪个不一样

但是当您将 boxedIboxedN 传递给 ContainsKey 时,该方法知道如何跟随引用(或指针)指向堆上的值 (5)。

所以当你要求 ContainsKey(boxedI) 时,你所做的就是要求 ContainsKey(5)(粗略地说)

这就是为什么这两个“相等”的原因

TLDR:使用 == 比较装箱值使用装箱对象的引用相等性,但使用 Equals() 比较装箱值使用基础值'Equals().


当值类型被装箱时,装箱对象的 GetHashCode()Equals() 方法实现调用装箱值的版本。​​

即给定:

  • 正确实现 GetHashCode()Equals() 的值类型 VT
  • VT 的实例 x
  • VT 的实例 y,其值与 x 相同。
  • x 的盒装实例:bx
  • y 的盒装实例:by

情况如下:

x.Equals(y)      == true             // Original values are equal
bx.Equals(by)    == true             // Boxed values are equal
x.GetHashCode()  == y.GetHashCode()  // Original hashes are equal
bx.GetHashCode() == by.GetHashCode() // Boxed hashes are equal
bx.GetHashCode() == x.GetHashCode()  // Original hash code == boxed hash code

然而,== 运算符未被盒装版本 委托,实际上它是使用 reference 实现的平等,所以:

(x == y)   == true  // Original values are equal using "=="
(bx == by) == false // Boxed values are not equal using "=="
ReferenceEquals(bx, by) == false // References differ

词典使用 GetHashCode()Equals() 来比较对象,因为它们委托给基础值,所以它可以正常工作。


下面的程序证明了这一点:

using System;

namespace Demo
{
    struct MyStruct: IEquatable<MyStruct>
    {
        public int X;

        public bool Equals(MyStruct other)
        {
            return X == other.X;
        }

        public override bool Equals(object obj)
        {
            if (obj is not MyStruct other)
                return false;

            return X == other.X;
        }

        public override int GetHashCode()
        {
            return -X;
        }
    }

    class Program
    {
        static void Main()
        {
            var x = new MyStruct { X = 42 };
            var y = new MyStruct { X = 42 };
            object bx = x;
            object by = y;

            Console.WriteLine(bx.GetHashCode()); // -42
            Console.WriteLine(y.GetHashCode()); // -42

            Console.WriteLine(bx.Equals(by)); // True
            Console.WriteLine(bx == by); // False
            Console.WriteLine(object.ReferenceEquals(bx, by)); // False
        }
    }
}