Muy buenas! Para ser sinceros esta es una pregunta que me he hecho siempre y, creo yo, que se han hecho muchas personas que vienen de C++. ¿Debería tener C# referencias const? El hecho es que hasta ayer no había encontrado una explicación razonada y de alguien de peso (quien mejor que Eric Lippert, cuyo blog es lectura obligada) del porque C# no las incluye. Al final del post hay el enlace al post de stackoverflow en el que Eric explica sus razones por las que C# no tiene referencias const.
En este post voy a intentar explicar que son las referencias const (en C++) porque a Eric Lippert no le convencen (ojo que no queda claro con una sola lectura de lo que él dice, tuve que leérmelo un par o tres de veces junto con los comentarios, porque Eric es de los que cuando dice algo cualquier palabra cuenta), además de algunas opciones que hay actualmente en C# para simularlas.
El objetivo del post es captar vuestra opinión: es decir, creéis que estarían bien? O que sobran totalmente? O que tal y como están en C++ no, pero de otra forma podrían ser interesantes?
Dado que voy a exponer varias opiniones al respecto este post será largo… Pero espero que os resulte de interés 😉
Referencias const en C++
Antes que nada aclaremos a que nos referimos por referencias const. Porque hay dos posibilidades:
- La referencia es constante, es decir, una vez inicializada su valor NO puede ser modificado (no puede apuntar a otro objeto). En C# eso sería una variable readonly.
- La referencia NO es constante pero a través de esa referencia NO puede modificarse el objeto apuntado.
En C++ ambas posibilidades existen, así que NO es lo mismo:
Foo& const foo
que
const Foo& foo
En el primer caso tenemos una referencia que es constante (básicamente lo que en C# conocemos como readonly, con la salvedad que pueden ser inicializadas en cualquier momento), mientras que en el segundo tenemos una referencia a través de la cual no podemos modificar el objeto. A esas referencias son a las que nos referimos con el nombre de “Referencias const”.
En C++ una clase debe declarar que métodos NO modifican los datos de la clase:
class Foo
{
int value;
public:
int GetValue (void) const;
void SetValue(int nv);
};
La clase Foo tiene dos métodos, GetValue y SetValue. Y el método GetValue está declarado como “const” para indicar que no modifica ningún miembro de la clase.
Así, el siguiente código NO compilará:
void FooConst1 (const Foo& foo)
{
foo.SetValue(10);
}
El compilador nos avisará que estamos intentando modificar un objeto Foo a través de una referencia const (el error real es un mensaje raro de que no puede convertir this pero quiere decir eso :p).
Usar una referencia const convierte el objeto en inmutable? No. Usar una referencia const evita que pueda ser modificado dicho objeto a través de esa referencia. Única y exclusivamente. Mira ese código y piensa cual es el valor de i después de ejecutar la función Bar:
void Bar(const int& v1, int& v2)
{
v2 = v1+1;
}
int _tmain(int argc, _TCHAR* argv[])
{
int i=10;
Bar(i, i);
}
El valor de i después de ejecutar Bar es de 11. Porque el valor de i se puede modificar a través de v2 que es una referencia tradicional (por supuesto intentar v1=v2+1 dentro de Bar sí que da error).
Ventajas de tener referencias const en el lenguaje
Como desarrollador el tener referencias const te permite:
- Tener la seguridad de que un método no va a modificar nada de tu clase. Si un método espera una referencia const sabes que el método no va a poder modificar el objeto que reciba.
- Devolver desde un método un objeto que no va a poder ser modificado por quien ha llamado el método. P.ej. en lugar de devolver una ReadOnlyCollection<T> (una clase que es una auténtica aberración desde el punto de vista de OOP) se podría devolver const List<T>.
La razón (de Eric Lippert) por la cual no existen en C#
En resumidas cuentas porque tal y como están implementadas en C++ no sirven. Él tiene dos objeciones al respecto, una muy pragmática y la otra mucho más profunda y filosófica.
- El casting, bien al estilo C, bien const_cast de C++, elimina la seguridad que podrían ofrecer las referencias const. Dicho de otro modo, la posibilidad de obtener una referencia tradicional a partir de una referencia const, convierten a éstas totalmente en inútiles (en lo que refiere a asegurar que a través de dicha referencia nadie podrá modificar el objeto). Imaginad que yo devuelvo una const List<T> con datos de mi objeto. Si quien obtiene la referencia puede a partir de esa referencia const, obtener una referencia tradicional (y por lo tanto añadir o eliminar elementos de la lista), yo pierdo la seguridad que tenía al devolver la referencia const. No puedo asumir que los contenidos del objeto no van a ser modificados. A eso se refiere Eric cuando dice que const is broken.
- La segunda razón es mucho más fuerte… Para Eric una referencia const sería realmente útil si convirtiese el objeto apuntado en inmutable. Lo deja muy claro con el siguiente comentario: If I have a reliably constant queue then I should be able to say "if (!q.Empty()) { M(); x = q.First(); }" and regardless of what M() does, the queue is still empty when it returns. The same way that if I say "const int y = 123; … if (y > 0) { M(); " and after M() returns, y is still greater than zero because it is constant.
Es decir, del mismo modo que const int i=10; significa que la variable i nunca podrá ser modificada bajo ningún concepto, del mismo modo const Queue q debería significar que el objeto q es inmutable. No puede modificarse. Eso plantea problemas muy serios en cuanto a como inicializamos estos objetos, o como convertimos un objeto mutable en inmutable, etc, etc. Pero quedaros con la clave: el objeto pasa a ser inmutable. No hay manera de echar esto atrás. No es una referencia const a un objeto, es una referencia a un objeto constante. Esa segunda razón es muy fuerte y va mucho más allá de lo que una referencia const de C++ pretende (que el objeto no sea modificable a través de esa referencia).
Olvidemos pues esa segunda razón, a pesar de que para Eric tiene un peso fundamental, y volvamos a la primera: la posibilidad de hacer casting y de obtener una referencia normal a partir de una referencia const. La solución parece rápida y trivial: se prohíben esos castings. Y punto.
Y como pasa casi siempre, nada es tan trivial:
- Que hacemos con reflection? Si alguien usa reflection teoricamente podría llamara a un método que modificase el objeto apuntado por la referencia (aquí sale a colación de nuevo el segundo argumento de Eric: dado que el objeto no es inmutable, sino que es la referencia la que está marcada como const). Es eso importante? Bueno, depende… actualmente con reflection podemos llamar a métodos privados de una clase, lo que (sin negar su utilidad) puede llegar a representar una violación todavía mayor.
- Que hacemos con object? O dicho de otro modo… una referencia const Foo puede ser pasada a un método que acepte Object? Seguramente sí, responderéis todos, ya que los Foo son Objects con independencia de que sean mutables o no. Pero ahora imaginad una jerarquía de clases:
class Foo
{
public int PropFoo { get; set; }
}
class Bar : Foo
{
public string PropBar { get; set; }
}
Si un método recibe un objeto Foo, le puedo pasar un const Bar? Los objetos Bar son todos ellos objetos Foo, pero los const Bar lo son? Tiene sentido que lo sean? Tiene sentido que no lo sean? O los objetos const Bar sólo pueden ser const Foo? Dicho de otro modo: tenemos UNA sola jerarquía de objetos que empieza por Object, o tenemos DOS (una que empieza por Object y la otra por const Object)? Si admitimos un sola jerarquía, entonces estaremos de acuerdo en que yo puedo tener un const Bar y ese ser modificado (al menos la propiedad PropFoo) a través de un método que espere un Foo.
Por supuesto hay versiones mixtas, como tener una sola jerarquía, pero sólo admitir llamadas a métodos que no muten el objeto. De esa manera una referencia const Bar podría ser pasada a un método Foo y eso sólo compilaría si el método no hace nada que pueda mutar el objeto (p.ej. estaría bien llamar al getter de la propiedad pero no al setter). Pero eso complica mucho (pero mucho, eh?) el tema y es que de hecho con esta visión estamos volviendo al segundo punto que comentaba Eric (no es la referencia lo que está marcada como const, es el objeto que está marcado como inmutable). Y de todos modos para mi esa opción es inviable el hecho de que no hay nada que diga a priori a quien llama el método si esa llamada es correcta o no: que el código compilase dependería de lo que hiciera internamente un método X (que puede estar en otro assembly ya compilado) y no de sus parámetros, ni nada que yo pueda ver externamente. Lo único que podría hacer es intentar pasar mi referencia const Bar a un método que acepta un Foo y… ver si compila! Surrealista.
¿Son necesarias las referencias const tal y como están en C++?
Dicho de forma rápida y corta: No. Eso no significa que no sean útiles. Simplemente que hay otros métodos para hacer lo mismo. ¿Y en que consisten esos métodos? Pues básicamente en declararse una versión solo lectura de mi clase. La mejor manera de hacer esto es a través de una interfaz:
interface IReadOnlyFoo
{
int PropFoo { get; }
}
class Foo : IReadOnlyFoo
{
public int PropFoo { get; set; }
}
Si quiero devolver una referencia a un objeto Foo que no pueda modificarse devuelvo un objeto Foo a través de una referencia de tipo IReadOnlyFoo. Y listos! Si un método quiere aceptar como parámetro un objeto de tipo Foo pero NO quiere modificarlo puede aceptar un parámetro de tipo IReadOnlyFoo. Así el equivalente de const Foo es IReadOnlyFoo.
Por lo tanto la misma semántica que obtenemos a través de las referencias const, las obtenemos con este mecanismo. Cierto: es más tedioso y nos obliga a hacerlo por cada clase de la cual querramos tener una versión de sólo lectura.
Métodos puros
Una de las ventajas de la implementación de referencias const en C++ es que obliga al creador de la clase a declarar que métodos son susceptibles de ser llamados a través de una referencia const. A estos métodos se los conoce en C++ como métodos const. El compilador comprueba efectivamente que un método declarado como const lo sea:
class Foo
{
int value;
public:
int GetValue (void) const;
void SetValue(int nv);
};
int Foo::GetValue(void) const
{
value–;
return value;
}
Este código no compila, porque el método GetValue intenta modificar value a pesar de ser declarado como método const.
Debajo de los métodos const subyace un concepto realmente potente e interesante: los métodos puros. Dicho rápidamente, un método puro (no confundir con un método virtual puro de C++ que no tiene nada que ver) es aquel que siempre devuelve el mismo valor si se le pasan los mismos parámetros y además durante su ejecución no se observa ningún efecto colateral (en el caso de una clase significa que no modifica el estado de dicha clase). Ojo, que los métodos const y los métodos puros NO son exactamente lo mismo:
- Un método const puede ser impuro si devuelve datos aleatorios o dependientes de algo externo no controlable (p.ej. un fichero). Así p.ej. la propiedad Now de DateTime no es pura porque su valor no es predecible (cada día cambia!).
- Un método const NO puede modificar ningún miembro de la clase. Pero NO todos los miembros de la clase conforman su estado. Puede haber miembros privados que sean susceptibles de ser modificados pero que no formen parte del estado de la clase. Un método puro podría modificar esos miembros y un método const no (aunque este segundo punto puede solventarse con el uso de mutable en C++).
¿Y porque son interesantes los métodos puros? Bueno… tenerlos identificados permite ciertas optimizaciones (se puede memoizar (así sin erre) la llamada) y también habilita estos métodos para ser usados en comprobaciones de precondiciones, postcondiciones e invariantes. Por definición la comprobación de precondiciones, postcondiciones e invariantes deben ser métodos puros (dado que deben ser predecibles y no modificar el estado del objeto). Sin métodos puros el diseño por contratos no es posible (de forma segura y validada por el compilador)…
¿Hola? Si todavía estás aquí… muchas gracias por llegar hasta el final del post. Recuérdame que te pague una cervecita cuando nos veamos por ahí 😉
He creado una encuesta para que podáis expresar vuestra opinión al respecto de todo lo comentado y por supuesto, tenéis los comentarios para explayaros a gusto! 😉
Enlace a la encuesta: http://www.easypolls.net/poll.html?p=4eb26acb011eb0e44d6335ab
Enlace al post de stackoverflow: http://stackoverflow.com/questions/3263001/why-const-parameters-are-not-allowed-in-c-sharp
Un saludo! 🙂
Sólo con una lectura superficial, me inclino a pensar que con los readonly y las interfaces es suficiente, aunque en algunos casos se necesiten clases especiales como ReadOnlyCollection (que bien podía ser una interfaz, pero entonces tendríamos el peligro de los casting). Los objetos inmutables también pueden construirse sobre el lenguaje actual, con poco esfuerzo (véase string).
Pero sí me gustaría que el lenguaje permitiera identificar (y validara) las funciones puras, eso he votado en tu encuesta.
Nada más, que me ha encantado el artículo, y la cerveza te la debemos a ti. Toda la divagación sobre la doble jerarquía de herencia es muy interesante, está claro que ahí hay un problema de difícil solución. En mi opinión, las cosas, cuanto más simples, mejor, y esta creo que fue la filosofía de C# al evitar construcciones complejas de C++.
Aunque en muchos aspectos no sé qué es culpa o mérito de C# y qué le viene impuesto por el .NET Framework.
El problema no es lo que tu no puedas hacer, es lo que pueden hacer otros, que luego te lo tienes que comer.
Cada vez que meten algo nuevo en C# tiemblo.