¿Por qué las estructuras no admiten la herencia?

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

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

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 el new FooType[10] {...} , si FooType es un tipo de referencia, se crearán 11 objetos en el montón gestionado (uno para la matriz y 10 para cada instancia de tipo). Si FooType es en su lugar 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 combinan con el comportamiento de arrays “de almacenamiento en línea” anterior, ocurren 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, un Derived[] es convertible a Base[] (para bien o para mal), así que si s / struct / class / g para el ejemplo anterior, se comstackrá y ejecutará como se espera, sin problemas . Pero si Base y Derived son tipos de valores, y los arreglos almacenan valores en línea, entonces tenemos un problema.

Tenemos un problema porque Square() no sabe nada sobre Derived , solo usará aritmética de puntero para acceder a cada elemento de la matriz, incrementándose 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í, eso es assembly abominable, pero el punto es que boostemos a través de la matriz en constantes de tiempo de comstackció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. Específicamente, dentro de Square() , los values[1].A*=2 serían en realidad values[0].B modificadores values[0].B !

Intenta depurar ESO !

Imagine estructura herencia soportada. Luego declarando:

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

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

Aún mejor, considera esto:

 BaseStruct[] baseArray = new BaseStruct[1000]; baseArray[500] = new InheritedStruct(); //?? morph/resize the array? 

Las estructuras no usan referencias (a menos que estén encuadradas, pero debes tratar de evitarlas), por lo que el polymorphism 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 referencian a través de punteros de referencia, pero las estructuras se asignan en la stack (a menos que estén encuadradas) o se asignan “dentro” de la memoria ocupada por un tipo de referencia en el montón.

Esto es lo que dicen los documentos :

Las estructuras son particularmente útiles para estructuras de datos pequeñas que tienen semántica de valores. 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 de 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 de manera conveniente usando la 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 sea técnicamente posible que admitan algún tipo de herencia limitada (no polymorphism, debido a que están en la stack), pero creo que también es una opción de diseño para no admitir la herencia (como muchas otras cosas en .NET). 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 que nuestra struct herede de otra, y nos damos cuenta de que no es posible. Pero en ese punto, la estructura de datos es probablemente tan avanzada que debería ser una clase de todos modos.

La clase como herencia no es posible, ya que una estructura se coloca directamente en la stack. Una estructura heredada sería más grande que la primaria, pero el JIT no lo sabe, y trata de poner demasiado demasiado espacio. Suena un poco confuso, vamos a escribir 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); 

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

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 stack es la correcta, es a la vez una explicación a medias correcta. Las estructuras, como cualquier otro tipo de valor, pueden vivir en la stack. Como dependerá de dónde se declare la variable, vivirán en la stack o en el montón . Esto será cuando sean variables locales o campos de instancia respectivamente.

Al decir eso, Cecil Has a Name lo clavó correctamente.

Me gustaría enfatizar esto, los tipos de valores pueden vivir en la stack. 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 pueden ser heredados. 🙂

Las estructuras se asignan en la stack. Esto significa que la semántica de valores es bastante libre, y acceder a los miembros de struct es muy barato. Esto no previene el polymorphism.

Puede hacer que cada estructura comience con un puntero a su tabla de funciones virtuales. Esto sería un problema de rendimiento (cada estructura sería al menos del 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 stack, asigna una cierta cantidad de espacio. El espacio requerido se determina en tiempo de comstackción (ya sea antes de tiempo o cuando JITting). Si agrega campos y luego asigne a un tipo base:

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

Esto sobrescribirá una parte desconocida de la stack.

La alternativa es que el tiempo de ejecución lo evite escribiendo solo el tamaño de (A) bytes en cualquier variable A.

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

Es perfectamente seguro tener herencia struct, siempre y cuando no uses estructuras polimórficamente, o siempre que no agregues campos cuando heredes. Pero estos no son terriblemente útiles.

Esta parece ser una pregunta muy frecuente. Tengo ganas de agregar que los tipos de valores se almacenan “en su 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í.

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

IL es un lenguaje basado en stack, por lo que llamar a un método con un argumento es algo como esto:

  1. Presiona el argumento en la stack
  2. Llamar al método

Cuando el método se ejecuta, saca algunos bytes de la stack para obtener su argumento. Sabe exactamente cuántos bytes saltar 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 es un puntero de tipo de referencia, el método busca el objeto en el montón y obtiene su manejador de tipo, que apunta a una tabla de métodos que maneja ese método particular para ese tipo exacto. Si se trata de un tipo de valor, no es necesaria ninguna búsqueda en una tabla de métodos porque los tipos de valores no admiten la herencia, por lo que solo hay una posible combinación de método / tipo.

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