代码之家  ›  专栏  ›  技术社区  ›  Juliet

为什么结构不支持继承?

  •  116
  • Juliet  · 技术社区  · 15 年前

    我知道.NET中的结构不支持继承,但它并不完全清楚 为什么? 它们在这方面是有限的。

    什么技术原因阻止结构从其他结构继承?

    10 回复  |  直到 9 年前
        1
  •  117
  •   nevermind    9 年前

    值类型不支持继承的原因是数组。

    问题是,由于性能和GC原因,值类型数组存储为“inline”。例如,给定 new FooType[10] {...} 如果 FooType 是引用类型,将在托管堆上创建11个对象(一个用于数组,10个用于每个类型实例)。如果 分型 而不是值类型,在托管堆上只会为数组本身创建一个实例(因为每个数组值将与数组“内联”存储)。

    现在,假设我们有了值类型的继承。当与上述数组的“内联存储”行为结合在一起时,就会发生一些不好的事情,正如我们所看到的那样。 in C++ .

    考虑这个伪C代码:

    struct Base
    {
        public int A;
    }
    
    struct Derived : Base
    {
        public int B;
    }
    
    void Square(Base[] values)
    {
      for (int i = 0; i < values.Length; ++i)
          values [i].A *= 2;
    }
    
    Derived[] v = new Derived[2];
    Square (v);
    

    根据正常转换规则,a Derived[] 可转换为 Base[] (无论好坏),所以如果您在上面的示例中使用s/struct/class/g,它将按预期编译和运行,不会有任何问题。但是如果 Base Derived 是值类型,数组以内联方式存储值,那么我们就有问题了。

    我们有问题是因为 Square() 什么都不知道 衍生的 ,它将只使用指针算法来访问数组的每个元素,增量为常量。( sizeof(A) )大会大致如下:

    for (int i = 0; i < values.Length; ++i)
    {
        A* value = (A*) (((char*) values) + i * sizeof(A));
        value->A *= 2;
    }
    

    (是的,这是一个令人讨厌的程序集,但关键是我们将在已知的编译时常量处通过数组递增,而不知道正在使用派生类型。)

    所以,如果这真的发生了,我们会有内存损坏的问题。具体来说,在 平方() , values[1].A*=2 事实上 正在修改 values[0].B !

    尝试调试 !

        2
  •  65
  •   Fabrício Matté    15 年前

    假设结构支持继承。然后声明:

    BaseStruct a;
    InheritedStruct b; //inherits from BaseStruct, added fields, etc.
    
    a = b; //?? expand size during assignment?
    

    这意味着结构变量没有固定的大小,这就是为什么我们有引用类型。

    更好的是,考虑一下:

    BaseStruct[] baseArray = new BaseStruct[1000];
    
    baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
    
        3
  •  14
  •   Martin Liversage    12 年前

    结构不使用引用(除非它们被装箱,但您应该尽量避免这样做),因此多态性没有意义,因为没有通过引用指针进行间接寻址。对象通常位于堆上,并通过引用指针引用,但结构是在堆栈上分配的(除非已装箱),或是在堆上引用类型占用的内存中“内部”分配的。

        4
  •  8
  •   Blixt    15 年前

    这里是什么 the docs 说:

    结构对于具有值语义的小型数据结构尤其有用。复数、坐标系中的点或字典中的键值对都是结构的好例子。这些数据结构的关键在于,它们的数据成员很少,不需要使用继承或引用标识,并且可以使用值语义方便地实现它们,其中赋值复制值而不是引用。

    基本上,它们应该保存简单的数据,因此没有继承等“额外特性”。从技术上讲,它们可能支持某种有限的继承(而不是多态性,因为它们位于堆栈中),但我认为,不支持继承也是一种设计选择(就像.NET语言中的许多其他东西一样)。

    另一方面,我同意继承的好处,我认为我们都达到了我们想要的程度。 struct 从另一个继承,并意识到这是不可能的。但是在那一点上,数据结构可能非常先进,无论如何它应该是一个类。

        5
  •  3
  •   Dykam    15 年前

    类继承是不可能的,因为结构直接放置在堆栈上。继承结构比它的父结构要大,但JIT不知道这一点,它试图在太少的空间上放置太多的空间。听起来有点不清楚,我们来举个例子:

    struct A {
        int property;
    } // sizeof A == sizeof int
    
    struct B : A {
        int childproperty;
    } // sizeof B == sizeof int * 2
    

    如果可能的话,它将崩溃在以下代码片段上:

    void DoSomething(A arg){};
    
    ...
    
    B b;
    DoSomething(b);
    

    空间分配给sizeof a,而不是sizeof b。

        6
  •  3
  •   Rui Craveiro    15 年前

    有一点我想纠正。尽管结构不能被继承的原因是因为它们位于堆栈上是正确的,但这是一个半正确的解释。结构,与任何其他值类型一样 可以 生活在堆栈中。因为它将取决于变量的声明位置,所以它们要么位于 堆栈 或者在 . 当它们分别是局部变量或实例字段时,就会出现这种情况。

    这么说,塞西尔有一个正确的名字。

    我想强调一下,价值类型 可以 生活在堆栈上。这并不意味着他们总是这样做。局部变量,包括方法参数,将。其他人都不会。然而,这仍然是他们不能被继承的原因。-)

        7
  •  3
  •   user38001    15 年前

    结构是在堆栈上分配的。这意味着值语义非常自由,访问结构成员非常便宜。这并不能阻止多态性。

    可以让每个结构以指向其虚拟函数表的指针开头。这将是一个性能问题(每个结构都至少有指针大小),但它是可行的。这将允许虚拟函数。

    添加字段怎么样?

    好吧,当您在堆栈上分配一个结构时,您将分配一定的空间。所需空间在编译时确定(无论是提前还是在抖动时)。如果添加字段,然后分配给基类型:

    struct A
    {
        public int Integer1;
    }
    
    struct B : A
    {
        public int Integer2;
    }
    
    A a = new B();
    

    这将覆盖堆栈的某些未知部分。

    另一种方法是运行时仅将sizeof(a)字节写入任何变量,以防止出现这种情况。

    如果B重写A中的一个方法并引用它的integer2字段,会发生什么?要么运行时抛出一个memberAccessException,要么该方法访问堆栈上的一些随机数据。这些都是不允许的。

    拥有结构继承是完全安全的,只要您不使用多态结构,或者在继承时不添加字段。但这些并不是非常有用。

        8
  •  2
  •   Cecil Has a Name    15 年前

    这似乎是一个非常常见的问题。我想补充一点,值类型存储在您声明变量的“就地”;除了实现细节之外,这意味着 表示关于对象的内容的对象头, 只有 变量知道驻留在那里的数据类型。

        9
  •  1
  •   Wyatt Barnett    15 年前

    结构确实支持接口,所以您可以这样做一些多态的事情。

        10
  •  0
  •   Matt Howells    15 年前

    IL是一种基于堆栈的语言,因此使用参数调用方法的过程如下:

    1. 将参数推到堆栈上
    2. 调用方法。

    当方法运行时,它会从堆栈中弹出一些字节以获取其参数。它知道 确切地 由于参数是引用类型指针(在32位上总是4个字节),或者它是一个值类型,其大小总是精确已知,所以要弹出多少字节。

    如果它是一个引用类型指针,那么该方法将查找堆中的对象并获取其类型句柄,该句柄指向一个方法表,该表为该确切类型处理该特定方法。如果是值类型,则不需要查找方法表,因为值类型不支持继承,因此只有一种可能的方法/类型组合。

    如果值类型支持继承,那么会有额外的开销,因为结构的特定类型必须放置在堆栈及其值上,这意味着对该类型的特定具体实例进行某种方法表查找。这将消除价值类型的速度和效率优势。