Poner una propiedad a readonly no te asegura que sea readonly II
Después de escribir una entrada el otro día sobre propiedades y readonly (Poner una propiedad a readonly no te asegura que sea readonly), y a colación de un interesante comentario de Eduard Tomás que hizo en la entrada que escribí, me he animado a escribir esta segunda entrada para explicar/aclarar algunas cosas que considero interesantes comentar para tenerlas presente y evitar posibles malos entendidos.
En C#, la palabra clave readonly representa un modificador que hace referencia a la funcionalidad de sólo lectura sobre lo que se declara.
Esto es lo que significa readonly en C#.
El problema es que readonly tiene una serie de acepciones sobre las que debemos prestar atención cuando programamos para evitar malos entendidos o efectos «colaterales».
Aunque las comentaba en términos generales en la anterior entrada, es posible que puedan generarse dudas al respecto, así que voy a desgranar un poco más dónde debemos prestar nuestra atención o qué cosas tener en cuenta en este caso.
El modificador readonly indica que la asignación del valor se puede realizar en la propia declaración del campo, o bien, en un constructor de la misma clase.
Una vez que el constructor de la clase finaliza, no se puede cambiar.
Uno puede pensar por lo tanto, que indicar readonly implica que lo que declaramos es de sólo lectura.
El matiz es que siempre tendemos a irnos al dato y no al objeto cuando pensamos en readonly, pero no nos adelantemos aún.
En la entrada anterior que escribí, trabajaba con readonly con tipos valor y tipos referencia.
La diferencia era notable como pudimos ver.
Mientras que en un tipo por valor contiene el valor o dato en sí mismo y por lo tanto su valor es asignado de forma directa e inmutable, en los tipos por referencia contienen una referencia o puntero a su dato.
Sin embargo y quizás este es el punto en el que debemos mostrar mayor atención.
Mientras en un tipo referenica el valor o valores no son inmutables, sí lo es el objeto en sí mismo.
Es decir, no podemos crear una nueva instancia del objeto readonly en un tipo referencia.
Voy a explicar todo esto con algún ejemplo práctico aunque casi se repita con respecto a la anterior entrada que escribí.
La declaración de un tipo valor con el modificador readonly quedaría:
private static readonly int _age = 30; public static void Main(string[] args) { _age = 40; }
En este ejemplo, _age = 40; devolverá un error en tiempo de compilación:
«A static readonly field cannot be assigned to (except in static constructor or a variable initializer)»
Como podemos apreciar, el modificador readonly nos impide cambiar su valor, es decir, su valor es inmutable una vez finalizada la propia declaración del campo o habiendo terminado el constructor de la clase dónde está declarado el campo, que son los únicos sitios dónde podemos asignar un valor a una variable de tipo readonly.
Ahora vamos a trabajar con un tipo referencia para ver el comportamiento, por ejemplo una clase:
private static readonly Person _person = new Person() { Name = "Foo", Age = 30 }; public static void Main(string[] args) { _person.Age = 40; }
En este ejemplo, el valor de la propiedad Age de la clase Person es modificable, porque pese a ser un tipo valor, está contenido dentro de una clase que es un tipo referencia.
Otra cosa diferente es que declaremos la propiedad Age como de sólo lectura con un get únicamente por ejemplo.
Así que la enseñanza que sacamos de este ejemplo es que si blindamos nuestra clase de forma que actúe como clase inmutable, no tendremos problemas.
Ahora bien, lo que decía antes de que no podemos crear una nueva instancia de un objeto readonly se demuestra en el siguiente código:
private static readonly Person _person = new Person() { Name = "Foo", Age = 30 }; public static void Main(string[] args) { _person = new Person() { Name = "Foo", Age = 40 }; }
En este ejemplo, recibiremos el siguiente mensaje de error en tiempo de compilación:
«A static readonly field cannot be assigned to (except in static constructor or a variable initializer)»
¿Este mensaje ya lo hemos visto antes verdad?.
Efectivamente, es el mismo error que nos daba el compilador al modificar el valor de la variable inmutable tipo valor que vimos en el primer ejemplo.
Así que después de todo esto, queda claro que el tipo valor es el que inmutabilidad cubre no sólo a la variable declarada, sino a su propio valor, ya que es él mismo.
Y algo «parecido» ocurre con el tipo referencia que contiene una referencia o puntero a los datos del objeto, lo cual significa que esos datos podrían cambiar si tiene un get por ejemplo o sino es de sólo lectura, pero el objeto en sí, sí es inmutable.
El hecho es que el operador new se encarga de crear una instancia de un tipo, por lo que no podremos realizar la acción que intentábamos hacer en el último código de ejemplo.
Así que el modificador readonly no evita que los datos del objeto puedan ser cambiados, pero sí evita que el campo se reemplace por una refencia diferente al tipo referencia que teníamos originalmente.
Una copia del objeto permite trabajar con una copia del objeto en una referencia diferente sin impactar en el original, evitando que nadie pueda modificar los valores que contienen el objeto original, a no ser que los valores del objeto sean también de tipo readonly.
Ahora bien y ya que estamos hablando de readonly.
Para ampliar un poco todo esto que comento, imaginemos ahora que trabajamos con estructuras.
¿Cuál será su comportamiento?.
Veámoslo en el siguiente código:
public struct DemoStruct { public string Name; public int Age; } ... private static readonly DemoStruct _demoStruct = new DemoStruct() { Name = "Foo", Age = 30 }; public static void Main(string[] args) { _demoStruct.Age = 40; }
Aquí, en tiempo de compilación el compilador nos indicará el siguiente mensaje:
«Fields of static readonly field ‘Program._demoStruct’ cannot be assigned to (except in a static constructor or a variable initializer)»
El comportamiento es el esperado y no tiene mayor secreto, ya que struct es un tipo valor.
Ahora bien, a partir de C# 7.2 y con respecto a las estructuras, podemos utilizar el modificador readonly para «forzar» su comportamiento inmutable además de aportar beneficios de cara al rendimiento y evitar malos usos, o bien forzar el comportamiento que sobre la estructura se planea realizar.
Así que el código de una estructura inmutable a partir de C# 7.2 se podría declarar de esta forma:
public readonly struct DemoStruct { public readonly string Name; public int Age { get; } public DemoStruct(string name, int age) { Name = name; Age = age; } } ... private static readonly DemoStruct _demoStruct = new DemoStruct("Foo", 30); public static void Main(string[] args) { ... }
En esta porción de código (C# 7.2 ó superior) forzamos su inmutabilidad con el uso del modificador readonly en la estructura, lo cuál nos obliga a su vez a que las propiedades o variables que forman parte de la estructura sean de tipo readonly, ya sea de forma directa como Name o bien siendo una propiedad, a través de su get como Age.
Sus valores, en el caso de quererlo, los deberemos asignar en el constructor de la estructura para evitar problemas, con lo que aseguramos su inmutabilidad.
Dicho todo esto, readonly sí indica la inmutabilidad del objeto.
Otra cosa es la inmutabilidad de sus datos o valores.
En el caso de los tipos valor, la inmutabilidad es completa por ser el propio valor en sí el que se almacena.
En el caso de los tipos referencia, la inmutabilidad es parcial implicando al objeto en sí, pero no tiene porqué ser para sus valores a los que hace referencia.
Recuerda por otro lado y como bonustrack, que a partir de C# 7.2 se indicó también la posibilidad de declarar parámetros inmutables en los argumentos de un método o en el propio constructor a través de in.
Por ejemplo, el siguiente código funcionará perfectamente en todas las versiones de C#:
public class Foo { public int Age { get; set; } public Foo(in int age) { Age = age; } } ... public static void Main(string[] args) { var foo = new Foo(10); }
Pero si indicamos dentro del constructor de la clase que declaramos lo siguiente:
public class Foo { public int Age { get; set; } public Foo(in int age) { age = 123; Age = age; } }
El compilador nos indicará el mensaje:
«Cannot assign to variable ‘in int’ because it is a readonly variable»
A no ser como siempre, que pasemos una variable de tipo referencia como por ejemplo una clase en lugar de un tipo valor, con lo que volveríamos a estar como siempre en la misma situación de inmutabilidad (objeto, valor) que comentaba y argumentaba en las dos entradas que sobre este asunto he escrito.
Happy Coding!