Tema 9: Propiedades
Concepto de
propiedad
Una propiedad es una mezcla entre el concepto de campo y el
concepto de método. Externamente es accedida como si de un
campo normal se tratase, pero internamente es posible asociar
código a ejecutar en cada asignación o lectura de su
valor. Éste código puede usarse para comprobar que no
se asignen valores inválidos, para calcular su valor
sólo al solicitar su lectura, etc.
Una propiedad no almacena datos, sino sólo se utiliza como
si los almacenase. En la práctica lo que se suele hacer
escribir como código a ejecutar cuando se le asigne un
valor, código que controle que ese valor sea correcto y que
lo almacene en un campo privado si lo es; y como código a
ejecutar cuando se lea su valor, código que devuelva el
valor almacenado en ese campo público. Así se simula
que se tiene un campo público sin los inconvenientes que
estos presentan por no poderse controlar el acceso a ellos.
Definición de
propiedades
Para definir una propiedad se usa la siguiente sintaxis:
< tipoPropiedad> <nombrePropiedad>
{
set
{
<códigoEscritura>
}
get
{
<códigoLectura>
}
}
Una propiedad así definida sería accedida como si de
un campo de tipo <tipoPropiedad> se tratase, pero en cada
lectura de su valor se ejecutaría el
<códigoLectura> y en cada escritura de un valor en
ella se ejecutaría <códigoEscritura>
Al escribir los bloques de código get y set
hay que tener en cuenta que dentro del código set se
puede hacer referencia al valor que se solicita asignar a
través de un parámetro especial del mismo tipo de
dato que la propiedad llamado value (luego nosotros no
podemos definir uno con ese nombre en
<códigoEscritura>); y que dentro del código
get se ha de devolver siempre un objeto del tipo de dato de
la propiedad.
En realidad el orden en que aparezcan los bloques de código
set y get es irrelevante. Además, es posible
definir propiedades que sólo tengan el bloque get
(propiedades de sólo lectura) o que sólo
tengan el bloque set (propiedades de sólo
escritura) Lo que no es válido es definir propiedades
que no incluyan ninguno de los dos bloques.
Las propiedades participan del mecanismo de polimorfismo igual que
los métodos, siendo incluso posible definir propiedades
cuyos bloques de código get o set sean
abstractos. Esto se haría prefijando el bloque apropiado con
un modificador abstract y sustituyendo la definición
de su código por un punto y coma. Por ejemplo:
using System;
abstract class A
{
public abstract int PropiedadEjemplo
{
set;
get;
}
}
class B:A
{
private int valor;
public override int PropiedadEjemplo
{
get
{
Console.WriteLine("Leído {0} de PropiedadEjemplo", valor);
return valor;
}
set
{
valor = value;
Console.WriteLine("Escrito {0} en PropiedadEjemplo", valor);
}
}
}
En este ejemplo se ve cómo se definen y redefinen
propiedades abstractas. Al igual que abstract y
override, también es posible usar cualquiera de los
modificadores relativos a herencia y polimorfismo ya vistos:
virtual, new y sealed.
Nótese que aunque en el ejemplo se ha optado por asociar un
campo privado valor a la propiedad PropiedadEjemplo, en realidad
nada obliga a que ello se haga y es posible definir propiedades que
no tenga campos asociados. Es decir, una propiedad no se tiene
porqué corresponder con un almacén de datos.
Acceso a propiedades
La forma de acceder a una propiedad, ya sea para lectura o
escritura, es exactamente la misma que la que se usaría para
acceder a un campo de su mismo tipo. Por ejemplo, se podría
acceder a la propiedad de un objeto de la clase B del ejemplo
anterior con:
B obj = new B();
obj.PropiedadEjemplo++;
El resultado que por pantalla se mostraría al hacer una
asignación como la anterior sería:
Leído 0 de PropiedadEjemplo;
Escrito 1 en PropiedadEjemplo;
Nótese que en el primer mensaje se muestra que el valor
leído es 0 porque lo que devuelve el bloque get de la
propiedad es el valor por defecto del campo privado valor, que como
es de tipo int tiene como valor por defecto 0.
Implementación
interna de propiedades
En realidad la definición de una propiedad con la sintaxis
antes vista es convertida por el compilador en la definición
de un par de métodos de la siguiente forma:
<tipoPropiedad> get_<nombrePropiedad>()
{ // Método en que se convierte el bloque get
<códigoLectura>
}
void set_<nombrePropiedad> (<tipoPropiedad> value)
{ // Método en que se convierte el bloque set
<códigoEscritura>
}
Esto se hace para que desde lenguajes que no soporten las
propiedades se pueda acceder también a ellas. Si una
propiedad es de sólo lectura sólo se generará
el método get_X(), y si es de sólo escritura
sólo se generará el set_X() Ahora bien, en
cualquier caso hay que tener cuidado con no definir en un mismo
tipo de dato métodos con signaturas como estas si se van a
generar internamente debido a la definición de una
propiedad, ya que ello provocaría un error de
definición múltiple de método.
Teniendo en cuenta la implementación interna de las
propiedades, es fácil ver que el último ejemplo de
acceso a propiedad es equivalente a:
B b = new B();
obj.set_PropiedadEjemplo(obj.get_Propiedad_Ejemplo()++);
Como se ve, gracias a las propiedades se tiene una sintaxis mucho
más compacta y clara para acceder a campos de manera
controlada. Se podría pensar que la contrapartida de esto es
que el tiempo de acceso al campo aumenta considerablemente por
perderse tiempo en hacer las llamada a métodos
set/get. Pues bien, esto no tiene porqué ser
así ya que el compilador de C# elimina llamadas haciendo
inlining (sustitución de la llamada por su cuerpo) en
los accesos a bloques get/set no virtuales y de
códigos pequeños, que son los más habituales.
Nótese que de la forma en que se definen los métodos
generados por el compilador se puede deducir el porqué
del hecho de que en el bloque set se pueda acceder a
través de value al valor asignado y de que el objeto
devuelto por el código de un bloque get tenga que ser
del mismo tipo de dato que la propiedad a la que pertenece.