C # no permite que las estructuras se deriven de las clases, pero todos los ValueTypes se derivan de Object. ¿Dónde se hace esta distinción?
¿Cómo maneja CLR esto?
C # no permite que las estructuras se deriven de las clases, pero todos los ValueTypes se derivan de Object. ¿Dónde se hace esta distinción?
¿Cómo maneja CLR esto?
Respuestas:
C # no permite que las estructuras se deriven de clases
Su declaración es incorrecta, de ahí su confusión. C # hace permiten estructuras que se derivan de las clases. Todas las estructuras derivan de la misma clase, System.ValueType, que deriva de System.Object. Y todas las enumeraciones se derivan de System.Enum.
ACTUALIZACIÓN: Ha habido cierta confusión en algunos comentarios (ahora eliminados), lo que merece una aclaración. Haré algunas preguntas adicionales:
¿Las estructuras derivan de un tipo base?
Claramente que sí. Podemos ver esto leyendo la primera página de la especificación:
Todos los tipos de C #, incluidos los tipos primitivos como int y double, heredan de un único tipo de objeto raíz.
Ahora, observo que la especificación exagera el caso aquí. Los tipos de puntero no se derivan de un objeto y la relación de derivación para los tipos de interfaz y los tipos de parámetros de tipo es más compleja de lo que indica este esquema. Sin embargo, es evidente que todos los tipos de estructuras derivan de un tipo base.
¿Hay otras formas en las que sabemos que los tipos de estructuras se derivan de un tipo base?
Por supuesto. Un tipo de estructura puede anular ToString
. ¿Qué es lo que prevalece si no es un método virtual de su tipo base? Por lo tanto, debe tener un tipo base. Ese tipo base es una clase.
¿Puedo derivar una estructura definida por el usuario de una clase de mi elección?
Claramente no. Esto no implica que las estructuras no se deriven de una clase . Las estructuras derivan de una clase y, por lo tanto, heredan los miembros hereditarios de esa clase. De hecho, se requiere que las estructuras se deriven de una clase específica: se requieren enumeraciones para derivar de ellas Enum
, se requieren estructuras para derivar de ellas ValueType
. Debido a que estos son obligatorios , el lenguaje C # le prohíbe indicar la relación de derivación en el código.
¿Por qué prohibirlo?
Cuando se requiere una relación , el diseñador del lenguaje tiene opciones: (1) requerir que el usuario escriba el encantamiento requerido, (2) hacerlo opcional o (3) prohibirlo. Cada uno tiene pros y contras, y los diseñadores del lenguaje C # han elegido de manera diferente según los detalles específicos de cada uno.
Por ejemplo, se requiere que los campos const sean estáticos, pero está prohibido decir que lo son porque hacerlo es, en primer lugar, una verborrea sin sentido y, en segundo lugar, implica que hay campos const no estáticos. Pero los operadores sobrecargados deben estar marcados como estáticos, aunque el desarrollador no tiene otra opción; Es demasiado fácil para los desarrolladores creer que una sobrecarga de operador es un método de instancia de otra manera. Esto anula la preocupación de que un usuario pueda llegar a creer que lo "estático" implica que, digamos, "virtual" también es una posibilidad.
En este caso, pedirle a un usuario que diga que su estructura se deriva de ValueType parece un mero exceso de palabrería, e implica que la estructura podría derivar de otro tipo. Para eliminar estos dos problemas, C # hace que sea ilegal indicar en el código que una estructura deriva de un tipo base, aunque claramente lo hace.
De manera similar, todos los tipos de delegados derivan de MulticastDelegate
, pero C # requiere que no digas eso.
Entonces, ahora hemos establecido que todas las estructuras en C # derivan de una clase .
¿Cuál es la relación entre herencia y derivación de una clase ?
Mucha gente está confundida por la relación de herencia en C #. La relación de herencia es bastante sencilla: si una estructura, clase o tipo de delegado D se deriva de una clase de tipo B, los miembros hereditarios de B también son miembros de D. Es tan simple como eso.
¿Qué significa con respecto a la herencia cuando decimos que una estructura se deriva de ValueType? Simplemente que todos los miembros heredables de ValueType también son miembros de la estructura. Así es como las estructuras obtienen su implementación de ToString
, por ejemplo; se hereda de la clase base de la estructura.
¿Todos los miembros heredables? Seguramente no. ¿Los miembros privados son hereditarios?
Si. Todos los miembros privados de una clase base también son miembros del tipo derivado. Por supuesto, es ilegal llamar a esos miembros por su nombre si el sitio de la llamada no está en el dominio de accesibilidad del miembro. ¡El hecho de que tenga un miembro no significa que pueda usarlo!
Ahora continuamos con la respuesta original:
¿Cómo maneja CLR esto?
Extremadamente bien. :-)
Lo que hace que un tipo de valor sea un tipo de valor es que sus instancias se copian por valor . Lo que hace que un tipo de referencia sea un tipo de referencia es que sus instancias se copian por referencia . Parece tener alguna creencia de que la relación de herencia entre los tipos de valor y los tipos de referencia es de alguna manera especial e inusual, pero no entiendo cuál es esa creencia. La herencia no tiene nada que ver con cómo se copian las cosas.
Míralo de esta manera. Suponga que le dije los siguientes hechos:
Hay dos tipos de cajas, cajas rojas y cajas azules.
Cada caja roja está vacía.
Hay tres casillas azules especiales llamadas O, V y E.
O no está dentro de ninguna caja.
V está dentro de O.
E está dentro de V.
No hay otra caja azul dentro de V.
No hay ninguna caja azul dentro de E.
Cada cuadro rojo está en V o E.
Cada caja azul que no sea O está dentro de una caja azul.
Los cuadros azules son tipos de referencia, los cuadros rojos son tipos de valor, O es System.Object, V es System.ValueType, E es System.Enum y la relación "interior" es "deriva de".
Ese es un conjunto de reglas perfectamente coherente y sencillo que podría implementar fácilmente usted mismo, si tuviera mucho cartón y mucha paciencia. Que una caja sea roja o azul no tiene nada que ver con lo que hay dentro; en el mundo real es perfectamente posible poner una caja roja dentro de una caja azul. En CLR, es perfectamente legal crear un tipo de valor que herede de un tipo de referencia, siempre que sea System.ValueType o System.Enum.
Así que reformulemos tu pregunta:
¿Cómo se derivan los ValueTypes de Object (ReferenceType) y siguen siendo ValueTypes?
como
¿Cómo es posible que cada caja roja (tipos de valor) esté dentro (se derive de) la caja O (System.Object), que es una caja azul (un Tipo de referencia) y aún sea una caja roja (un tipo de valor)?
Cuando lo expresas así, espero que sea obvio. No hay nada que le impida poner un cuadro rojo dentro del cuadro V, que está dentro del cuadro O, que es azul. ¿Por qué habría?
UNA ACTUALIZACIÓN ADICIONAL:
La pregunta original de Joan era sobre cómo es posibleque un tipo de valor deriva de un tipo de referencia. Mi respuesta original no explicó realmente ninguno de los mecanismos que utiliza el CLR para dar cuenta del hecho de que tenemos una relación de derivación entre dos cosas que tienen representaciones completamente diferentes, es decir, si los datos a los que se hace referencia tienen un encabezado de objeto, un bloque de sincronización, si posee su propio almacenamiento para la recolección de basura, etc. Estos mecanismos son complicados, demasiado complicados de explicar en una sola respuesta. Las reglas del sistema de tipos CLR son un poco más complejas que el sabor algo simplificado que vemos en C #, donde no se hace una distinción fuerte entre las versiones en caja y sin caja de un tipo, por ejemplo. La introducción de genéricos también provocó que se agregara una gran cantidad de complejidad adicional al CLR.
Pequeña corrección, C # no permite que las estructuras se deriven de forma personalizada de cualquier cosa, no solo de las clases. Todo lo que puede hacer una estructura es implementar una interfaz que es muy diferente a la derivación.
Creo que la mejor forma de responder a esto es que ValueType
es especial. Es esencialmente la clase base para todos los tipos de valor en el sistema de tipos CLR. Es difícil saber cómo responder "cómo maneja el CLR esto" porque es simplemente una regla del CLR.
ValueType
es especial, pero vale la pena mencionar explícitamente que en ValueType
sí mismo es un tipo de referencia.
Esta es una construcción algo artificial mantenida por CLR para permitir que todos los tipos sean tratados como un System.Object.
Los tipos de valor derivan de System.Object a través de System.ValueType , que es donde ocurre el manejo especial (es decir, el CLR maneja el boxeo / unboxing, etc. para cualquier tipo derivado de ValueType).
Su declaración es incorrecta, de ahí su confusión. C # permite que las estructuras se deriven de las clases. Todas las estructuras derivan de la misma clase, System.ValueType
Intentemos esto:
struct MyStruct : System.ValueType
{
}
Esto ni siquiera se compilará. El compilador le recordará que "Escriba 'System.ValueType' en la lista de interfaces no es una interfaz".
Cuando descompile Int32, que es una estructura, encontrará:
public struct Int32: IComparable, IFormattable, IConvertible {}, sin mencionar que se deriva de System.ValueType. Pero en el navegador de objetos, encuentra que Int32 hereda de System.ValueType.
Entonces todo esto me lleva a creer:
Creo que la mejor manera de responder a esto es que ValueType es especial. Es esencialmente la clase base para todos los tipos de valor en el sistema de tipos CLR. Es difícil saber cómo responder "cómo el CLR maneja esto" porque es simplemente una regla del CLR.
ValueType
, la usa para definir dos tipos de objetos: un tipo de objeto de montón que se comporta como un tipo de referencia y un tipo de ubicación de almacenamiento que está efectivamente fuera del sistema de herencia de tipos. Debido a que esos dos tipos de cosas se usan en contextos mutuamente excluyentes, se pueden usar los mismos descriptores de tipo para ambos. En el nivel CLR, una estructura se define como clase cuyo padre es System.ValueType
, pero C # ...
System.ValueType
), y prohíbe que las clases especifiquen de qué heredan System.ValueType
porque cualquier clase que se haya declarado de esa manera se comportaría como un tipo de valor.
Un tipo de valor en caja es efectivamente un tipo de referencia (camina como uno y grazna como uno, por lo que efectivamente es uno). Sugeriría que ValueType no es realmente el tipo base de tipos de valor, sino que es el tipo de referencia base al que se pueden convertir los tipos de valor cuando se convierten en objetos de tipo. Los propios tipos de valores que no están encuadrados están fuera de la jerarquía de objetos.
De todas las respuestas, la respuesta de @ supercat se acerca más a la respuesta real. Dado que las otras respuestas realmente no responden a la pregunta, y francamente hacen afirmaciones incorrectas (por ejemplo, que los tipos de valores heredan de cualquier cosa), decidí responder la pregunta.
Esta respuesta se basa en mi propia ingeniería inversa y la especificación CLI.
struct
y class
son palabras clave de C #. En lo que respecta a la CLI, todos los tipos (clases, interfaces, estructuras, etc.) se definen mediante definiciones de clase.
Por ejemplo, un tipo de objeto (conocido en C # como class
) se define de la siguiente manera:
.class MyClass
{
}
Una interfaz se define mediante una definición de clase con el interface
atributo semántico:
.class interface MyInterface
{
}
La razón por la que las estructuras pueden heredar System.ValueType
y seguir siendo tipos de valor es porque ... no lo hacen.
Los tipos de valor son estructuras de datos simples. Los tipos de valor no heredan de nada y no pueden implementar interfaces. Los tipos de valor no son subtipos de ningún tipo y no tienen ninguna información de tipo. Dada una dirección de memoria de un tipo de valor, no es posible identificar lo que representa el tipo de valor, a diferencia de un tipo de referencia que tiene información de tipo en un campo oculto.
Si imaginamos la siguiente estructura C #:
namespace MyNamespace
{
struct MyValueType : ICloneable
{
public int A;
public int B;
public int C;
public object Clone()
{
// body omitted
}
}
}
La siguiente es la definición de clase IL de esa estructura:
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable
{
.field public int32 A;
.field public int32 B;
.field public int32 C;
.method public final hidebysig newslot virtual instance object Clone() cil managed
{
// body omitted
}
}
Entonces, ¿qué está pasando aquí? Se extiende claramente System.ValueType
, que es un tipo de objeto / referencia, e implementa System.ICloneable
.
La explicación es que cuando la definición de una clase se extiende, System.ValueType
en realidad define 2 cosas: un tipo de valor y el tipo en caja correspondiente del tipo de valor. Los miembros de la definición de clase definen la representación tanto para el tipo de valor como para el tipo en caja correspondiente. No es el tipo de valor que se extiende e implementa, es el tipo en caja correspondiente el que lo hace. Las palabras clave extends
y implements
solo se aplican al tipo encuadrado.
Para aclarar, la definición de clase anterior hace 2 cosas:
System.ValueType
e implementando la System.ICloneable
interfaz.Tenga en cuenta también que cualquier extensión de definición de clase System.ValueType
también está intrínsecamente sellada, ya sea que la sealed
palabra clave esté especificada o no.
Dado que los tipos de valor son solo estructuras simples, no heredan, no implementan y no admiten polimorfismo, no se pueden usar con el resto del sistema de tipos. Para solucionar esto, además del tipo de valor, el CLR también define un tipo de referencia correspondiente con los mismos campos, conocido como el tipo en caja. Entonces, si bien un tipo de valor no se puede pasar a métodos que toman un object
, su tipo en caja correspondiente sí .
Ahora, si tuviera que definir un método en C # como
public static void BlaBla(MyNamespace.MyValueType x)
,
sabes que el método tomará el tipo de valor MyNamespace.MyValueType
.
Arriba, aprendimos que la definición de clase que resulta de la struct
palabra clave en C # en realidad define tanto un tipo de valor como un tipo de objeto. Sin embargo, solo podemos hacer referencia al tipo de valor definido. Aunque la especificación CLI establece que la palabra clave de restricción boxed
se puede utilizar para hacer referencia a una versión en caja de un tipo, esta palabra clave no existe (consulte ECMA-335, II.13.1 Tipos de valores de referencia). Pero imaginemos que lo hace por un momento.
Cuando se hace referencia a tipos en IL, se admiten un par de restricciones, entre las que se encuentran class
y valuetype
. Si usamos valuetype MyNamespace.MyType
, estamos especificando la definición de clase de tipo de valor llamada MyNamespace.MyType. Asimismo, podemos usar class MyNamespace.MyType
para especificar la definición de clase de tipo de objeto llamada MyNamespace.MyType. Lo que significa que en IL puede tener un tipo de valor (estructura) y un tipo de objeto (clase) con el mismo nombre y aún distinguirlos. Ahora, si la boxed
palabra clave anotada por la especificación CLI se implementó realmente, podríamos usar boxed MyNamespace.MyType
para especificar el tipo en caja de la definición de clase de tipo de valor llamada MyNamespace.MyType.
Entonces, .method static void Print(valuetype MyNamespace.MyType test) cil managed
toma el tipo de valor definido por una definición de clase de tipo de valor llamada MyNamespace.MyType
,
while .method static void Print(class MyNamespace.MyType test) cil managed
toma el tipo de objeto definido por la definición de clase de tipo de objeto nombrada MyNamespace.MyType
.
de la misma forma, si boxed
fuera una palabra clave, .method static void Print(boxed MyNamespace.MyType test) cil managed
tomaría el tipo en caja del tipo de valor definido por una definición de clase llamada MyNamespace.MyType
.
Entonces podrá crear una instancia del tipo en caja como cualquier otro tipo de objeto y pasarlo a cualquier método que tome un System.ValueType
, object
o boxed MyNamespace.MyValueType
como argumento, y funcionaría, para todos los efectos, como cualquier otro tipo de referencia. NO es un tipo de valor, sino el tipo en caja correspondiente de un tipo de valor.
Entonces, en resumen, y para responder a la pregunta:
Los tipos de valor no son tipos de referencia y no heredan de System.ValueType
ningún otro tipo, y no pueden implementar interfaces. Los correspondientes recuadros tipos que están también definidas hacen hereda de System.ValueType
y pueden implementar interfaces.
Una .class
definición define diferentes cosas según las circunstancias.
interface
se especifica el atributo semántico, la definición de clase define una interfaz.interface
no se especifica el atributo semántico y la definición no se extiende System.ValueType
, la definición de clase define un tipo de objeto (clase).interface
no se especifica el atributo semántico, y la definición se extiende System.ValueType
, la definición de clase define un tipo de valor y su tipo en caja correspondiente (estructura).Esta sección asume un proceso de 32 bits
Como ya se mencionó, los tipos de valor no tienen información de tipo y, por lo tanto, no es posible identificar qué representa un tipo de valor desde su ubicación de memoria. Una estructura describe un tipo de datos simple y contiene solo los campos que define:
public struct MyStruct
{
public int A;
public short B;
public int C;
}
Si imaginamos que se asignó una instancia de MyStruct en la dirección 0x1000, este es el diseño de la memoria:
0x1000: int A;
0x1004: short B;
0x1006: 2 byte padding
0x1008: int C;
Las estructuras tienen por defecto el diseño secuencial. Los campos están alineados en límites de su propio tamaño. Se agrega relleno para satisfacer esto.
Si definimos una clase exactamente de la misma manera, como:
public class MyClass
{
public int A;
public short B;
public int C;
}
Imaginando la misma dirección, el diseño de la memoria es el siguiente:
0x1000: Pointer to object header
0x1004: int A;
0x1008: int C;
0x100C: short B;
0x100E: 2 byte padding
0x1010: 4 bytes extra
Las clases tienen por defecto el diseño automático, y el compilador JIT las organizará en el orden más óptimo. Los campos están alineados en límites de su propio tamaño. Se agrega relleno para satisfacer esto. No estoy seguro de por qué, pero cada clase siempre tiene 4 bytes adicionales al final.
El desplazamiento 0 contiene la dirección del encabezado del objeto, que contiene información de tipo, la tabla de método virtual, etc. Esto permite que el tiempo de ejecución identifique lo que representan los datos en una dirección, a diferencia de los tipos de valor.
Por tanto, los tipos de valor no admiten herencia, interfaces ni polimorfismo.
Los tipos de valor no tienen tablas de métodos virtuales y, por lo tanto, no admiten polimorfismo. Sin embargo , su tipo en caja correspondiente sí lo hace .
Cuando tiene una instancia de una estructura e intenta llamar a un método virtual como se ToString()
define en System.Object
, el tiempo de ejecución tiene que enmarcar la estructura.
MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
Sin embargo, si la estructura se anula ToString()
, la llamada se vinculará estáticamente y el tiempo de ejecución llamará MyStruct.ToString()
sin boxing y sin buscar en ninguna tabla de métodos virtuales (las estructuras no tienen ninguna). Por esta razón, también puede integrar la ToString()
llamada.
Si la estructura se anula ToString()
y está encuadrada, la llamada se resolverá utilizando la tabla de métodos virtuales.
System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct.
Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
Sin embargo, recuerde que ToString()
está definido en la estructura y, por lo tanto, opera sobre el valor de la estructura, por lo que espera un tipo de valor. El tipo en caja, como cualquier otra clase, tiene un encabezado de objeto. Si el ToString()
método definido en la estructura fuera llamado directamente con el tipo en caja en el this
puntero, al intentar acceder al campo A
en MyStruct
, accedería al desplazamiento 0, que en el tipo en caja sería el puntero del encabezado del objeto. Por lo tanto, el tipo en caja tiene un método oculto que reemplaza ToString()
. Este método oculto desempaqueta (solo cálculo de direcciones, como la unbox
instrucción IL) el tipo en caja y luego llama estáticamente al ToString()
definido en la estructura.
Asimismo, el tipo en caja tiene un método oculto para cada método de interfaz implementado que hace el mismo unboxing y luego llama estáticamente al método definido en la estructura.
I.8.2.4 Para cada tipo de valor, el CTS define un tipo de referencia correspondiente denominado tipo en caja. Lo contrario no es cierto: en general, los tipos de referencia no tienen un tipo de valor correspondiente. La representación de un valor de un tipo en caja (un valor en caja) es una ubicación donde se puede almacenar un valor del tipo de valor. Un tipo en caja es un tipo de objeto y un valor en caja es un objeto.
I.8.9.7 No todos los tipos definidos por una definición de clase son tipos de objeto (véase §I.8.2.3); en particular, los tipos de valor no son tipos de objetos, pero se definen mediante una definición de clase. Una definición de clase para un tipo de valor define tanto el tipo de valor (sin caja) como el tipo de caja asociado (ver §I.8.2.4). Los miembros de la definición de clase definen la representación de ambos.
II.10.1.3 Los atributos semánticos de tipo especifican si se debe definir una interfaz, clase o tipo de valor. El atributo de interfaz especifica una interfaz. Si este atributo no está presente y la definición amplía (directa o indirectamente) System.ValueType, y la definición no es para System.Enum, se definirá un tipo de valor (§II.13). De lo contrario, se definirá una clase (§II.11).
I.8.9.10 En su forma sin caja, los tipos de valor no heredan de ningún tipo. Los tipos de valor encuadrados heredarán directamente de System.ValueType a menos que sean enumeraciones, en cuyo caso, heredarán de System.Enum. Los tipos de valor en caja deben estar sellados.
II.13 Los tipos de valor sin caja no se consideran subtipos de otro tipo y no es válido utilizar la instrucción isinst (ver Partición III) en tipos de valor sin caja. Sin embargo, la instrucción isinst se puede utilizar para tipos de valores en caja.
I.8.9.10 Un tipo de valor no se hereda; más bien, el tipo base especificado en la definición de clase define el tipo base del tipo en caja.
I.8.9.7 Los tipos de valor no admiten contratos de interfaz, pero sí los tipos encuadrados asociados.
II.13 Los tipos de valor implementarán cero o más interfaces, pero esto solo tiene significado en su forma encuadrada (§II.13.3).
I.8.2.4 Las interfaces y la herencia se definen únicamente en tipos de referencia. Por lo tanto, mientras que una definición de tipo de valor (§I.8.9.7) puede especificar ambas interfaces que serán implementadas por el tipo de valor y la clase (System.ValueType o System.Enum) de la que hereda, estos se aplican solo a valores en caja. .
II.13.1 Se hará referencia a la forma sin caja de un tipo de valor utilizando la palabra clave valuetype seguida de una referencia de tipo. Se hará referencia a la forma encuadrada de un tipo de valor mediante la palabra clave encuadrada seguida de una referencia de tipo.
Nota: La especificación es incorrecta aquí, no hay una boxed
palabra clave.
Creo que parte de la confusión de cómo los tipos de valor parecen heredar, se debe al hecho de que C # usa la sintaxis de conversión para realizar boxeo y unboxing, lo que hace que parezca que estás realizando casts, lo cual no es realmente el caso (aunque, el CLR lanzará una InvalidCastException si intenta desempaquetar el tipo incorrecto).
(object)myStruct
en C # crea una nueva instancia del tipo en caja del tipo de valor; no realiza ningún molde. Asimismo, (MyStruct)obj
en C # desempaqueta un tipo en caja, copiando la parte del valor; no realiza ningún molde.
System.ValueType
tipo en el sistema de tipo CLR.