¿Por qué las estructuras no admiten la herencia?

128

Sé que las estructuras en .NET no admiten la herencia, pero no está exactamente claro por qué están limitadas de esta manera.

¿Qué razón técnica impide que las estructuras hereden de otras estructuras?

Julieta
fuente
66
No me estoy muriendo por esta funcionalidad, pero puedo pensar en algunos casos en los que la herencia de estructura sería útil: es posible que desee extender una estructura Point2D a una estructura Point3D con herencia, es posible que desee heredar de Int32 para restringir sus valores entre 1 y 100, es posible que desee crear un tipo de definición que es visible a través de múltiples archivos (el uso de typeA = truco typeB tiene ámbito de archivo solamente), etc.
Julieta
44
Es posible que desee leer stackoverflow.com/questions/1082311/… , que explica un poco más sobre las estructuras y por qué deberían restringirse a un cierto tamaño. Si desea usar la herencia en una estructura, entonces probablemente debería estar usando una clase.
Justin
1
Y es posible que desee leer stackoverflow.com/questions/1222935/… a medida que profundiza por qué no se pudo hacer en la plataforma dotNet. Se han convertido en C ++, con los mismos problemas que pueden ser desastrosos para una plataforma administrada.
Dykam el
Las clases de @Justin tienen costos de rendimiento que las estructuras pueden evitar. Y en el desarrollo de juegos eso realmente importa. Entonces, en algunos casos, no deberías usar una clase si puedes evitarla.
Gavin Williams
@Dykam Creo que se puede hacer en C #. Desastroso es una exageración. Puedo escribir código desastroso hoy en C # cuando no estoy familiarizado con una técnica. Entonces eso no es realmente un problema. Si la herencia de estructura puede resolver algunos problemas y brindar un mejor rendimiento en ciertos escenarios, entonces estoy a favor.
Gavin Williams

Respuestas:

121

La razón por la que los tipos de valor no pueden admitir la herencia se debe a las matrices.

El problema es que, por razones de rendimiento y GC, las matrices de tipos de valores se almacenan "en línea". Por ejemplo, dado que new FooType[10] {...}si FooTypees un tipo de referencia, se crearán 11 objetos en el montón administrado (uno para la matriz y 10 para cada instancia de tipo). Si, en FooTypecambio, es un tipo de valor, solo se creará una instancia en el montón administrado, para la matriz misma (ya que cada valor de la matriz se almacenará "en línea" con la matriz).

Ahora, supongamos que tenemos herencia con tipos de valor. Cuando se combina con el comportamiento anterior de "almacenamiento en línea" de las matrices, suceden cosas malas, como se puede ver en C ++ .

Considere este código pseudo-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);

Según las reglas de conversión normales, a Derived[]es convertible a a Base[](para bien o para mal), por lo que si s / struct / class / g para el ejemplo anterior, se compilará y ejecutará como se esperaba, sin problemas. Pero si Basey Derivedson tipos de valores, y las matrices almacenan valores en línea, entonces tenemos un problema.

Tenemos un problema porque Square()no sabe nada Derived, solo usará aritmética de puntero para acceder a cada elemento de la matriz, incrementándolo en una cantidad constante ( sizeof(A)). La asamblea sería vagamente como:

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

(Sí, es un ensamblaje abominable, pero el punto es que incrementaremos a través de la matriz en constantes de tiempo de compilación conocidas, sin ningún conocimiento de que se esté utilizando un tipo derivado).

Entonces, si esto realmente sucediera, tendríamos problemas de corrupción de memoria. En concreto, dentro Square(), values[1].A*=2sería realmente estar modificando values[0].B!

Intenta depurar ESO !

jonp
fuente
11
La solución sensata a ese problema sería no permitir la forma de lanzamiento Base [] a Detived []. Al igual que está prohibido emitir de short [] a int [], aunque es posible emitir de short a int.
Niki el
3
+ respuesta: el problema con la herencia no hizo clic conmigo hasta que lo pones en términos de matrices. Otro usuario declaró que este problema podría mitigarse al "cortar" estructuras al tamaño apropiado, pero veo que cortar es la causa de más problemas de los que resuelve.
Julieta el
44
Sí, pero eso "tiene sentido" porque las conversiones de matriz son para conversiones implícitas, no conversiones explícitas. short to int es posible, pero requiere una conversión, por lo que es sensato que short [] no se puede convertir en int [] (short del código de conversión, como 'a.Select (x => (int) x) .ToArray ( ) '). Si el tiempo de ejecución no permitiera el lanzamiento de Base a Derivado, sería una "verruga", ya que ESTO está permitido para los tipos de referencia. Por lo tanto, tenemos dos posibles "verrugas" diferentes: prohibir la herencia de estructuras o prohibir las conversiones de matrices derivadas a matrices de base.
jonp
3
Al menos al evitar la herencia de estructuras tenemos una palabra clave separada y podemos decir más fácilmente "las estructuras son especiales", en lugar de tener una limitación "aleatoria" en algo que funciona para un conjunto de cosas (clases) pero no para otro (estructuras) . Me imagino que la limitación de estructura es mucho más fácil de explicar ("¡son diferentes!").
jonp
2
necesita cambiar el nombre de la función de 'cuadrado' a 'doble'
John
69

Imagine estructuras compatibles con la herencia. Luego declarando:

BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.

a = b; //?? expand size during assignment?

significaría que las variables de estructura no tienen un tamaño fijo, y es por eso que tenemos tipos de referencia.

Aún mejor, considere esto:

BaseStruct[] baseArray = new BaseStruct[1000];

baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Kenan EK
fuente
55
C ++ respondió esto mediante la introducción del concepto de 'corte', por lo que es un problema solucionable. Entonces, ¿por qué no se debe admitir la herencia de estructura?
jonp
1
Considere las matrices de estructuras heredables y recuerde que C # es un lenguaje administrado (memoria). Rebanar o cualquier opción similar causaría estragos en los fundamentos del CLR.
Kenan EK
44
@ jonp: Soluble, sí. ¿Deseable? Aquí hay un experimento mental: imagine si tiene una clase base Vector2D (x, y) y una clase derivada Vector3D (x, y, z). Ambas clases tienen una propiedad Magnitud que calcula sqrt (x ^ 2 + y ^ 2) y sqrt (x ^ 2 + y ^ 2 + z ^ 2) respectivamente. Si escribe 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', ¿qué debería devolver' a.Magnitude == b.Magnitude '? Si luego escribimos 'a = (Vector3D) b', ¿tiene a.Magnitude el mismo valor antes de la asignación que después? Los diseñadores de .NET probablemente se dijeron a sí mismos "no, no tendremos nada de eso".
Julieta el
1
El hecho de que un problema pueda resolverse no significa que deba resolverse. A veces es mejor evitar situaciones donde surge el problema.
Dan Diplo
@ kek444: Tener una Fooherencia de estructura Barno debe permitir Fooque se asigne a a Bar, pero declarar una estructura de esa manera podría permitir un par de efectos útiles: (1) Crear un miembro de tipo especialmente nombrado Barcomo el primer elemento en Fooe Fooincluir nombres de los miembros que alias a aquellos miembros en Bar, lo que permite código que habían usado Barpara adaptarse al uso de una Foovez, sin tener que reemplazar todas las referencias a thing.BarMembercon thing.theBar.BarMember, y retener la capacidad de leer y escribir todos Bar's campos como grupo; ...
supercat
14

Las estructuras no usan referencias (a menos que estén encuadradas, pero debe tratar de evitar eso), por lo tanto, el polimorfismo no es significativo ya que no hay indirección a través de un puntero de referencia. Los objetos normalmente viven en el montón y se hace referencia a ellos mediante punteros de referencia, pero las estructuras se asignan en la pila (a menos que estén encuadradas) o se asignan "dentro" de la memoria ocupada por un tipo de referencia en el montón.

Martin Liversage
fuente
8
uno no necesita usar el polimorfismo de tomar ventaja de la herencia
rmeador
Entonces, ¿cuántos tipos de herencia diferentes hay en .NET?
John Saunders
El polimorfismo existe en las estructuras, solo considere la diferencia entre llamar a ToString () cuando lo implementa en una estructura personalizada o cuando no existe una implementación personalizada de ToString ().
Kenan EK
Eso es porque todos derivan de System.Object. Es más el polimorfismo del sistema. Tipo de objeto que de estructuras.
John Saunders
El polimorfismo podría ser significativo con estructuras utilizadas como parámetros de tipo genérico. El polimorfismo funciona con estructuras que implementan interfaces; El mayor problema con las interfaces es que no pueden exponer byrefs a los campos de estructura. De lo contrario, lo más importante que creo que sería útil en la medida en que "heredar" estructuras sería un medio para tener un tipo (estructura o clase) Fooque tenga un campo de tipo estructura Barcapaz de considerar a Barlos miembros como propios, de modo que una Point3dclase podría, por ejemplo, encapsular a Point2d xypero referirse a Xese campo como xy.Xo X.
supercat
8

Esto es lo que dicen los documentos :

Las estructuras son particularmente útiles para pequeñas estructuras de datos que tienen semántica de valor. Los números complejos, los puntos en un sistema de coordenadas o los pares clave-valor en un diccionario son buenos ejemplos de estructuras. La clave para estas estructuras de datos es que tienen pocos miembros de datos, que no requieren el uso de herencia o identidad referencial, y que pueden implementarse convenientemente utilizando semántica de valores donde la asignación copia el valor en lugar de la referencia.

Básicamente, se supone que contienen datos simples y, por lo tanto, no tienen "características adicionales" como la herencia. Probablemente sería técnicamente posible para ellos admitir algún tipo limitado de herencia (no polimorfismo, debido a que están en la pila), pero creo que también es una opción de diseño no admitir la herencia (como muchas otras cosas en .NET los idiomas son)

Por otro lado, estoy de acuerdo con los beneficios de la herencia, y creo que todos hemos llegado al punto en que queremos structque heredemos de otro, y nos damos cuenta de que no es posible. Pero en ese punto, la estructura de datos es probablemente tan avanzada que de todos modos debería ser una clase.

Blixt
fuente
44
Esa no es la razón por la cual no hay herencia.
Dykam
Creo que la herencia de la que se habla aquí no es poder usar dos estructuras donde una hereda de la otra de manera intercambiable, sino reutilizar y agregar a la implementación de una estructura a otra (es decir, crear una Point3Dde a Point2D; no sería capaz de usar un en Point3Dlugar de un Point2D, pero no Point3D
tendrías que volver a implementar
En resumen: podría soportar la herencia sin polimorfismo. No lo hace. Creo que es una opción de diseño para ayudar a una persona elegir classsobre structsu caso.
Blixt
3

La clase como herencia no es posible, ya que una estructura se coloca directamente en la pila. Una estructura heredada sería más grande que su padre, pero el JIT no lo sabe e intenta poner demasiado en menos espacio. Suena un poco confuso, escribamos un ejemplo:

struct A {
    int property;
} // sizeof A == sizeof int

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

Si esto fuera posible, se bloquearía en el siguiente fragmento:

void DoSomething(A arg){};

...

B b;
DoSomething(b);

Se asigna espacio para el tamaño de A, no para el tamaño de B.

Dykam
fuente
2
C ++ maneja este caso muy bien, IIRC. La instancia de B se divide para ajustarse al tamaño de una A. Si es un tipo de datos puro, como lo son las estructuras .NET, entonces no sucederá nada malo. Te encuentras con un pequeño problema con un método que devuelve una A y estás almacenando ese valor de retorno en una B, pero eso no debería permitirse. En resumen, los diseñadores .NET podrían haber tratado esto si quisieran, pero no lo hicieron por alguna razón.
rmeador 03 de
1
Para su DoSomething (), no es probable que haya un problema ya que (suponiendo que la semántica de C ++) 'b' se "corte" para crear una instancia A. El problema es con <i> arrays </i>. Considere sus estructuras A y B existentes y un método <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Dado que las matrices de tipos de valores almacenan los valores "en línea", DoSomething (actual = new B [2] {}) hará que se establezca [0] .childproperty real, no real [1] .property. Esto es malo.
jonp
2
@John: No estaba afirmando que lo fuera, y tampoco creo que @jonp lo fuera. Simplemente mencionamos que este problema es antiguo y se ha resuelto, por lo que los diseñadores de .NET decidieron no admitirlo por alguna otra razón que no sea la inviabilidad técnica.
rmeador 03 de
Cabe señalar que el problema de "matrices de tipos derivados" no es nuevo en C ++; ver parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (¡Las matrices en C ++ son malas! ;-)
jonp
@John: la solución para el problema "las matrices de tipos derivados y los tipos base no se mezclan" es, como de costumbre, Don't Do That. Es por eso que las matrices en C ++ son malas (permite más fácilmente la corrupción de la memoria), y por qué .NET no admite la herencia con tipos de valor (el compilador y JIT aseguran que no pueda suceder).
jonp
3

Hay un punto que me gustaría corregir. Aunque la razón por la que las estructuras no pueden heredarse es porque viven en la pila es la correcta, es al mismo tiempo una explicación medio correcta. Las estructuras, como cualquier otro tipo de valor, pueden vivir en la pila. Debido a que dependerá de dónde se declare la variable, vivirán en la pila o en el montón . Esto será cuando sean variables locales o campos de instancia respectivamente.

Al decir eso, Cecil tiene un nombre lo clavó correctamente.

Me gustaría enfatizar esto, los tipos de valor pueden vivir en la pila. Esto no significa que siempre lo hagan. Las variables locales, incluidos los parámetros del método, lo harán. Todos los demás no lo harán. Sin embargo, sigue siendo la razón por la que no se pueden heredar. :-)

Rui Craveiro
fuente
3

Las estructuras se asignan en la pila. Esto significa que la semántica del valor es bastante gratuita, y acceder a los miembros de la estructura es muy barato. Esto no previene el polimorfismo.

Puede hacer que cada estructura comience con un puntero a su tabla de funciones virtuales. Esto sería un problema de rendimiento (cada estructura tendría al menos el tamaño de un puntero), pero es factible. Esto permitiría funciones virtuales.

¿Qué hay de agregar campos?

Bueno, cuando asigna una estructura en la pila, asigna una cierta cantidad de espacio. El espacio requerido se determina en tiempo de compilación (ya sea antes de tiempo o cuando JITting). Si agrega campos y luego los asigna a un tipo base:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Esto sobrescribirá alguna parte desconocida de la pila.

La alternativa es que el tiempo de ejecución evite esto escribiendo solo bytes sizeof (A) en cualquier variable A.

¿Qué sucede si B anula un método en A y hace referencia a su campo Integer2? O el tiempo de ejecución arroja una excepción MemberAccessException o el método accede a algunos datos aleatorios en la pila. Ninguno de estos es permisible.

Es perfectamente seguro tener herencia de estructura, siempre y cuando no use estructuras polimórficas, o mientras no agregue campos al heredar. Pero estos no son terriblemente útiles.

user38001
fuente
1
Casi. Nadie más mencionó el problema de segmentación en referencia a la pila, solo en referencia a las matrices. Y nadie más mencionó las soluciones disponibles.
user38001
1
Todos los tipos de valor en .net se rellenan con cero al crearlos, independientemente de su tipo o de los campos que contienen. Agregar algo así como un puntero vtable a una estructura requeriría un medio de inicializar tipos con valores predeterminados distintos de cero. Tal característica podría ser útil para una variedad de propósitos, y la implementación de tal cosa para la mayoría de los casos podría no ser demasiado difícil, pero no existe nada cercano en .net.
supercat
@ user38001 "Las estructuras se asignan en la pila", a menos que sean campos de instancia, en cuyo caso se asignan en el montón.
David Klempfner el
2

Esta parece una pregunta muy frecuente. Tengo ganas de agregar que los tipos de valores se almacenan "en el lugar" donde declaras la variable; aparte de los detalles de implementación, esto significa que no hay un encabezado de objeto que diga algo sobre el objeto, solo la variable sabe qué tipo de datos reside allí.

Cecil tiene un nombre
fuente
El compilador sabe lo que hay allí. Hacer referencia a C ++ no puede ser la respuesta.
Henk Holterman el
¿De dónde inferiste C ++? Diría que decir in situ porque eso es lo que mejor se adapta al comportamiento, la pila es un detalle de implementación, por citar un artículo de blog de MSDN.
Cecil tiene nombre el
Sí, mencionar C ++ fue malo, solo mi línea de pensamiento. Pero aparte de la pregunta de si se necesita información de tiempo de ejecución, ¿por qué las estructuras no deberían tener un 'encabezado de objeto'? El compilador puede mezclarlos como quiera. Incluso podría ocultar un encabezado en una estructura [Structlayout].
Henk Holterman el
Debido a que las estructuras son tipos de valor, no es necesario con un encabezado de objeto porque el tiempo de ejecución siempre copia el contenido como para otros tipos de valor (una restricción). No tendría sentido con un encabezado, porque para eso están las clases de tipo de referencia: P
Cecil tiene un nombre el
1

Las estructuras admiten interfaces, por lo que puede hacer algunas cosas polimórficas de esa manera.

Wyatt Barnett
fuente
0

IL es un lenguaje basado en pila, por lo que llamar a un método con un argumento es algo así:

  1. Empuja el argumento sobre la pila
  2. Llama al método.

Cuando se ejecuta el método, saca algunos bytes de la pila para obtener su argumento. Sabe exactamente cuántos bytes aparecer porque el argumento es un puntero de tipo de referencia (siempre 4 bytes en 32 bits) o es un tipo de valor para el que el tamaño siempre se conoce exactamente.

Si se trata de un puntero de tipo de referencia, el método busca el objeto en el montón y obtiene su identificador de tipo, que apunta a una tabla de métodos que maneja ese método en particular para ese tipo exacto. Si se trata de un tipo de valor, no es necesario buscar una tabla de métodos porque los tipos de valores no admiten la herencia, por lo que solo hay una combinación posible de método / tipo.

Si los tipos de valor admitían la herencia, habría una sobrecarga adicional en que el tipo particular de la estructura tendría que colocarse en la pila, así como su valor, lo que significaría algún tipo de búsqueda en la tabla de métodos para la instancia concreta particular del tipo. Esto eliminaría las ventajas de velocidad y eficiencia de los tipos de valor.

Matt Howells
fuente
1
C ++ ha resuelto eso, lea esta respuesta para el problema real: stackoverflow.com/questions/1222935/…
Dykam