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

43

Mientras programaba en C #, me topé con una decisión de diseño de lenguaje extraño que simplemente no puedo entender.

Entonces, C # (y 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 te topas con un método que toma un parámetro de un tipo agregado, y para averiguar si es realmente de un tipo de valor o de un tipo de referencia, debes encontrar la declaración de su tipo . Puede ser realmente confuso a veces.

La solución generalmente aceptada para el problema parece ser declarar a todos struct s como "inmutables" (configurando sus campos a readonly ) para evitar posibles errores, limitando la utilidad de struct s.

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 simplemente no puedo entender por qué no adoptó esta técnica. Combinar 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 una buena cosa.

La pregunta es, ¿por qué class y struct se convierten en conceptos separados en C # y CLR en lugar de un tipo agregado con dos opciones de asignación?

    
pregunta Mints97 26.02.2015 - 21:39

4 respuestas

58

La razón por la que C # (y Java y esencialmente todos los demás lenguajes OO desarrollados después de C ++) no copió el modelo de C ++ en este aspecto es porque la forma en que C ++ lo hace es un lío terrible.

Has identificado 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, el polimorfismo y el paso por valor) no se mezclan; Si pasa un objeto de tipo Derived a un argumento de método de tipo Base , y luego invoca un método virtual, la única forma de obtener el comportamiento adecuado es asegurarse de que lo que se aprobó fue una referencia.

Entre ese y todos los otros líos con los que se encuentra en C ++ al tener objetos heredables como tipos de valor (copie los constructores y rebanar objetos viene a la mente!) la mejor solución es simplemente decir no.

Un buen diseño de lenguaje no es solo implementar características, también es saber qué características no se deben implementar, y una de las mejores maneras de hacerlo es aprender de los errores de quienes vinieron antes que usted.

    
respondido por el Mason Wheeler 26.02.2015 - 21:49
19

Por analogía, C # es básicamente como un conjunto de herramientas mecánicas en las que alguien ha leído que generalmente se deben evitar los alicates y las llaves ajustables, por lo que no incluye las llaves ajustables, y los alicates están bloqueados en un cajón especial marcado "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 ajustables y alicates, sino también algunas herramientas de uso especial más bien extrañas cuyo propósito no es aparente de inmediato, y si no conoce la forma correcta de sostenerlas, podrían fácilmente corta el pulgar (pero una vez que entiendas cómo usarlos, puedes 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 cortar metales, etc., para permitirle diseñar y crear herramientas completamente nuevas cada vez que sienta la necesidad (pero sí, esas herramientas de maquinista pueden y causarán lesiones graves si no sabes lo que estás haciendo con ellos, o incluso si simplemente te descuidas).

Eso refleja la diferencia básica en filosofía: C ++ intenta proporcionarle todas las herramientas que pueda necesitar para prácticamente 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 al desacoplar 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 solo escribir C ++ y escribir bien C ++. Para escribir bien en C ++, necesitas conocer muchos modismos y reglas generales (incluidas las reglas básicas sobre cómo considerar seriamente antes de romper otras reglas básicas). Como resultado, C ++ se orienta mucho más hacia la facilidad de uso (por parte de expertos) que a la facilidad de aprendizaje. También hay (demasiadas) circunstancias en las que tampoco es realmente fácil de usar.

C # hace mucho más para intentar forzar (o al menos extremadamente sugiere) lo que los diseñadores de idiomas consideraron buenas prácticas de diseño. Algunas cosas que están desacopladas en C ++ (pero generalmente van juntas en la práctica) están directamente acopladas en C #. Permite que el código "inseguro" empuje los límites un poco, pero honestamente, no mucho.

El resultado es que, por un lado, hay bastantes diseños que pueden expresarse bastante directamente en C ++ que son mucho más torpes de expresar en C #. Por otro lado, es mucho más fácil aprender C # entero , y las posibilidades de producir un diseño realmente horrible que no funcione para su situación (o probablemente cualquier otro) son drásticamente reducido. En muchos casos (probablemente incluso en la mayoría), puede obtener un diseño sólido y funcional simplemente "siguiendo el flujo", por así decirlo. O, como a uno de mis amigos (al menos me gusta verlo 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.

Por lo tanto, analizando más específicamente la pregunta de cómo class y struct entendieron cómo están en los dos idiomas: objetos creados en una jerarquía de herencia donde se podría usar un objeto de una clase derivada en la forma de su base clase / interfaz, 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 ser se trata como una instancia de la clase / interfaz base, y el objeto derivado se manipula a través de la dirección de esa parte de la memoria.

En C ++, depende del programador hacerlo 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 a través de un puntero o referencia a la clase base.

En C #, lo que es fundamentalmente la misma separación entre los tipos es mucho más explícito, y lo impone el propio lenguaje. El programador no necesita tomar ningún paso para pasar una instancia de una clase por referencia, porque eso sucederá por defecto.

    
respondido por el Jerry Coffin 27.02.2015 - 17:32
7

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

  

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

     

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

     

[...]

     

Tipos de valor , sobrecarga del operador y conversiones definidas por el usuario, todo agrega   complejidad para el lenguaje , pero permite que un escenario de usuario importante sea   tremendamente simplificado.

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 Parece que nunca debería usar semántica de referencia, ¿no? para un punto de vista diferente).

¿Qué mejor método para tomar, por lo tanto, que segregar esos objetos sucios, feos y malos con valor semántico bajo la etiqueta de struct ?

    
respondido por el manlio 27.02.2015 - 16:09
4

En lugar de pensar en los tipos de valor que se derivan de Object , sería más útil pensar en los tipos de ubicación de almacenamiento que existen en un universo completamente separado de los tipos de instancia de clase, pero que cada tipo de valor tenga un objeto de pila correspondiente tipo. Una ubicación de almacenamiento del tipo de estructura simplemente contiene una concatenación de los campos públicos y privados del tipo, y el tipo de pila se genera automáticamente según 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);

para ser traducido como:     boxed_Point box1 = new boxed_Point (somePoint);     Console.WriteLine ("El valor es {0}", box1);

En la práctica, como los tipos de ubicación de almacenamiento y los tipos de instancia de montón existen en universos separados, no es necesario llamar a los tipos de instancia de montón como boxed_Int32 ; ya que el sistema sabría qué contextos requieren la instancia de objeto de pila y cuáles requieren una ubicación de almacenamiento.

Algunas personas piensan que cualquier tipo de valor que no se comporte como objetos debe considerarse "malo". Tengo la opinión opuesta: dado que las ubicaciones de almacenamiento de los tipos de valor no son objetos ni referencias a objetos, la expectativa de que se comporten como objetos debería considerarse inútil. En los casos en que una estructura puede comportarse de manera útil como un objeto, no hay nada de malo en tener una que lo haga, pero cada struct no es más que un conjunto de campos públicos y privados pegados con cinta adhesiva.

    
respondido por el supercat 27.02.2015 - 19:18

Lea otras preguntas en las etiquetas