¿Por qué las estructuras y las clases separan los conceptos en C #?

44

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 structlos mensajes de correo electrónico como "inmutables" (estableciendo sus campos en readonly) para evitar posibles errores, limitando structla 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 classy structen 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 refy outparece algo bueno.

La pregunta es, ¿por qué classy structse convierten en conceptos separados en C # y el CLR en lugar de un tipo agregada con dos opciones de asignación?

Mentas97
fuente
34
"La solución al problema generalmente aceptada parece declarar todas las estructuras como" inmutables "... limitando la utilidad de las estructuras". Muchas personas argumentan que hacer que algo sea inmutable generalmente lo hace más útil cuando no es la causa de un cuello de botella en el rendimiento. Además, los structs no siempre se almacenan en la pila; considere un objeto con un structcampo. Aparte de eso, como Mason Wheeler mencionó, el problema del corte es probablemente la razón más importante.
Doval
77
No es cierto que C # se haya inspirado en C ++; más bien C # se inspiró en todos los errores (bien intencionados y de buen sonido en ese momento) en el diseño de C ++ y Java.
Pieter Geerkens
18
Nota: stack y heap son detalles de implementación. No hay nada que diga que las instancias de estructura deben asignarse en la pila y las instancias de clase deben asignarse en el montón. Y de hecho, ni siquiera es cierto. Por ejemplo, es muy posible que un compilador pueda determinar, mediante el análisis de escape, que una variable no puede escapar del ámbito local y así asignarla en la pila, incluso si se trata de una instancia de clase. Ni siquiera dice que tiene que haber una pila o un montón en absoluto. Puede asignar marcos de entorno como una lista vinculada en el montón y ni siquiera tener una pila en absoluto.
Jörg W Mittag
3
"pero luego te topas con un método que toma un parámetro de un tipo agregado, y para descubrir si en realidad es de un tipo de valor o de un tipo de referencia, tienes que encontrar la declaración de su tipo" Umm, ¿por qué eso importa exactamente? ? El método le pide que pase un valor. Pasas un valor. ¿En qué punto debe preocuparse si es un tipo de referencia o un tipo de valor?
Luaan

Respuestas:

58

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:: structtipo 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 tipo Deriveda un argumento de tipo de método Base, 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.

Mason Wheeler
fuente
29
Esta es solo otra crítica subjetiva sin sentido en C ++. No puedo votar en contra, pero lo haría si pudiera.
Bartek Banachewicz
17
@MasonWheeler: " es un desastre horrible " suena bastante subjetivo. Esto ya fue discutido en un largo hilo de comentarios a otra respuesta tuya ; el hilo se nukeó (desafortunadamente, porque contenía comentarios útiles aunque en salsa de guerra de fuego) No creo que valga la pena repetir todo aquí, pero "C # lo hizo bien y C ++ lo hizo mal" (que parece ser el mensaje que está tratando de transmitir) es una declaración subjetiva.
Andy Prowl
15
@MasonWheeler: Lo hice en el hilo que se destruyó, y también lo hicieron otras personas, por lo que creo que es lamentable que se haya eliminado. No creo que sea una buena idea replicar ese hilo aquí, pero la versión corta es: en C ++, el usuario del tipo, no su diseñador , decide con qué semántica se debe usar un tipo (semántica de referencia o valor semántica). Esto tiene ventajas y desventajas: te enfureces contra las desventajas sin considerar (¿o saber?) Las ventajas. Por eso el análisis es subjetivo.
Andy Prowl
77
suspiro Creo que la discusión de violación de LSP ya ha tenido lugar. Y creo que la mayoría de la gente estuvo de acuerdo en que la mención de LSP es bastante extraña y no está relacionada, pero no puede verificarlo porque un mod nukeó el hilo de comentarios .
Bartek Banachewicz
99
Si mueve su último párrafo a la parte superior y elimina el primer párrafo actual, creo que tiene el argumento perfecto. Pero el primer párrafo actual es solo subjetivo.
Martin York
19

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 classy structcó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.

Jerry Coffin
fuente
2
Como fanático de C ++, creo que este es un excelente resumen de las diferencias entre C # y la motosierra Swiss Army.
David Thornley
1
@DavidThornley: Al menos intenté escribir lo que pensé que sería una comparación algo equilibrada. No para señalar con el dedo, pero algo de lo que vi cuando escribí esto me pareció ... algo impreciso (por decirlo bien).
Jerry Coffin
7

Esto es de "C #: ¿Por qué necesitamos otro idioma?" - Gunnerson, Eric:

La simplicidad era un objetivo de diseño importante para C #.

Es posible exagerar la simplicidad y la pureza del lenguaje, pero la pureza por el bien de la pureza es de poca utilidad para el programador profesional. Por lo tanto, tratamos de equilibrar nuestro deseo de tener un lenguaje simple y conciso con la solución de los problemas del mundo real que enfrentan los programadores.

[...]

Los tipos de valor , la sobrecarga del operador y las conversiones definidas por el usuario agregan complejidad al lenguaje , pero permiten simplificar enormemente un escenario de usuario importante.

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?

manlio
fuente
1
No sé, ¿tal vez no usar esa semántica de objetos sucios, feos y malos con referencia semántica?
Bartek Banachewicz
Quizás ... soy una causa perdida.
manlio
2
En mi humilde opinión, uno de los mayores defectos en el diseño de Java es la falta de cualquier medio para declarar si una variable se está utilizando para encapsular la identidad o la propiedad , y uno de los mayores defectos en C # es la falta de un medio para distinguir las operaciones en una variable de operaciones en un objeto al que una variable contiene una referencia. Incluso si Runtime no se preocupara por tales distinciones, ser capaz de especificar en un idioma si una variable de tipo 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.
supercat
4

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:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

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 structes más que una agregación de campos públicos y privados pegados con cinta adhesiva.

Super gato
fuente