Mientras programaba en C #, me topé con una extraña decisión de diseño de lenguaje que simplemente no puedo entender.
Entonces, C # (y el CLR) tiene dos tipos de datos agregados: struct
(tipo de valor, almacenado en la pila, sin herencia) y class
(tipo de referencia, almacenado en el montón, tiene herencia).
Esta configuración suena bien al principio, pero luego se topa con un método que toma un parámetro de un tipo agregado y para determinar si en realidad es de un tipo de valor o de un tipo de referencia, debe encontrar la declaración de su tipo. Puede ser realmente confuso a veces.
La solución al problema generalmente aceptada parece declarar todos struct
los mensajes de correo electrónico como "inmutables" (estableciendo sus campos en readonly
) para evitar posibles errores, limitando struct
la utilidad del correo electrónico.
C ++, por ejemplo, emplea un modelo mucho más utilizable: le permite crear una instancia de objeto en la pila o en el montón y pasarla por valor o por referencia (o por puntero). Sigo escuchando que C # se inspiró en C ++, y no puedo entender por qué no adoptó esta técnica. La combinación class
y struct
en una construcción con dos opciones de asignación diferentes (montón y pila) y pasarlos como valores o (explícitamente) como referencias a través de las palabras clave ref
y out
parece algo bueno.
La pregunta es, ¿por qué class
y struct
se convierten en conceptos separados en C # y el CLR en lugar de un tipo agregada con dos opciones de asignación?
fuente
struct
s no siempre se almacenan en la pila; considere un objeto con unstruct
campo. Aparte de eso, como Mason Wheeler mencionó, el problema del corte es probablemente la razón más importante.Respuestas:
La razón por la que C # (y Java y, esencialmente, cualquier otro lenguaje OO desarrollado después de C ++) no copió el modelo de C ++ en este aspecto es porque la forma en que C ++ lo hace es un desastre horrendo.
Identificó correctamente los puntos relevantes anteriores::
struct
tipo de valor, sin herencia.class
: tipo de referencia, tiene herencia. Los tipos de herencia y valor (o más específicamente, polimorfismo y valor de paso) no se mezclan; si pasa un objeto de tipoDerived
a un argumento de tipo de métodoBase
, y luego llama a un método virtual, la única forma de obtener un comportamiento adecuado es asegurarse de que lo que se pasó sea una referencia.Entre eso y todos los otros problemas con los que te encuentras en C ++ al tener objetos heredables como tipos de valor (¡los constructores de copias y la división de objetos vienen a la mente!), La mejor solución es simplemente decir no.
El buen diseño del lenguaje no es solo implementar características, también es saber qué características no implementar, y una de las mejores maneras de hacerlo es aprender de los errores de quienes vinieron antes que usted.
fuente
Por analogía, C # es básicamente como un conjunto de herramientas mecánicas en las que alguien ha leído que generalmente debe evitar los alicates y las llaves ajustables, por lo que no incluye llaves ajustables, y los alicates están bloqueados en un cajón especial marcado como "inseguro" , y solo se puede usar con la aprobación de un supervisor, después de firmar un descargo de responsabilidad que exime a su empleador de cualquier responsabilidad por su salud.
C ++, en comparación, no solo incluye llaves y alicates ajustables, sino también algunas herramientas de propósito especial bastante extrañas cuyo propósito no es evidente de inmediato, y si no conoce la forma correcta de sostenerlas, podrían cortar fácilmente su thumb (pero una vez que comprenda cómo usarlos, puede hacer cosas que son esencialmente imposibles con las herramientas básicas en la caja de herramientas de C #). Además, tiene un torno, una fresadora, una amoladora de superficie, una sierra de cinta para corte de metales, etc., que le permite diseñar y crear herramientas completamente nuevas cada vez que lo necesite (pero sí, esas herramientas de maquinista pueden causar y causarán lesiones graves si no sabe lo que está haciendo con ellas, o incluso si simplemente se descuida).
Eso refleja la diferencia básica en filosofía: C ++ intenta darle todas las herramientas que pueda necesitar para esencialmente cualquier diseño que desee. Casi no intenta controlar cómo usa esas herramientas, por lo que también es fácil usarlas para producir diseños que solo funcionan bien en situaciones raras, así como diseños que probablemente son solo una mala idea y nadie sabe de una situación en la que Es probable que funcionen bien. En particular, gran parte de esto se hace desacoplando las decisiones de diseño, incluso aquellas que en la práctica realmente casi siempre están acopladas. Como resultado, hay una gran diferencia entre simplemente escribir C ++ y escribir bien C ++. Para escribir bien C ++, debe conocer muchas expresiones idiomáticas y reglas generales (incluidas las reglas generales acerca de la seriedad de reconsiderar antes de romper otras reglas generales). Como resultado, C ++ está orientado mucho más a la facilidad de uso (por expertos) que a la facilidad de aprendizaje. También hay (demasiadas) circunstancias en las que tampoco es muy fácil de usar.
C # hace mucho más para tratar de forzar (o al menos sugerir extremadamente ) lo que los diseñadores de lenguaje consideraron buenas prácticas de diseño. Algunas cosas que están desacopladas en C ++ (pero que generalmente van juntas en la práctica) se acoplan directamente en C #. Sí permite que el código "inseguro" empuje un poco los límites, pero honestamente, no mucho.
El resultado es que, por un lado, hay bastantes diseños que se pueden expresar de manera bastante directa en C ++ que son mucho más torpes de expresar en C #. Por otro lado, se trata de un conjunto mucho más fácil de aprender C #, y las posibilidades de producir un diseño realmente horrible que no funcionará para su situación (o probablemente cualquier otro) están drásticamente reducida. En muchos (probablemente incluso en la mayoría de los casos), puede obtener un diseño sólido y viable simplemente "siguiendo la corriente", por así decirlo. O, como a uno de mis amigos (al menos me gusta pensar en él como un amigo, no estoy seguro de si realmente está de acuerdo) le gusta decirlo, C # hace que sea fácil caer en el pozo del éxito.
Entonces, mirando más específicamente la cuestión de cómo
class
ystruct
cómo se encuentran en los dos idiomas: objetos creados en una jerarquía de herencia en la que podría usar un objeto de una clase derivada bajo la apariencia de su clase / interfaz base, está bastante atascado con el hecho de que normalmente necesita hacerlo a través de algún tipo de puntero o referencia: en un nivel concreto, lo que sucede es que el objeto de la clase derivada contiene algo de memoria que puede tratarse como una instancia de la clase base / interfaz, y el objeto derivado se manipula a través de la dirección de esa parte de la memoria.En C ++, depende del programador hacer eso correctamente; cuando usa la herencia, depende de él asegurarse de que (por ejemplo) una función que funciona con clases polimórficas en una jerarquía lo haga mediante un puntero o una referencia a la base clase.
En C #, lo que es fundamentalmente la misma separación entre los tipos es mucho más explícito y el propio lenguaje lo impone. El programador no necesita dar ningún paso para pasar una instancia de una clase por referencia, porque eso sucederá de manera predeterminada.
fuente
Esto es de "C #: ¿Por qué necesitamos otro idioma?" - Gunnerson, Eric:
La semántica de referencia para objetos es una forma de evitar muchos problemas (por supuesto, y no solo el corte de objetos), pero los problemas del mundo real a veces pueden requerir objetos con valor semántico (por ejemplo, eche un vistazo a Sonidos como si nunca debería usar semántica de referencia, ¿verdad? para un punto de vista diferente).
¿Qué mejor enfoque para tomar, por lo tanto, que segregar esos objetos sucios, feos y malos semánticos con valor bajo la etiqueta de
struct
?fuente
int[]
debería ser compartible o modificable (las matrices pueden ser cualquiera, pero generalmente no ambas) ayudaría a hacer que el código incorrecto parezca incorrecto.En lugar de pensar en los tipos de valor derivados
Object
, sería más útil pensar en los tipos de ubicación de almacenamiento existentes en un universo completamente separado de los tipos de instancia de clase, pero para que cada tipo de valor tenga un tipo de objeto de montón correspondiente. Una ubicación de almacenamiento de tipo de estructura simplemente contiene una concatenación de los campos públicos y privados del tipo, y el tipo de montón se genera automáticamente de acuerdo con un patrón como:y para una declaración como: Console.WriteLine ("El valor es {0}", somePoint);
se traducirá como: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("El valor es {0}", box1);
En la práctica, dado que los tipos de ubicación de almacenamiento y los tipos de instancia de almacenamiento dinámico existen en universos separados, no es necesario llamar a los tipos de instancia de almacenamiento dinámico cosas como
boxed_Int32
; ya que el sistema sabría qué contextos requieren la instancia de objeto de almacenamiento dinámico y cuáles requieren una ubicación de almacenamiento.Algunas personas piensan que cualquier tipo de valor que no se comporte como un objeto debería considerarse "malvado". Considero lo contrario: dado que las ubicaciones de almacenamiento de los tipos de valor no son objetos ni referencias a objetos, la expectativa de que deberían comportarse como objetos debería considerarse inútil. En los casos en que una estructura puede comportarse útilmente como un objeto, no hay nada de malo en que uno lo haga, pero cada uno de ellos no
struct
es más que una agregación de campos públicos y privados pegados con cinta adhesiva.fuente