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?
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?
Respuestas:
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 FooType
es 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 FooType
cambio, 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 Base
y Derived
son 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*=2
sería realmente estar modificando values[0].B
!
Intenta depurar ESO !
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?
Foo
herencia de estructura Bar
no debe permitir Foo
que 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 Bar
como el primer elemento en Foo
e Foo
incluir nombres de los miembros que alias a aquellos miembros en Bar
, lo que permite código que habían usado Bar
para adaptarse al uso de una Foo
vez, sin tener que reemplazar todas las referencias a thing.BarMember
con thing.theBar.BarMember
, y retener la capacidad de leer y escribir todos Bar
's campos como grupo; ...
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.
Foo
que tenga un campo de tipo estructura Bar
capaz de considerar a Bar
los miembros como propios, de modo que una Point3d
clase podría, por ejemplo, encapsular a Point2d xy
pero referirse a X
ese campo como xy.X
o X
.
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 struct
que 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.
Point3D
de a Point2D
; no sería capaz de usar un en Point3D
lugar de un Point2D
, pero no Point3D
class
sobre struct
su caso.
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.
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. :-)
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.
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í.
Las estructuras admiten interfaces, por lo que puede hacer algunas cosas polimórficas de esa manera.
IL es un lenguaje basado en pila, por lo que llamar a un método con un argumento es algo así:
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.