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#
.net
clr
value-type
reference-type
Joan Venge
fuente
fuente
System.ValueType
tipo en el sistema de tipo CLR.Respuestas:
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:
Claramente que sí. Podemos ver esto leyendo la primera página de la especificación:
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.
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.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 ellasValueType
. Debido a que estos son obligatorios , el lenguaje C # le prohíbe indicar la relación de derivación en el código.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 .
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.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:
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:
como
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.
fuente
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.fuente
ValueType
es especial, pero vale la pena mencionar explícitamente que enValueType
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).
fuente
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:
fuente
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 esSystem.ValueType
, pero C # ...System.ValueType
), y prohíbe que las clases especifiquen de qué heredanSystem.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.
fuente
Razón fundamental
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.
Prólogo
Esta respuesta se basa en mi propia ingeniería inversa y la especificación CLI.
struct
yclass
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:Una interfaz se define mediante una definición de clase con el
interface
atributo semántico:.class interface MyInterface { }
¿Qué pasa con los tipos de valor?
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 implementaSystem.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 claveextends
yimplements
solo se aplican al tipo encuadrado.Para aclarar, la definición de clase anterior hace 2 cosas:
System.ValueType
e implementando laSystem.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 lasealed
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ónboxed
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
yvaluetype
. Si usamosvaluetype MyNamespace.MyType
, estamos especificando la definición de clase de tipo de valor llamada MyNamespace.MyType. Asimismo, podemos usarclass 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 laboxed
palabra clave anotada por la especificación CLI se implementó realmente, podríamos usarboxed 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 llamadaMyNamespace.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 nombradaMyNamespace.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 llamadaMyNamespace.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
oboxed 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.Resumen
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 deSystem.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 extiendeSystem.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 extiendeSystem.ValueType
, la definición de clase define un tipo de valor y su tipo en caja correspondiente (estructura).Disposición de la memoria
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.
Métodos
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 enSystem.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 laToString()
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 elToString()
método definido en la estructura fuera llamado directamente con el tipo en caja en elthis
puntero, al intentar acceder al campoA
enMyStruct
, 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 reemplazaToString()
. Este método oculto desempaqueta (solo cálculo de direcciones, como launbox
instrucción IL) el tipo en caja y luego llama estáticamente alToString()
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.
Especificación CLI
Boxeo
Definición de tipos de valor
Los tipos de valor no heredan
Los tipos de valor no implementan interfaces
La palabra clave en caja inexistente
Nota: La especificación es incorrecta aquí, no hay una
boxed
palabra clave.Epílogo
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.fuente