Jared 的回答当然是正确的。只是添加一点:
当我调试并将鼠标悬停在变量上时,它会显示类型信息
没错。你有一个碗。碗标有“水果”。碗是空的。碗里的水果是什么类型的?你不能说,因为碗里没有水果。但这并不意味着你对碗一无所知。你知道碗里可以装任何水果。
当您将鼠标悬停在一个变量上时,调试器可以告诉您变量本身或其内容。
调试器是否使用其他信息而不是某种类型的反射来找出数据类型?
当然。调试器不仅需要知道此引用所引用的事物的类型是什么,还需要知道对可以存储在此变量中的内容施加了哪些限制。运行时知道有关对特定存储位置施加什么限制的所有信息,并且运行时可以将这些信息告知调试器。
它怎么比我知道的多?
我拒绝问题的前提。调试器代表您运行;它不能做任何你自己做不到的事情。如果你不知道某个特定变量的类型限制是什么,那并不是因为你缺乏找出来的能力。你只是还没看过。
如果它知道这一点,为什么它不能在 NullReferenceException 中显示数据类型?
想想当你取消引用 null 时实际发生了什么。例如,假设您这样做:
Fruit f = null;
string s = f.ToString();
ToString 可能在 Fruit 中被重载。抖动必须生成什么代码?假设局部变量 f 存储在堆栈位置。抖动说:
- 将与 f 关联的堆栈指针偏移处的内存地址的内容复制到寄存器 1
- 虚函数表将是,比方说,该指针顶部的八个字节,ToString 将是,比方说,该表顶部的四个字节。 (我只是在编造这些数字;我不知道真正的偏移量是多少。)所以,首先在寄存器 1 的当前内容中添加 8。
- 现在取消引用寄存器 1 的当前内容以将 vtable 的地址放入寄存器 2 中
- 现在向寄存器 2 添加四个字节
- 现在我们有了一个指向 ToString 方法的指针...
但是等一下,让我们再次遵循这个逻辑。第一步将零放入寄存器 1,因为 f 包含空值。第二步增加了八。第三步解引用指针 0x00000008,虚拟内存系统发出异常,指出刚刚触及了非法内存页面。 CLR 处理异常,确定异常发生在前 64 K 内存上,并猜测有人刚刚取消引用空指针。因此它会创建一个空引用异常并抛出它。
虚拟内存系统肯定不知道它取消引用指针 0x00000008 的原因是因为有人试图调用 f.ToString()。该信息在过去丢失了;内存管理器的工作是在你触摸了你无权触摸的东西时告诉你; 为什么你试图触摸你不拥有的记忆不是它的工作。
CLR 可以维护一个单独的辅助数据结构,这样每次您访问内存时,它都会记录您尝试这样做的原因。这样,异常可以包含更多信息,描述异常发生时您正在做什么。想象一下为每次对内存的访问维护这种数据结构的成本!托管代码很容易比现在慢十倍,而正确代码和损坏代码所承担的成本一样高。为了什么?告诉您您可以轻松弄清楚的内容:您取消引用了哪个包含 null 的变量。
该功能不值得,因此 CLR 不这样做。没有技术原因不能这样做。只是不实用。