Poner una propiedad a readonly no te asegura que sea readonly
El título de esta entrada tiene un poco de truco, no lo voy a negar, pero no es menos cierto que tiene una buena parte de realidad.
Tampoco es una entrada pensada para los programadores más experimentados de C# que ya conocen esto que voy a contar, pero sí viene bien para recordar algunas cosas y sobre todo para aquellos programadores de .Net que tengan menos experiencia.
Cuando ponemos una propiedad a readonly creemos bien que su contenido no va a ser modificable, pero no a veces no es así.
Imaginemos el siguiente código:
public class MyClass { private int _myProperty = 0; public int MyProperty { get { return _myProperty; } } }
Esta propiedad se comportará como una propiedad de sólo lectura, por lo que si en nuestro código escribimos algo así:
var myClass = new MyClass(); myClass.MyProperty = 1;
El compilador no nos dejará continuar informándonos que MyProperty es de sólo lectura.
Bien, esto es lo que esperamos, sin embargo vamos a modificar un poco el código.
Voy a crear ahora un par de clases cuyo código será el siguiente:
public class Person { public string Name { get; set; } public int Age { get; set; } } public class People { private List<Person> _people = new List<Person>(); public List<Person> GetPeople { get { return _people; } } }
Nuestra propiedad de sólo lectura es en este caso GetPeople en lugar de MyProperty.
Su tipo es List<Person> en lugar de int.
Ahora bien, si escribimos esto en nuestro código:
var people = new People(); var myPeople = people.GetPeople; myPeople.Add(new Person() { Name = "Pedro", Age = 20 });
El compilador no nos indicará ningún problema, es más, podremos ejecutar nuestra aplicación sin problemas también y podremos agregar el elemento (Pedro, 20) sobre la propiedad de sólo lectura.
Esto no es en realidad ningún truco ni mal funcionamiento, es única y exclusivamente una mala praxis.
Hemos declarado una propiedad de tipo List<Person> como de sólo lectura sin tener en cuenta que una clase (como Person) es un tipo por referencia, es decir, estamos apuntando con el puntero a la misma clase que creamos.
Es decir, pese a haberla declarado como sólo lectura, en realidad no es de sólo lectura y se está comportando de forma correcta.
Nuestro error aquí es no haber tenido en cuenta este detalle.
¿Hay alguna forma de resolverlo?.
Sí.
Nuestra propiedad debería devolver una copia de la colección en lugar de un puntero a la colección que es lo que estaba realizando antes.
Este código resolvería el problema:
public class People { private List<Person> _people = new List<Person>(); public List<Person> GetPeople { get { return new List<Person>(_people); } } }
Ahora, cuando escribamos el mismo código que vimos antes:
var people = new People(); var myPeople = people.GetPeople; myPeople.Add(new Person() { Name = "Pedro", Age = 20 });
Nuestra aplicación creará un nuevo objeto (copia de la colección) y sobre él y de forma independiente podremos modificar sus elementos a nuestro antojo, pero nuestra propiedad se habrá comportado como una propiedad de sólo lectura realmente.
Ahora bien y para completar la explicación, modifiquemos ahora el código del primer ejemplo de MyProperty.
Voy a agregar sus elementos como colección List.
El código quedaría de esta forma:
public class MyOtherClass { private List<int> _myOtherProperty = new List<int>(); public List<int> MyOtherProperty { get { return _myOtherProperty; } } }
Para consumir este ejemplo, podríamos escribir el siguiente código:
var myOtherClass = new MyOtherClass(); var myOtherProperty = myOtherClass.MyOtherProperty; myOtherProperty.Add(2); Console.WriteLine(myOtherProperty.Count);
Si el ejemplo de int daba un problema a la hora de modificar su propiedad de sólo lectura, aquí nos encontramos con el mismo comportamiento de la colección de la clase Person.
Esto se debe a que List es una clase, y como tal, es un tipo por referencia.
Así que el comportamiento de List<Person> es el mismo de List.
De igual manera si tuviéramos una declaración de Person únicamente.
Espero que quede claro que el hecho de declarar una propiedad como sólo lectura, no implica que realmente sea de sólo lectura.
Para recordar, un enlace a la documentación sobre tipos por referencia de C#.
Y lo mismo sobre List<T>.
Happy Coding!
One Responseso far
Personalmente, más que un tema de paso por referencia, lo veo como una confusión de nombres.
Es exactamente lo mismo que sucede en JavaScript: readonly no significa inmutable, del mismo modo que en JavaScript const n significa inmutable.
Ni C# ni JavaScript permiten definir definir referencias inmutables: o el tipo es inmutable por definción (p. ej. String) o no hay nada qué hacer.
El tema de paso por referencia entra porque, en efecto, si la propiedad es de un tipo por valor, dado que se crea un copia, se modifica la copia recibida, dando lugar a comportamentos erráticos. Es exactamente tu ejemplo donde explícitamente devuelves una copia de la List (debes hacerlo pq es por referencia).
Pero, en mi opinión, si no quieres que te modifiquen la lista de elementos, NO devuelvas una List… Ni tan siquiera es buena opción devolver una copia: devuelve un IEnumerable . Con ello te aseguras que no te pueden añadir o borrar objetos, aunque ciertamente te pueden modificar los existentes si T es un tipo por referencia y tiene setters.