为什么值类型要继承自引用类型?

13

我有两个问题:

  1. 我们知道所有类型都派生自引用类型 Object。我的问题是为什么 int - 一个值类型 - 要从引用类型 Object 继承?这是否可能?

  2. 如果 int 是从 Object 派生的,那么为什么在将 int 传递给期望参数为 object 的函数时需要进行装箱?通常,在引用类型时,当您需要将派生类型的对象作为参数传递给期望基类型对象的函数时,您不需要进行任何其他操作。为什么这里要装箱?

对我来说,这种情况似乎是类型层次结构设计上的问题。

PS。我找到了一个相关问题,但那里的答案没有提供任何实际的见解 - 只是抽象地谈论盒子。


2
这个问题的另一个问题的被接受答案更加相关。 - Paolo Falabella
1
你的问题已经有了答案。是装箱操作创建了一个假象,即值类型派生自Object。 - Hans Passant
1
@HansPassant,文档明确指出值类型继承自对象 - 而对象是引用类型,不是吗? - user5528169
2
幻觉有什么问题吗?如果它像鸭子一样嘎嘎叫,那么文档会说它是一只鸭子。 - Hans Passant
显示剩余4条评论
3个回答

5
我们需要小心不要混淆概念。首先,有一个子类型的概念。int是object的子类型。子类型基本上意味着由超类型保证的契约(例如,“有一个方法ToString,它返回适当的字符串表示形式。”)也对子类型保证。其次,C#中有继承的概念。
在C#中,继承通过以下方式创建子类型:
1. 确保超类型提供的接口在子类型中也可用
2. 提供默认实现,即,如果您没有覆盖方法,则会获得超类型的实现。这基本上是一种方便的特性。(C#中的接口实现将是另一种提供1而不提供2的子类型机制的例子。)
以上就是全部内容。子类型或继承都不保证关于内存布局、值/引用类型语义等的任何保证。这些概念是正交的。
你可能会说,“但这不对。”“object”合同的一部分是“引用类型语义”。这就是为什么需要装箱。每当值类型的编译时类型是引用类型(即object,ValueType或接口)时,它模拟了引用类型语义。

“但这不对,”你可能会说。“对象契约的一部分是‘引用类型语义’。”这就是需要装箱的地方。每当值类型的编译时类型为引用类型(即对象、ValueType或接口)时,它模拟引用类型语义。”- 好吧,但是通常情况下,当将派生类型的对象作为参数传递给期望基类型引用的函数时,您不需要做任何事情。但在这里,您需要进行装箱。因此,这在某种程度上是奇怪的情况。 - user5528169
@user200312:确实。我想这就是我们为了拥有结构体和类的共同基类以及接口类型变量具有一致(引用类型)语义而必须付出的代价。由于编译器完成了所有工作(自动装箱和拆箱),所以这是我愿意支付的代价。 - Heinzi

3
我们知道所有类型都派生自Object,它是引用类型。我的问题是,为什么int - 这是值类型 - 要继承自引用类型Object?这可能吗?
System.Int32派生自System.ValueType,包括C#中的所有结构体。编译器允许这种继承链,这与编译器禁止您在任何其他struct类型中进行继承相同。公共语言运行时(CLR)对从System.ValueType派生的类型有特殊的语义。System.ValueType本身不是值类型,而是引用类型,它是所有结构体的基类。尽管存在这种继承层次结构,但它并不保证关于对象在内存中的布局方面的任何内容。
为什么我们需要将int装箱(boxing)传递给期望参数为object的函数?通常情况下,当您需要将派生类型的对象作为参数传递给期望基类型对象的函数时,您不需要执行任何其他操作。为什么要在这里进行装箱?
因为虽然任何结构体最终都派生自object,但实际上它们被运行时以不同的方式处理。所有结构体都被视为数据块,它们没有方法表指针或同步块索引,这是.NET中每个引用类型都具有的。这就是为什么将任何值类型传递给接受object的方法都必须进行装箱,因为需要添加附加数据以使其成为完全合格的object类型。当您调用作为实现接口的结果添加到struct的方法时,Value Types也会被装箱,例如。该值类型需要进行装箱,以获取要调用的实际方法表指针。
您可以通过一个小例子来看到它:
void Main()
{
    IFoo m = new M();
    m.X();
}

public struct M : IFoo
{
    public void X() { }
}

public interface IFoo 
{
    void X();
}

将在 Release 模式下编译,产生以下 IL 代码:
IL_0000:  ldloca.s    00 
IL_0002:  initobj     UserQuery.M
IL_0008:  ldloc.0     
IL_0009:  box         UserQuery.M
IL_000E:  callvirt    UserQuery+IFoo.X
IL_0013:  ret         

@downvoter请解释一下你的负评。 - Yuval Itzchakov
我们都有一个。这里的每篇帖子。 - Patrick Hofman
@Henk,我并没有说需要解释downvotes。我只是友好地询问谁给了downvote,如果他真的很友善,能否解释一下回答有什么问题,这样我就可以改进它。我并没有强制任何人这样做。 - Yuval Itzchakov
1
@HenkHolterman,虽然不必解释为什么要downvote,但是没有解释就downvote仍然很粗鲁。当然,粗鲁并不是被禁止的... - Thomas Levesque
1
嗨!又见面了!还记得我说过我在向你学习吗?哈哈,这很有趣,所以我自己也尝试了一下,但你需要再澄清一件事情!当我使用 M m = new M(); 而不是 IFoo m = new M(); 进行测试时,没有发生装箱。 - Jenix
哦,我认为这行代码会导致装箱,无论我是否调用该方法。 - Jenix

1

值类型可以在栈上分配或内联分配在结构中。引用类型是在堆上分配的。引用类型和值类型都派生自最终基类Object。在需要将值类型作为对象时,会在堆上分配一个使值类型看起来像引用对象的包装器,并将值类型的值复制到其中。该包装器被标记为包含值类型,系统知道它是值类型。这个过程称为装箱,反向过程称为拆箱。装箱和拆箱允许任何类型被视为对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接