Constructores primarios
Cuando defines un registro con una lista de parámetros, el compilador genera automáticamente declaraciones de propiedades, así como un constructor primario (y un deconstructor). Como hemos visto, esto funciona bien en casos sencillos, y en casos más complejos puedes omitir la lista de parámetros y escribir manualmente las declaraciones de propiedades y el constructor.
C# también ofrece una opción intermedia medianamente útil -si estás dispuesto a lidiar con la curiosa semántica de los constructores primarios- que consiste en definir una lista de parámetros mientras escribes tú mismo algunas o todas las declaraciones de propiedades:
record Student (string ID, string LastName, string GivenName) { public string ID { get; } = ID; }
En este caso, nos "apoderamos" de la definición de la propiedad ID
, definiéndola como de sólo lectura (en lugar de sólo init), lo que impide que participe en la mutación no destructiva. Si nunca necesitas mutar de forma no destructiva una propiedad concreta, hacerla de sólo lectura te permite almacenar datos calculados en el registro sin tener que codificar un mecanismo de actualización.
Observa que hemos tenido que incluir un inicializador de propiedades (en negrita):
public string ID { get; } = ID;
Cuando "asumes" la declaración de una propiedad, te haces responsable de inicializar su valor; el constructor primario ya no lo hace automáticamente. (Esto coincide exactamente con el comportamiento al definir constructores primarios en clases o structs). Observa también que el ID
en negrita se refiere al parámetro del constructor primario, no a la propiedad ID
.
Nota
Con los structs de registro, es legal redefinir una propiedad como campo:
record struct Student (string ID) { public string ID = ID; }
De acuerdo con la semántica de los constructores primarios en clases y structs (véase "Constructores primarios"), los parámetros del constructor primario (ID
, LastName
, y GivenName
en este caso) son mágicamente visibles para todos los inicializadores de campos y propiedades. Podemos ilustrarlo ampliando nuestro ejemplo como sigue:
record Student (string ID, string LastName, string FirstName) { public string ID { get; } = ID; readonly int _enrollmentYear = int.Parse (ID.Substring (0, 4)); }
De nuevo, el ID
en negrita se refiere al parámetro principal del constructor, no a la propiedad. (La razón de que no haya ambigüedad es que es ilegal acceder a propiedades desde inicializadores).
En este ejemplo, calculamos _enrollmentYear
a partir de los cuatro primeros dígitos de ID
. Aunque es seguro almacenarlo en un campo de sólo lectura (porque la propiedad ID
es de sólo lectura y, por tanto, no se puede mutar de forma no destructiva), este código no funcionaría tan bien en el mundo real. La razón es que, sin un constructor explícito, no hay un lugar central en el que validar ID
y lanzar una excepción significativa en caso de que no sea válido (un requisito habitual).
La validación también es una buena razón para necesitar escribir accesores explícitos de sólo init (como ya comentamos en "Validación de propiedades"). Por desgracia, los constructores primarios no funcionan bien en este escenario. Para ilustrarlo, considera el siguiente registro, en el que un accesor init
realiza una comprobación de validación de nulos:
record Person (string Name) { string _name = Name; public string Name { get => _name; init => _name = value ?? throw new ArgumentNullException ("Name"); } }
Como Name
no es una propiedad automática, no puede definir un inicializador. Lo mejor que podemos hacer es poner el inicializador en el campo de respaldo (en negrita). Desgraciadamente, al hacerlo se omite la comprobación de nulos:
var p = new Person (null); // Succeeds! (bypasses the null check)
La dificultad es que no hay forma de asignar un parámetro constructor primario a una propiedad sin escribir el constructor nosotros mismos. Aunque hay soluciones (como factorizar la lógica de validación de init
en un método estático independiente que llamamos dos veces), la solución más sencilla es evitar la lista de parámetros y escribir manualmente un constructor ordinario (y un deconstructor, si lo necesitas):
record Person { public Person (string name) => Name = name; // Assign to *PROPERTY* string _name; public string Name { get => _name; init => ... } }