C#.NET - 继承 - 在内存中创建的对象数

时间:2016-02-03 09:13:22

标签: c# .net oop inheritance

问题的目的是了解遗产是如何运作的;我知道它是什么以及何时使用它。

以下是用例 -

class A {}

class B : A {}

class C
{
B b = new B();
}

现在,在内存中创建了多少个对象( EXCLUDING C类的对象,因为它将是入口点,任何默认的DotNet / CLR对象)?是两个(一个用于A,另一个用于B)?或者它只是B的一个,也包含A的成员?一些解释会有所帮助。

4 个答案:

答案 0 :(得分:4)

为了论证,我们假设您创建了C

的实例
var c = new C();

此时您有两个对象实例,因为在构造期间C会生成B的实例。

要回答您的问题,您的实例为C,实例为B。您没有A的实例,即使B来自A。 (更新:忽略C和任何reflection,您有一个B的对象实例。)

你可以用一些代码证明这一点:

class A { }

class B : A { }

class C
{
    public B B = new B();
}

class Program
{
    static void Main(string[] args)
    {
        var c = new C();

        var b = c.B;

        var BasA = (A)b;

        bool BisA = BasA.GetType() == typeof (A);

        Console.WriteLine($"Assert That B is not A: {!BisA}");
    }
}

另外,您可以通过调试器查看所有内存:

enter image description here

还要注意术语。 ABC是类。对象是类的实例。在C#中,描述类的信息可以封装在System.Type类的实例中。

让我们一点儿走下兔子洞; 在执行程序集中,你在内存中有多少个对象?

将我们的范围仅限于这些类,除了从实例C获得的两个对象之外,您还将有System.RuntimeType的三个实例,A,{{1} }和B

C

enter image description here

同样,这显示了您有三个 var assemblyTypes = Assembly.GetExecutingAssembly().GetTypes(); foreach (var classType in assemblyTypes) Console.WriteLine("Type instance: " + classType); 实例,描述 System.RuntimeTypeAB

迂腐,您还将获得C(对于您的可执行文件)和RuntimeAssembly(对于您的控制台程序类)以及其他人的实例

答案 1 :(得分:1)

如果忽略C(作为程序条目),将有一个B对象和两个描述System.RuntimeTypeA类的B个对象。

通过C#,第四版,P100-110参考CLR

答案 2 :(得分:1)

使用关键字时会创建新对象。

源代码更像是一个蓝图,为简单起见,您指定一个对象扩展行为和另一个对象的成员(继承它)。描述类型本身需要此信息。实际对象是使用 new 关键字基于类型描述构建的。

在您的情况下,只会创建一个对象。

答案 3 :(得分:1)

因为你的问题的目的似乎是基本上看到" physical"使用构图和继承之间的区别,我将专注于此。

当你使用new时,会创建一个类型的实例,并且该类型的相应构造函数(及其所有" parent")将被执行一次。

在C#中,默认的继承方法(B : A种类)是子类化。在这种方法中,派生类基本上是其父类的副本,加上派生类的实例字段,以及与例如相关联的元数据。任何新的虚拟方法。

这意味着在您的情况下,调用new B()只会创建一个对象实例,而这就是它。 B的实例本身包含A的字段和元数据,但不包含A实例的引用

如果你这样定义B2

class A2
{
  int myInt;
}

class B2
{
  A2 aInstance = new A2();
}

然后B2构造函数还会创建A2的实例,因此您有两个对象实例,一个类型为A2,另一个类型为B2B2仅包含对A2实例的引用,而不是其副本。

这如何转化为运行时成本?

  • 第二种方法意味着一层间接。由于.NET分配的工作方式,这可能会影响数据的局部性,但不是通常的情况 - 实际上,A2往往会分配在B2后面。
  • 第二种方法意味着您需要一些额外的元数据,因为您有两个实例而不是一个。这基本上意味着指向类型句柄和同步块索引的指针。这是每个实例的固定成本 - 同步块为4个字节,类型句柄为4个字节。我不确定这是否会在64位上发生变化。除非您的实例具有非常少的实例数据,否则这不是一个巨大的成本。我很确定这不是契约性的,事实上,实际的最小大小是12个字节,而不是8个字节(或者至少它是早期的GC)。
  • 第二种方法意味着GC需要担心的额外实例。我不确定这在实践中会产生多大的影响 - GC仍然必须经历相同的内存量,而且我认为GC在实践中对于GC性能的影响比对象。但那只是我的大概估计:)
  • 两者的分配成本应该几乎相同,从而提供实例元数据的少量额外字节。 .NET堆分配实际上更像堆栈分配 - 您只需移动指针。这不太可能有所作为,特别是与收集和压缩内存的成本相比:)

结果呢?好吧,我不认为这是你需要提前关心的事情。拥有额外的实例需要付出代价,但除非您分配数百万个实例,否则它可能不会产生很大的可观察差异。如果您的应用程序允许,您甚至可能获得净收益,因为组合模型可以允许您在多个地方重用相同的实例,这对于子类化是不可能的。有时这是有道理的,有时它不会:)

当然,请注意,您不必总是使用课程。例如,A2可以很容易地成为struct,从而消除了额外的实例 - 再次,不可能通过子类化,因为struct不能继承。在这种情况下,这两种方法就相同了。

通常情况就是如此,你真的需要做一些实用的分析来得到你的答案。结果可能就像" 99.9%的代码表现不错,但如果我们将它改为struct并将多态性移动到一个类,那么这一个类可以为我们节省大量的CPU / RAM更高层"。

最后,我非常确定这些都不是合同的一部分。如果Microsoft决定改变继承在.NET框架的未来版本中的工作方式,并使其创建一个新实例而不是" inlining"父母,我不认为它会以任何方式违反规范。除非你绝对需要依赖这些信息,否则不要。