装箱类型相等键和字典键

Boxing type equality and dictionary keys

提问人:Skary 提问时间:11/24/2021 最后编辑:Uwe KeimSkary 更新时间:11/24/2021 访问量:320

问:

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

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
     }
}

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

这是我使用的小提琴:https://dotnetfiddle.net/DW54nN

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

C# 字典 相等 装箱

评论

0赞 Mark Davies 11/24/2021
这是一个引用与值类型的东西,当你使用对象时,它会变成一个值,所以当你比较它们时,它们会比较引用(boxedI 和 boxedN 的两个引用不相同),而当你使用它时,它使用引用类型中的值_dict.ContainsKey(boxedN)
3赞 Jon Skeet 11/24/2021
嗯,更重要的是,它使用 Equals 和 GetHashCode。如果调用 that 将返回 true,如果对它们都调用 GetHashCode,它们将返回相同的值。boxedI.Equals(boxedN)
1赞 Skary 11/24/2021
@JonSkeet所以我的错误是假设 == 操作数等价于 Equals 是等价的(只需阅读一篇关于 C# corer 的文章)。谢谢简洁明了。

答:

1赞 Mark Davies 11/24/2021 #1

这是一个引用与值类型的事情:

int i = 5;
int n = 5;

这些是值类型并被放在堆栈上,因此当我们比较它们时,我们转到堆栈,可以说 i 和 n 的值是 5,这使它们“相等”。

object boxedI = i;
object boxedN = n;

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

#0005 -> boxedI
#0006 -> boxedN

现在,当您执行相等操作时,您正在比较哪些不相同#0005 == #0006

但是,当您传入或传入该方法时,该方法知道如何遵循对堆 (5) 上值的引用(或指针)。boxedIboxedNContainsKey

因此,当你要求你正在做的事情时,就是要求(粗略地说)ContainsKey(boxedI)ContainsKey(5)

这就是为什么这两者是“平等的”

评论

0赞 Skary 11/24/2021
是的,我只想补充几句关于我的错误是如何起源的。我错误地认为“==”运算符只是 Equal() 运算符的简写。前者比较参考,后者比较值,因此 bheavoiour 与我的期望不同。
0赞 Charlieface 11/25/2021
堆栈是一个实现细节,与问题完全无关。这完全是关于引用与值类型的语义,这同样适用于堆栈分配的引用和/或堆分配的值
0赞 Mark Davies 11/25/2021
@Charlieface我在解释为什么海报看到比较两种值类型和两种引用类型之间存在差异时,我看不到你是如何做到的,而不谈论堆栈,我不认为它像你所说的那样“完全无关紧要”。只是以抽象的方式解释为什么存在差异的简单方法
0赞 Charlieface 11/25/2021
这是绝对无关紧要的。它们之间的区别不在于它是否在堆栈上,而在于引用和值类型具有不同的语义和定义的行为:一个具有引用语义(新值引用旧值),另一个具有复制义(新值被克隆)。无论如何,这不是此 OP 中的问题,实际问题是在类型上将进行引用比较,而理论上您可以调用装箱值类型并获得正确的输出(尽管 CLR 和 C# 不支持定义为装箱值类型的变量)==objectEquals
2赞 Matthew Watson 11/24/2021 #2

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


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

也就是说,给定:

  • 正确实现的值类型。VTGetHashCode()Equals()
  • 的实例。xVT
  • 的值与 相同的实例。yVTx
  • 的盒装实例 : 。xbx
  • 的盒装实例 : 。yby

情况如下:

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

但是,== 运算符不是由盒装版本委托的,实际上它是使用引用相等来实现的,因此:

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

Dictionary 使用 和 用于比较对象,并且由于它们委托给基础值,因此它可以正常工作。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
        }
    }
}