Recientemente estuve trabajando en un código que cambió de usar decimales a usar un tipo complejo que tiene el número decimal y un tipo para representar una fracción. Tuve que actualizar algunas pruebas y, mientras escribía, olvidé agregar la nueva palabra clave. El código se compiló, pero la prueba seguía fallando y arrojaba una NullReferenceException. Ahí me di cuenta de que faltaba nueva y que la propiedad no estaba inicializada. ¿Alguien tiene una idea de por qué sucede esto? No pude encontrar nada en la especificación lang de C# que explicara esto.
Aquí está el ejemplo de código:
public class Fraction { public int Numerator { get; set; } public int Denominator { get; set; } } public class MyDecimal { public decimal? Decimal { get; set; } public Fraction Fractional { get; set; } } public class ClassA { public MyDecimal Value { get; set; } } //... var instance = new ClassA { Value = // new MyDecimal is missing here { Decimal = 2.0m, Fractional = new Fraction { Numerator = 3, Denominator = 4 } } }Tenga en cuenta que estoy usando C# 6 y VS 2015, pero también obtengo el mismo resultado en LINQPad.
Si alguien pudiera explicar esto (estoy mirando en tu dirección, Jon Skeet :)), me alegraría.
La Especificación 5.0 de C# define el inicializador de objetos como ( 7.6.10.2 Inicializadores de objetos ):
Un inicializador de objetos especifica valores para cero o más campos o propiedades de un objeto.
object-initializer: { member-initializer-listopt } { member-initializer-list , }
Y después de la explicación detallada, se da un ejemplo que es muy similar a su código:
Si el constructor de Rectangle asigna las dos instancias de Point incrustadas
public class Rectangle { Point p1 = new Point(); Point p2 = new Point(); public Point P1 { get { return p1; } } public Point P2 { get { return p2; } } }la siguiente construcción se puede usar para inicializar las instancias de Point incrustadas en lugar de asignar nuevas instancias:
Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };que tiene el mismo efecto que
Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
Pero solo hay una diferencia, las instancias de Point aquí se inicializan dentro de la clase Rectangle que se produce en el constructor de Rectangle .
Por lo tanto, la sintaxis es válida según la especificación, pero debe asegurarse de que Value se inicialice antes de usar el inicializador de objetos para inicializar sus propiedades a fin de evitar NRE.
Un inicializador de objeto realmente no crea una instancia de sus miembros.
Ver el siguiente código:
var myInstance = new MyInstance { MyMember = new MyMember { Value = 3 }; }Esto compila a:
var myMember= new MyMember(); myMember.Value = 3; var myInstance = new MyInstance(); myInstance.MyMember = myMember; En su caso, olvidó crear una instancia de MyMember , por lo que el inicializador de objetos intenta acceder a esa propiedad y asignarle más valores. Esto se debe al hecho de que los inicializadores de objetos siempre se ejecutan después del constructor apropiado, que no se llamó en su caso. Así que en tu caso se compila a esto:
var myInstance = new MyInstance(); myMymber.Value = 3; Causando una NullReferenceException como myMember nunca fue instanciado.
¿Por qué esto incluso compila? Bueno, asumo que el compilador asume que crea una instancia de MyMember dentro del constructor de MyInstance . No puede saber si realmente hiciste esto.
class Instance { MyMember MyMember = new MyMember(); } Dejar a los miembros null es, por supuesto, absolutamente válido.
La sintaxis del inicializador de objetos le permite inicializar un objeto sin crearlo primero. Esto es bastante importante si desea preservar la identidad del objeto.
Por ejemplo, podría convertir ClassA.Value en una propiedad de solo lectura e inicializarla en el constructor de objetos:
public class ClassA { public ClassA() { Value = new MyDecimal(); } public MyDecimal Value { get; private set; } }Por supuesto, este comportamiento se describe explícitamente en la especificación de C# (extracto de la versión 5):
7.6.10.2 Inicializadores de objetos
Un inicializador de miembro que especifica una expresión después del signo igual se procesa de la misma manera que una asignación (§7.17.1) al campo o propiedad.
Un inicializador de miembro que especifica un inicializador de objeto después del signo igual es un inicializador de objeto anidado, es decir, una inicialización de un objeto incrustado. En lugar de asignar un nuevo valor al campo o la propiedad, las asignaciones en el inicializador de objetos anidados se tratan como asignaciones a miembros del campo o la propiedad. Los inicializadores de objetos anidados no se pueden aplicar a propiedades con un tipo de valor o a campos de solo lectura con un tipo de valor.
Dado que su inicializador de Value es un inicializador anidado, le permite simplemente asignar miembros de Value sin inicializarlo, siempre que Value ya se haya inicializado, por supuesto. El compilador no tiene forma de verificar si Value es null , por lo que no puede darte un error.