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

261

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

¿Por qué no está permitido definir un 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 el 0/0que 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 sucediera cuando alguien lo hizo:

 Rational[] fractions = new Rational[1000];

¿Debería pasar por su constructor 1000 veces?

Claro que debería, por eso escribí el constructor predeterminado en primer lugar. El CLR debe usar el constructor de puesta a cero predeterminado cuando no se define un constructor predeterminado explícito; de esa manera solo paga por lo que usa. Entonces, si quiero un contenedor de 1000 s no predeterminados Rational(y quiero optimizar las 1000 construcciones), List<Rational>usaré una matriz en 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.

Motti
fuente
3
+1 tuvo un problema similar una vez, finalmente convirtió la estructura en una clase.
Dirk Vollmar
44
Los parámetros predeterminados en C # 4 no pueden ayudar porque Rational()invoca el ctor sin parámetros en lugar del Rational(long num=0, long denom=1).
LaTeX
66
Tenga en cuenta que en C # 6.0, que viene con Visual Studio 2015, se le permitirá escribir constructores de instancias de parámetro cero para estructuras. Entonces new Rational()invocará al constructor si existe, sin embargo, si no existe, new Rational()será equivalente a default(Rational). En cualquier caso, se le recomienda utilizar la sintaxis default(Rational)cuando desee el "valor cero" de su estructura (que es un número "malo" con su diseño propuesto de Rational). El valor predeterminado para un tipo de valor Tes siempre default(T). Por new Rational[1000]lo tanto , nunca invocará constructores de estructuras.
Jeppe Stig Nielsen
66
Para resolver este problema específico, puede almacenarlo denominator - 1dentro de la estructura, de modo que el valor predeterminado se convierta en 0/1
miniBill
3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array. ¿Por qué esperarías que una matriz invoque un constructor diferente a una Lista para una estructura?
mjwills

Respuestas:

197

Nota: la respuesta a continuación se escribió mucho antes de C # 6, que planea introducir la capacidad de declarar constructores sin parámetros en estructuras, pero aún no se invocarán en todas las situaciones (por ejemplo, para la creación de matrices) (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 valores 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, considere esto:

MyStruct[] foo = new MyStruct[1000];

El CLR puede hacer esto de manera muy eficiente simplemente asignando la memoria adecuada y poniendo a cero todo. Si tuviera que ejecutar el constructor MyStruct 1000 veces, sería mucho menos eficiente. (De hecho, no es así - si hacer tener un constructor sin parámetros, que no consigue ejecutar cuando se crea una matriz, o cuando se tiene una variable de instancia sin inicializar.)

La regla básica en C # es "el valor predeterminado para cualquier tipo no puede confiar en ninguna inicialización". Ahora podrían haber permitido que se definieran constructores sin parámetros, pero no hubieran requerido que se ejecutara ese constructor en todos los casos, pero eso habría llevado a una mayor confusión. (O al menos, así que creo que el argumento continúa).

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

Rational[] fractions = new Rational[1000];

¿Debería pasar por su constructor 1000 veces?

  • Si no, terminamos con 1000 racionales no válidos
  • Si lo hace, entonces potencialmente hemos desperdiciado una gran cantidad de trabajo si estamos a punto de completar la matriz con valores reales.

EDITAR: (Respondiendo un poco más de la pregunta) El compilador no crea el constructor sin parámetros. Los tipos de valor no tienen que tener constructores en lo que respecta al CLR, aunque resulta que sí puede si lo escribe en IL. Cuando escribe " new Guid()" en C # que emite IL diferente a lo que obtiene si llama a un constructor normal. Vea esta pregunta SO para obtener un poco más sobre ese aspecto.

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

Jon Skeet
fuente
9
Breve explicación: en C ++, struct y class eran solo dos caras de la misma moneda. La única diferencia real es que una era pública por defecto y la otra era privada. En .Net, hay una diferencia mucho mayor entre una estructura y una clase, y es importante entenderla.
Joel Coehoorn
39
@ Joel: Sin embargo, eso realmente no explica esta restricción en particular, ¿verdad?
Jon Skeet
66
El CLR permite que los tipos de valores tengan constructores sin parámetros. Y sí, lo ejecutará para todos y cada uno de los elementos de una matriz. C # piensa que esta es una mala idea y no lo permite, pero podría escribir un lenguaje .NET que sí lo haga.
Jonathan Allen
2
Lo siento, estoy un poco confundido con lo siguiente. ¿ Rational[] fractions = new Rational[1000];También desperdicia una gran cantidad de trabajo si Rationales una clase en lugar de una estructura? Si es así, ¿por qué las clases tienen un ctor predeterminado?
Bésame la axila el
55
@ FifaEarthCup2014: Tendría que ser más específico sobre lo que quiere decir con "desperdiciar una carga de trabajo". Pero cuando de cualquier manera, no va a llamar al constructor 1000 veces. Si Rationales una clase, terminará con una matriz de 1000 referencias nulas.
Jon Skeet
48

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

MyClass m;
MyStruct m2;

Si declara dos campos como el anterior sin crear ninguna instancia, entonces romper el depurador, mserá nulo pero m2no lo será. Dado esto, un constructor sin parámetros no tendría sentido, de hecho, todo constructor en una estructura hace es asignar valores, la cosa en sí ya existe simplemente declarándolo. ¡De hecho, m2 podría utilizarse con mucho gusto en el ejemplo anterior y tener sus métodos llamados, si los hay, y sus campos y propiedades manipulados!

usuario42467
fuente
3
No estoy seguro de por qué alguien te rechazó. Parece ser la respuesta más correcta aquí.
pipTheGeek
12
El comportamiento en C ++ es que si un tipo tiene un constructor predeterminado, entonces se usa cuando dicho objeto se crea sin un constructor explícito. Esto podría haberse usado en C # para inicializar m2 con el constructor predeterminado, por lo que esta respuesta no es útil.
Motti
3
onester: si no quieres que las estructuras llamen a su propio constructor cuando se declaran, ¡entonces no definas un constructor predeterminado! :) eso es lo que dice Motti
Stefan Monov
8
@Tarik. No estoy de acuerdo. Por el contrario, un constructor sin parámetros tendría mucho sentido: si quiero crear una estructura "Matrix" que siempre tenga una matriz de identidad como valor predeterminado, ¿cómo podría hacerlo por otros medios?
Elo
1
No estoy seguro de estar totalmente de acuerdo con el "Indeed m2 podría ser utilizado con mucho gusto ..." . Puede haber sido cierto en un C # anterior, pero es un error del compilador declarar una estructura, no newIt, luego tratar de usar sus miembros
Caius Jard
18

Aunque el 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 compiladores por defecto no generan un constructor predeterminado ni generan una llamada al constructor predeterminado. Por lo tanto, incluso si definió un constructor predeterminado, no se llamará, y eso solo lo confundirá.

Para evitar tales problemas, el compilador 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 campos al definirlos.

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

No tiene que instanciar su estructura con la newpalabra clave. En cambio, funciona como un int; puedes acceder directamente a él.

Las estructuras no pueden contener constructores explícitos sin parámetros. Los miembros de estructura se inicializan automáticamente a sus valores predeterminados.

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

Adiii
fuente
Esta respuesta es, de lejos, la mejor. Al final, el objetivo de restringir es evitar sorpresas, como MyStruct s;no llamar al constructor predeterminado que proporcionó.
Talles
1
Gracias por la explicación. Por lo tanto, es solo una falta de compilador que debería mejorarse, no hay una buena razón teórica para prohibir a los constructores sin parámetros (tan pronto como puedan restringirse a solo las propiedades de acceso).
Elo
16

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

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

Y úsalo como:

var rat = Rational.One;
AustinWBryan
fuente
24
En este caso, Rational.Zeropodría ser un poco menos confuso.
Kevin
13

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 privado.

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

Joel Coehoorn
fuente
77
Vas a tener que ser un poco más explícito acerca de cómo esto está implícito en el tipo de referencia de valor frente a dividir Yo no lo entiendo ...
Motti
Los tipos de valor tienen un valor predeterminado: no son nulos, incluso si no define un constructor. Si bien a primera vista esto no excluye también la definición de un constructor predeterminado, el marco que usa esta característica interna para hacer ciertas suposiciones sobre las estructuras.
Joel Coehoorn
@annakata: Otros constructores son probablemente útiles en algunos escenarios que involucran Reflexión. Además, si los genéricos alguna vez se mejoraran para permitir una restricción "nueva" parametrizada, sería útil tener estructuras que pudieran cumplir con ellos.
supercat
@annakata Creo que es porque C # tiene un requisito especial que newrealmente debe escribirse para llamar a un constructor. En C ++, los constructores se llaman de forma oculta, en la declaración o instancia de las matrices. En C #, o bien todo es un puntero, así que comienza en nulo, o bien es una estructura y debe comenzar en algo, pero cuando no puede escribir new... (como array init), eso rompería una regla fuerte de C #.
v.oddou
3

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

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

Esta solución funciona para estructuras simples con solo tipos de valor (sin tipo de referencia o estructura anulable).

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 utiliza un desplazamiento que funcionará para todos los rangos.

ejemplo con enumeraciones 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 solo tiene campos de valor, solo usted lo sabe si funciona en su caso o no. solo examina. pero entiendes la idea general.

M.kazem Akhgary
fuente
Esta es una buena solución para el ejemplo que di, pero se suponía que solo era un ejemplo, la pregunta es general.
Motti
2

Solo caso especial. Si ve un numerador de 0 y un denominador de 0, imagine que tiene los valores que realmente desea.

Jonathan Allen
fuente
55
A mí personalmente no me gustaría que mis clases / estructuras tengan este tipo de comportamiento. Fallar en silencio (o recuperarse de la forma en que el desarrollador adivina es lo mejor para usted) es el camino hacia los errores no descubiertos.
Boris Callens
2
+1 Esta es una buena respuesta, porque para los tipos de valor, debe tener en cuenta su valor predeterminado. Esto le permite "establecer" el valor predeterminado con su comportamiento.
IllidanS4 quiere que Mónica regrese el
Así es exactamente como implementan clases como Nullable<T>(por ejemplo int?).
Jonathan Allen
Esa es una muy mala idea. 0/0 siempre debe ser una fracción no válida (NaN). ¿Qué pasa si alguien llama new Rational(x,y)donde x e y resultan ser 0?
Mike Rosoft
Si tiene un constructor real, puede lanzar una excepción, evitando que ocurra un 0/0 real. O si desea que suceda, debe agregar un valor adicional para distinguir entre el valor predeterminado y 0/0.
Jonathan Allen el
2

Lo que uso es el operador de fusión nula (??) combinado con un campo de respaldo como este:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Espero que esto ayude ;)

Nota: la asignación de fusión nula es actualmente una propuesta de función para C # 8.0.

Axel Samyn
fuente
1

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

Las estructuras pueden tener constructores predeterminados en .NET, aunque no conozco ningún lenguaje específico que lo admita.

Jonathan Allen
fuente
En C #, las clases y las estructuras son semánticamente diferentes. Una estructura es un tipo de valor, mientras que una clase es un tipo de referencia.
Tom Sarduy
-1

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 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( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    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 solo para todos los cuadrantes positivos), usando get; set; en C #, puede tener un intento / captura / finalmente, para tratar los errores en los que un tipo de datos en particular no es inicializado por el constructor predeterminado Point2D (). Supongo que esto es esquivo como una solución para algunas personas en esta respuesta. Eso es sobre todo por qué estoy agregando el mío. El uso de la funcionalidad getter y setter en C # le permitirá omitir este constructor predeterminado sin sentido y poner a prueba lo que no ha inicializado. Para mí, esto funciona bien, para alguien más puede que desee agregar algunas declaraciones if. Entonces, en el caso de que desee una configuración de Numerador / Denominador, este código podría ayudar. Solo quisiera reiterar que esta solución no se ve bien, probablemente funciona aún peor desde el punto de vista de la eficiencia, pero, para alguien que viene 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;
    }
}
G1xb17
fuente
2
Este es un código muy malo. ¿Por qué tiene una referencia de matriz en una estructura? ¿Por qué no tienes simplemente las coordenadas X e Y como campos? Y usar excepciones para el control de flujo es una mala idea; generalmente debe escribir su código de tal manera que NullReferenceException nunca ocurra. Si realmente necesita esto, aunque tal construcción sería más adecuada para una clase que para una estructura, entonces debería usar una inicialización diferida. (Y técnicamente, usted está, completamente innecesariamente en todas las configuraciones de una coordenada,
excepto
-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}
eMeL
fuente
55
Está permitido pero no se usa cuando no se especifican parámetros ideone.com/xsLloQ
Motti