¿Por qué no puedo definir un constructor predeterminado para una estructura en .NET?

En .NET, un tipo de valor ( struct C #) no puede tener un constructor sin parámetros. De acuerdo con esta publicación, esto es obligatorio por la especificación CLI. Lo que ocurre es que para cada tipo de valor se crea un constructor predeterminado (¿por el comstackdor?) Que inicializa todos los miembros a cero (o null ).

¿Por qué no se permite definir dicho constructor predeterminado?

Un uso trivial es para números racionales:

 public struct Rational { private long numerator; private long denominator; public Rational(long num, long denom) { /* Todo: Find GCD etc. */ } public Rational(long num) { numerator = num; denominator = 1; } public Rational() // This is not allowed { numerator = 0; denominator = 1; } } 

Usando la versión actual de C #, un Rational predeterminado es 0/0 que no es tan genial.

PD : ¿Los parámetros predeterminados ayudarán a resolver esto para C # 4.0 o se llamará al constructor predeterminado definido por CLR?


Jon Skeet respondió:

Para usar su ejemplo, ¿qué le gustaría que suceda cuando alguien lo hizo?

  Rational[] fractions = new Rational[1000]; 

¿Debe ejecutarse a través de su constructor 1000 veces?

Claro que debería, es por eso que escribí el constructor predeterminado en primer lugar. El CLR debería usar el constructor de puesta a cero predeterminado cuando no se define un constructor predeterminado explícito; de esa manera solo pagas por lo que usas. Entonces, si quiero un contenedor de 1000 Rational no predeterminados (y deseo optimizar las 1000 construcciones) usaré List lugar de una matriz.

Esta razón, en mi opinión, no es lo suficientemente fuerte como para evitar la definición de un constructor predeterminado.

Nota: la respuesta siguiente se escribió mucho antes de C # 6, que está planeando introducir la capacidad de declarar constructores sin parámetros en las estructuras, pero aún no se invocarán en todas las situaciones (por ejemplo, para la creación de un array) (al final esta característica no se agregó a C # 6 ).


EDITAR: He editado la respuesta a continuación debido a la visión de Grauenwolf sobre el CLR.

El CLR permite que los tipos de valor tengan constructores sin parámetros, pero C # no. Creo que esto se debe a que introduciría una expectativa de que se llamaría al constructor cuando no lo haría. Por ejemplo, considera esto:

 MyStruct[] foo = new MyStruct[1000]; 

El CLR puede hacer esto de manera muy eficiente con solo asignar la memoria adecuada y ponerla a cero. Si tuviera que ejecutar el constructor de MyStruct 1000 veces, eso sería mucho menos eficiente. (De hecho, no es así; si tiene un constructor sin parámetros, no se ejecuta cuando crea una matriz, o cuando tiene una variable de instancia no inicializada).

La regla básica en C # es “el valor predeterminado para cualquier tipo no puede depender de ninguna inicialización”. Ahora podrían haber permitido que se definieran constructores sin parámetros, pero luego no se requería que el constructor se ejecutara en todos los casos, pero eso hubiera generado más confusión. (O al menos, así que creo que el argumento va).

EDITAR: Para usar su ejemplo, ¿qué le gustaría que suceda cuando alguien lo hizo?

 Rational[] fractions = new Rational[1000]; 

¿Debe ejecutarse a través de su constructor 1000 veces?

  • Si no, terminamos con 1000 racionales inválidos
  • Si lo hace, posiblemente hayamos desperdiciado una gran cantidad de trabajo si vamos a completar la matriz con valores reales.

EDITAR: (Respondiendo un poco más de la pregunta) El constructor sin parámetros no es creado por el comstackdor. Los tipos de valor no tienen que tener constructores en lo que se refiere al CLR, aunque resulta que sí puede escribirlo en IL. Cuando escribe ” new Guid() ” en C # que emite diferentes IL a lo que obtiene si llama a un constructor normal. Vea esta pregunta ASÍ por un poco más sobre ese aspecto.

Sospecho que no hay ningún tipo de valor en el marco con constructores sin parámetros. Sin duda NDepend podría decirme si lo pregunté lo suficientemente bien … El hecho de que C # lo prohíba es una pista lo suficientemente grande como para que pueda pensar que probablemente sea una mala idea.

Una estructura es un tipo de valor y un tipo de valor debe tener un valor predeterminado tan pronto como se declara.

 MyClass m; MyStruct m2; 

Si declara dos campos como los anteriores sin instanciar tampoco, entonces rompa el depurador, m será nulo pero m2 no. Dado esto, un constructor sin parámetros no tendría sentido, de hecho, todo lo que hace un constructor en una estructura es asignar valores, la cosa misma ya existe simplemente al declararlo. De hecho, m2 podría ser utilizado felizmente en el ejemplo anterior y tener sus métodos llamados, si los hubiera, y sus campos y propiedades manipulados.

Puede crear una propiedad estática que se inicialice y devuelva un número “racional” predeterminado:

 public static Rational One => new Rational(0, 1); 

Y úsalo como:

 var rat = Rational.One; 

Breve explicación:

En C ++, struct y class eran solo dos caras de la misma moneda. La única diferencia real es que uno era público por defecto y el otro era privado.

En .NET , hay una diferencia mucho mayor entre una estructura y una clase. Lo principal es que struct proporciona semántica de tipo de valor, mientras que la clase proporciona semántica de tipo de referencia. Cuando comienzas a pensar en las implicaciones de este cambio, otros cambios comienzan a tener más sentido también, incluido el comportamiento del constructor que describes.

Aunque CLR lo permite, C # no permite que las estructuras tengan un constructor predeterminado sin parámetros. La razón es que, para un tipo de valor, los comstackdores de forma predeterminada no generan un constructor predeterminado ni generan una llamada al constructor predeterminado. Por lo tanto, aunque haya definido un constructor predeterminado, no se invocará, y eso solo lo confundirá.

Para evitar estos problemas, el comstackdor de C # no permite la definición de un constructor predeterminado por parte del usuario. Y debido a que no genera un constructor predeterminado, no puede inicializar los campos al definirlos.

O la gran razón es que una estructura es un tipo de valor y los tipos de valores se inicializan con un valor predeterminado y el constructor se usa para la inicialización.

No tiene que crear una instancia de su estructura con la new palabra clave. En su lugar, funciona como un int; puedes acceder directamente a él.

Structs no puede contener constructores explícitos sin parámetros. Los miembros de Struct se inicializan automáticamente a sus valores predeterminados.

Un constructor predeterminado (sin parámetros) para una estructura podría establecer valores diferentes a los del estado completamente a cero, lo que sería un comportamiento inesperado. El tiempo de ejecución .NET por lo tanto prohíbe los constructores por defecto para una estructura.

Sólo en caso especial. Si ves un numerador de 0 y un denominador de 0, finge que tiene los valores que realmente deseas.

No puede definir un constructor predeterminado porque está usando C #.

Structs puede tener constructores por defecto en .NET, aunque no conozco ningún lenguaje específico que lo admita.

Aquí está mi solución al dilema del constructor no predeterminado. Sé que esta es una solución tardía, pero creo que vale la pena señalar que esta es una solución.

 public struct Point2D { public static Point2D NULL = new Point2D(-1,-1); private int[] Data; public int X { get { return this.Data[ 0 ]; } set { try { this.Data[ 0 ] = value; } catch( Exception ) { this.Data = new int[ 2 ]; } finally { this.Data[ 0 ] = value; } } } public int Z { get { return this.Data[ 1 ]; } set { try { this.Data[ 1 ] = value; } catch( Exception ) { this.Data = new int[ 2 ]; } finally { this.Data[ 1 ] = value; } } } public Point2D( int x , int z ) { this.Data = new int[ 2 ] { x , z }; } public static Point2D operator +( Point2D A , Point2D B ) { return new Point2D( AX + BX , AZ + BZ ); } public static Point2D operator -( Point2D A , Point2D B ) { return new Point2D( AX - BX , AZ - BZ ); } public static Point2D operator *( Point2D A , int B ) { return new Point2D( B * AX , B * AZ ); } public static Point2D operator *( int A , Point2D B ) { return new Point2D( A * BZ , A * BZ ); } public override string ToString() { return string.Format( "({0},{1})" , this.X , this.Z ); } } 

ignorando el hecho de que tengo una estructura estática llamada nula, (Nota: Esto es para todos los cuadrantes positivos solamente), usando get; set; en C #, puede tener una try / catch / finally para tratar los errores donde un tipo de datos particular no es inicializado por el constructor predeterminado Point2D (). Creo que esto es difícil de encontrar para algunas personas en esta respuesta. Eso es principalmente por qué estoy agregando el mío. El uso de la funcionalidad getter y setter en C # le permitirá omitir el descreimiento de este constructor predeterminado y poner a prueba lo que no ha inicializado. Para mí, esto funciona bien, para otra persona es posible que desee agregar algunas declaraciones if. Por lo tanto, en el caso en que desee una configuración Numerador / Denominador, este código podría ayudar. Solo me gustaría reiterar que esta solución no se ve bien, probablemente funcione incluso peor desde el punto de vista de la eficiencia, pero, para alguien que proviene de una versión anterior de C #, el uso de tipos de datos de matriz le brinda esta funcionalidad. Si solo quieres algo que funcione, prueba esto:

 public struct Rational { private long[] Data; public long Numerator { get { try { return this.Data[ 0 ]; } catch( Exception ) { this.Data = new long[ 2 ] { 0 , 1 }; return this.Data[ 0 ]; } } set { try { this.Data[ 0 ] = value; } catch( Exception ) { this.Data = new long[ 2 ] { 0 , 1 }; this.Data[ 0 ] = value; } } } public long Denominator { get { try { return this.Data[ 1 ]; } catch( Exception ) { this.Data = new long[ 2 ] { 0 , 1 }; return this.Data[ 1 ]; } } set { try { this.Data[ 1 ] = value; } catch( Exception ) { this.Data = new long[ 2 ] { 0 , 1 }; this.Data[ 1 ] = value; } } } public Rational( long num , long denom ) { this.Data = new long[ 2 ] { num , denom }; /* Todo: Find GCD etc. */ } public Rational( long num ) { this.Data = new long[ 2 ] { num , 1 }; this.Numerator = num; this.Denominator = 1; } } 

No he visto equivalente a la solución tardía que voy a dar, así que aquí está.

use compensaciones para mover los valores del valor predeterminado 0 a cualquier valor que desee. aquí deben usarse las propiedades en lugar de acceder directamente a los campos. (tal vez con la posible característica c # 7, es mejor definir los campos con ámbito de propiedad para que permanezcan protegidos de ser accedidos directamente en el código).

Esta solución funciona para estructuras simples con solo tipos de valores (sin tipo de ref o struct nullable).

 public struct Tempo { const double DefaultBpm = 120; private double _bpm; // this field must not be modified other than with its property. public double BeatsPerMinute { get => _bpm + DefaultBpm; set => _bpm = value - DefaultBpm; } } 

Esto es diferente a esta respuesta, este enfoque no es una carcasa especial, sino que usa una compensación que funcionará para todos los rangos.

ejemplo con enums como campo.

 public struct Difficaulty { Easy, Medium, Hard } public struct Level { const Difficaulty DefaultLevel = Difficaulty.Medium; private Difficaulty _level; // this field must not be modified other than with its property. public Difficaulty Difficaulty { get => _level + DefaultLevel; set => _level = value - DefaultLevel; } } 

Como dije, este truco puede no funcionar en todos los casos, incluso si struct tiene solo campos de valor, solo usted sabe si funciona en su caso o no. solo examina. pero entiendes la idea general.

 public struct Rational { private long numerator; private long denominator; public Rational(long num = 0, long denom = 1) // This is allowed!!! { numerator = num; denominator = denom; } }