问题描述
问题的目的是了解继承是如何在后台进行的;我知道它是什么以及何时使用.
以下是用例-
class A {}
class B : A {}
class C
{
B b = new B();
}
现在,在内存中创建了多少个对象( 排除一个用于C类的对象,因为它将成为入口点,并且会创建任何默认的DotNet/CLR对象)?是两个吗(一个代表A,另一个代表B)?还是只有一个包含A成员的B成员?一些解释会有所帮助.
由于您的问题的目的似乎是基本上了解使用合成和继承之间的物理"区别,因此我将重点介绍
使用new
时,将创建该类型的单个实例,并且该类型的适当构造函数(及其所有父代")将执行一次.
在C#中,默认继承方法(B : A
类型)是子类化.在这种方法中,派生类基本上是其父级的副本,再加上派生类的实例字段,再加上与例如任何新的虚拟方法.
这意味着在您的情况下,调用new B()
仅创建一个对象实例,仅此而已. B
的实例本身包含A
的字段和元数据,但不包含对A
的实例的引用.
如果您这样定义B2
:
class A2
{
int myInt;
}
class B2
{
A2 aInstance = new A2();
}
然后,B2
构造函数还会创建一个A2
实例,因此您有两个对象实例,一个是A2
类型,另一个是B2
类型. B2
仅包含对A2
实例的引用,而不是其副本.
这如何转换为运行时成本?
- 第二种方法意味着间接层.由于.NET分配的工作方式,这可能会影响数据的位置,尽管在通常情况下不会如此-实际上,
A2
往往会被分配在B2
之后. - 第二种方法意味着您将需要一些额外的元数据,因为您有两个实例而不是一个.这基本上意味着指向类型句柄和syncblock索引的指针.这是每个实例的固定成本-同步块为4字节,类型句柄为4字节.我不确定这是否会在64位上更改.除非您的实例具有很少的实例数据,否则这不是很大的代价.我非常确定这不是合同规定的,实际上,实际的最小大小是12个字节,而不是8个字节(或者至少在早期的GC中是这样).
- 第二种方法意味着GC需要担心的一个额外实例.我不确定这在实践中会产生多大的影响-GC仍必须经历相同的内存量,并且我认为这对于实践中的GC性能比对象数量更为重要.但这只是我的估计:)
- 两者的分配成本应该几乎相同,并提供了实例元数据的几个额外字节. .NET堆分配实际上更像是堆栈分配-您只需移动一个指针即可.这不太可能产生变化,特别是与收集和压缩内存的成本相比:)
结果?好吧,我认为这不是您需要事先关心的事情.拥有额外的实例是有代价的,但是除非您要分配数百万个实例,否则可能不会产生太大的可观的改变.如果您的应用程序允许,您甚至可能会获得纯收益,因为合成模型可以让您在多个地方重用同一实例,而子类化是根本不可能的.有时候这很有意义,有时候却没有:)
当然,请注意,您不必总是使用类.例如,A2
可以很容易地成为struct
,从而消除了多余的实例-再次,对于子类来说是不可能的,因为不能继承struct
.在这种情况下,这两种方法就等效了.
与性能通常一样,您确实需要进行实际的分析以获取答案.结果可能类似于"99.9%的代码执行良好,但是如果将其更改为struct
并将多态性移到更高的层,则这一类可以为我们节省很多CPU/RAM". /p>
最后,我很确定这些都不是合同的一部分.如果Microsoft决定在.NET Framework的未来版本中更改继承的工作方式,并使其创建一个新实例而不是内联"父级,则我认为它不会以任何方式违反规范.除非您绝对需要依赖此信息,否则不要.
Purpose of question is to understand how inheritance works under the hood; I am aware about what it is and when to use it.
Following is use case -
class A {}
class B : A {}
class C
{
B b = new B();
}
Now, how many objects (EXCLUDING the one for C class as it will be entry point and any default DotNet/CLR objects) are created in memory? Is it two (one for A and other for B)? Or is it only one for B which also contains members of A? Some explanation will help.
Since it seems that the purpose of your question is to basically to see the "physical" difference between using composition and inheritance, I'm going to focus on that.
When you use new
, a single instance of the type is created, and the appropriate constructor of the type (and all of its "parents") is executed once.
In C#, the default inheritance approach (the B : A
kind) is subclassing. In this approach, a derived class is basically a copy of its parent, plus the instance fields of the derived class, plus the metadata associated with e.g. any new virtual methods.
This means that in your case, calling new B()
only creates a single object instance, and that's it. The instance of B
contains within itself the fields and metadata of A
, but not a reference to an instance of A
.
If you define B2
like this:
class A2
{
int myInt;
}
class B2
{
A2 aInstance = new A2();
}
Then the B2
constructor also creates an instance of A2
, so you have two object instances, one of type A2
, another of type B2
. B2
only contains a reference to the A2
instance, rather than a copy of it.
How does this translate to runtime costs?
- The second approach means a layer of indirection. This can impact data locality, though not in the usual case, due to the way .NET allocation works - in practice,
A2
will tend to be allocated right behindB2
. - The second approach means you'll need a bit of extra metadata, since you have two instances instead of one. This basically means a pointer to a type handle and a syncblock index. This is a fixed cost per instance - 4 bytes for the syncblock, 4 bytes for the type handle. I'm not sure if this changes on 64-bit or not. Unless your instances have very little instance data, this is not a huge cost. I'm pretty sure this is not contractual, though, and in fact, the actual minimum size is 12 bytes, rather than 8 (or at least it used to be with early GCs).
- The second approach means an extra instance for the GC to worry about. I'm not sure how much of an impact this can have in practice - the GC still has to go through the same amount of memory, and I think that's more important for GC performance in practice than the amount of objects. But that's just my ballpark estimate :)
- The allocation cost of both should be pretty much the same, affording for the few extra bytes of instance metadata. .NET heap allocations are really more like stack allocations - you just shift a pointer. This is unlikely to make a difference, especially compared to the cost of collecting and compacting the memory :)
The result? Well, I don't think it's something you need to care about much in advance. There is a cost to having the extra instances, but unless you're allocating millions of instances, it probably isn't going to make much of an observable difference. If your application allows it, you might even have a net gain, since the composition model can allow you to reuse the same instance in multiple places which simply isn't possible with subclassing. Sometimes this makes sense, sometimes it doesn't :)
And of course, note that you don't always have to use classes. For example, A2
can easily be a struct
, eliminating the extra instance - again, impossible with subclassing, since struct
s can't be inherited from. In that case, the two approaches become equivalent.
As is usually the case with performance, you really need to do practical profiling to get your answer. And the result will probably be something like "99.9% of the code performs fine, but this one class could save us a lot of CPU/RAM if we changed it to a struct
and moved the polymorphism to a higher layer".
Finally, I'm pretty sure none of this is part of the contract. If Microsoft decides to change the way inheritance works in a future version of the .NET framework, and makes it create a new instance instead of "inlining" the parent, I don't think it's going to violate the specification in any way. Unless you absolutely need to depend on this information, don't.
这篇关于C#.NET-继承-在内存中创建的对象数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!