c# - intuicion - Comportamiento no intuitivo con estructura inicialización y argumentos predeterminados
diseño intuitivo definicion (5)
public struct Test
{
public double Val;
public Test(double val = double.NaN) { Val = val; }
public bool IsValid { get { return !double.IsNaN(Val); } }
}
Test myTest = new Test();
bool valid = myTest.IsValid;
Lo anterior da valid==true
porque el constructor con el argumento predeterminado NO se llama y el objeto se crea con el valor predeterminado estándar val = 0.0.
Si la estructura es una clase, el comportamiento es valid==false
que es lo que yo esperaría.
Considero que esta diferencia en el comportamiento y en particular el comportamiento en el caso de la estructura es sorprendente y poco intuitivo: ¿qué sucede? ¿Qué sirve el argumento por defecto en la construcción de la estructura? Si es inútil, ¿para qué compilar esto?
Actualización: Aclarar el enfoque aquí no es sobre cuál es el comportamiento, sino por qué compila esto sin previo aviso y se comporta de manera no intuitiva. Es decir, si el argumento predeterminado no se aplica porque en el nuevo caso Test () no se llama al constructor, ¿por qué dejar que se compile?
Considero que esta diferencia en el comportamiento y en particular el comportamiento en el caso de la estructura es sorprendente y poco intuitivo. ¿Qué está pasando? ¿Qué sirve el argumento por defecto en la construcción de la estructura? Si es inútil, ¿para qué compilar esto?
No sirve para nada. El código IL emitido no generará una llamada al constructor con el parámetro predeterminado, sino que llamará el default(Test)
. Parece totalmente razonable que el compilador emita una advertencia que diga que el constructor no será invocado (aunque eso es un detalle de la implementación). Presento un problema en http://connect.microsoft.com
Si miramos el código IL generado para:
Test myTest = new Test();
bool valid = myTest.IsValid;
Ya veremos:
IL_0000: ldloca.s 00 // myTest
IL_0002: initobj UserQuery.Test // default(Test);
IL_0008: ldloca.s 00 // myTest
IL_000A: call UserQuery+Test.get_IsValid
Tenga en cuenta que la llamada realizada en IL no es una llamada de método al constructor (que se vería como: call Test..ctor
), generó una llamada a initobj
:
Inicializa cada campo del tipo de valor en una dirección específica a una referencia nula o un 0 del tipo primitivo apropiado. A diferencia de Newobj , initobj no llama al método constructor. Initobj está diseñado para inicializar tipos de valor, mientras que newobj se utiliza para asignar e inicializar objetos.
Lo que significa que el compilador simplemente está haciendo caso omiso del constructor con los parámetros predeterminados, ya que hasta C # -6.0 está prohibido declarar tal constructor.
@JonSkeet toma esto con gran profundidad en su respuesta a ¿ Usar "nuevo" en una estructura lo asigna en el montón o pila?
Editar :
De hecho, le hice una pregunta a Mads Torgerson sobre el nuevo uso del constructor sin parámetros en C # -6.0 que creo que está relacionado, y dijo:
@Yuval y otros, con respecto a constructores sin parámetros en estructuras: la cosa a tener en cuenta es que, antes y ahora, los constructores no necesariamente se ejecutan en estructuras. Todo lo que hicimos fue agregar la capacidad de tener un constructor sin parámetros que tampoco se puede garantizar que se ejecute . No hay una manera razonable de que las estructuras que se han garantizado se hayan inicializado, y los constructores sin parámetros no ayudan con eso.
Lo que ayuda a los constructores sin parámetros es permitirte tener un constructor sin parámetros.
Creo que una fuente principal de confusión es que ''new S ()'' puede significar ''default (S)''. Ese es un error histórico en el idioma y me gustaría mucho poder quitarlo. Rechazo encarecidamente a cualquiera que use "new S ()" en una estructura que no tenga un constructor sin parámetros . Por lo que puedo decir, esto se debe a que la sintaxis predeterminada (S) no existía en C # 1.0, por lo que esta fue solo la sintaxis utilizada para obtener el valor predeterminado de una estructura.
Es cierto que la implementación del compilador hasta ahora ha "optimizado" ''new T ()'' para significar esencialmente por defecto (T) cuando T es una estructura. En realidad, se trataba de un error (siempre se suponía que se llamaba a un constructor real sin parámetros si existe), que podría haber existido desde el principio, ya que está permitido en IL. Estamos arreglando esto, por lo que llamaremos al constructor incluso en el caso genérico.
Por lo tanto, la semántica es limpia: la nueva S () es la única forma de ejecutar un constructor sin parámetros en una estructura, y siempre ejecuta ese constructor, incluso a través de los genéricos.
En C # (al menos hasta C # 6 - ver publicación de blog ), invocar new Test()
es equivalente a escribir default(Test)
: no se llama a ningún constructor, se proporciona el valor predeterminado.
El argumento predeterminado no sirve para nada, lo que sucede es que es probable que sea el resultado de una supervisión en la implementación del compilador, debido al hecho de que los argumentos opcionales solo se agregaron en C # 4:
- El código que verifica que los argumentos opcionales no entren en conflicto con las sobrecargas ya existentes no tiene conocimiento de un posible conflicto con el inicializador en el caso de estructuras;
El código que traduce lo que significa
new Test()
probablemente desconoce la existencia de argumentos opcionales;Después de profundizar en los comentarios, noté la siguiente gema de Mads Torgersen :
Es cierto que la implementación del compilador hasta ahora ha "optimizado" ''new T ()'' para significar esencialmente por defecto (T) cuando T es una estructura. En realidad, se trataba de un error (siempre se suponía que se llamaba a un constructor real sin parámetros si existe), que podría haber existido desde el principio, ya que está permitido en IL.
Para su ejemplo, significa que el compilador reemplaza efectivamente a
new Test()
aldefault(Test)
, por lo que es un error que se solucionará en la próxima versión de Visual Studio.
En otras palabras, tienes un caso de esquina. Probablemente sea un buen momento para ver cómo se comporta en la próxima versión de Visual Studio, ya que ese comportamiento está cambiando.
Para todos los tipos de valor T
, la new T()
y la default(T)
son equivalentes. No llaman a ningún constructor, simplemente establecen todos los campos en cero. Esta es también la razón por la que C # no le permite escribir un constructor sin parámetros: public Test() { Val = double.NaN; }
public Test() { Val = double.NaN; }
no se compilaría, porque no habría manera de usar ese constructor.
Has encontrado un caso de esquina. Su constructor parece que se usaría para la new T()
. Dado que su tipo sigue siendo un tipo de valor, no se usa. Como se puede llamar a su constructor, no se emite ningún error.
Porque una estructura no puede tener un constructor sin parámetros definido por el usuario.
Test(double val = double.NaN)
parece uno, pero en realidad se compila como Test(double val)
con algunos metadatos sobre el valor predeterminado.
Sospecho que esto se debe a que un constructor predeterminado sin parámetros no está permitido en c #, por lo que cuando llama al constructor Test sin parámetros, simplemente los inicializa de la forma habitual. Verifique esta publicación para obtener más detalles (no es un duplicado): ¿Por qué no puedo definir un constructor predeterminado para una estructura en .NET?