Curso de iniciación a la programación con C# |
CAMPOS
Algunos autores hablan indistintamente de campos y propiedades como si fueran la misma cosa, y tiene su lógica, no creáis que no, porque para el cliente de una clase van a ser cosas muy parecidas. Sin embargo, yo me voy a mojar y voy a establecer distinción entre campos y propiedades, no por complicar la vida a nadie, sino para que sepáis a qué me refiero cuando hablo de un campo o cuando hablo de una propiedad. Ambas cosas (campos y propiedades) representan a los datos de una clase, aunque cada uno de ellos lo hace de un modo diferente. Recuerda que en la tercera entrega hablamos de los indicadores, y desde entonces hemos ido usando alguno. Pues bien, los campos de una clase se construyen a base de indicadores. Vamos a empezar jugando un poco con todo esto (le daremos más de una vuelta):
using
System;
namespace
Circunferencia{
class Circunferencia
{
public Circunferencia(double radio)
{
this.Radio=radio;
}
public double Radio;
public double Perimetro;
public double Area;
public const double PI=3.1415926;
}
class CircunferenciaApp
{
static void Main()
{
Circunferencia c;
string rad; double radio=0;
do
{
try{
Console.Write("Dame un radio para la circunferencia: ");
rad=Console.ReadLine();
radio=Double.Parse(rad);
}
catch{
continue;}
}
while (radio<=0);
c=
new Circunferencia(radio);c.Perimetro=2 * Circunferencia.PI * c.Radio;
c.Area=Circunferencia.PI * Math.Pow(c.Radio,2);
Console.WriteLine("El radio es: {0}", c.Radio);
Console.WriteLine("El perímetro es: {0}", c.Perimetro);
Console.WriteLine("El área es : {0}", c.Area);
string a=Console.ReadLine();
}
}
}
Bueno, una vez más te pido que no te preocupes por lo que no entiendas, porque hay cosas que veremos más adelante, como los bloques do, try y catch. Están puestos para evitar errores en tiempo de ejecución (para que veáis que me preocupo de que no tengáis dificultades). Bien, lo más importante de todo es la clase circunferencia. ¿Qué es lo que te he puesto en negrilla? Efectivamente, tres variables y una constante. Pues bien, esos son los campos de la clase Circunferencia. ¿Serías capaz de ver si hay declarado algún campo en la clase CircunferenciaApp, que es donde hemos puesto el método Main? A ver... a ver... Por ahí hay uno que dice que hay tres: uno de la clase Circunferencia, otro de la clase string y otro de la clase double. ¿Alguien está de acuerdo...? Pues yo no. En efecto, hay tres variables dentro del método Main, pero no son campos de la clase CircunferenciaApp, porque están dentro de un método. Por lo tanto, todos los campos son indicadores, pero no todos los indicadores son campos, ya que si una variable representa o no un campo depende del lugar donde se declare.
Recuerda los modificadores de acceso de los que hablamos por primera vez en la tercera entrega de este curso (private, protected, internal y public). Pues bien, estos modificadores son aplicables a las variables y constantes solamente cuando estas representan los campos de una clase, y para ello deben estar declaradas como miembros de la misma dentro de su bloque. Sin embargo, una variable que esté declarada en otro bloque distinto (dentro de un método, por ejemplo) no podrá ser un campo de la clase, pues será siempre privada para el código que esté dentro de ese bloque, de modo que no se podrá acceder a ella desde fuera del mismo. Por este motivo, las tres variables que están declaradas dentro del método Main en nuestro ejemplo no son campos, sino variables privadas accesibles solamente desde el código de dicho método. Del mismo modo, si hubiéramos declarado una variable dentro del bloque "do", esta hubiera sido accesible solamente dentro del bloque "do", e inaccesible desde el resto del método Main.
Ahora quiero que te fijes especialmente en el código del método Main. Estamos accediendo a los campos del objeto con la sintaxis "nombreobjeto.campo", igual que se hacía para acceder a los métodos, aunque sin poner paréntesis al final. Sin embargo, hay una diferencia importante entre el modo de acceder a los tres campos variables (Area, Perimetro y Radio) y el campo constante (PI): En efecto, a los campos variables hemos accedido como si fueran métodos normales, pero al campo constante hemos accedido como accedíamos a los métodos static, es decir, poniendo el nombre de la clase en lugar del nombre del objeto. ¿Por qué? Porque, dado que un campo constante mantendrá el mismo valor para todas las instancias de la clase, el compilador ahorra memoria colocándolo como si fuera static, evitando así tener que reservar un espacio de memoria distinto para este dato (que, recuerda, siempre es el mismo) en cada una de las instancias de la clase.
Ya sé que alguno estará pensando: "pues vaya una clase Circunferencia has hecho, que tienes que hacerte todos los cálculos a mano en el método Main. Para eso nos habíamos declarado las variables en dicho método y nos ahorrábamos la clase". Pues tienes razón. Lo suyo sería que fuera la clase Circunferencia la que hiciera todos los cálculos a partir del radio, en lugar de tener que hacerlos en el método Main o en cualquier otro método o programa que utilice esta clase. Vamos con ello, a ver qué se puede hacer:
class Circunferencia
{
public Circunferencia(double radio)
{
this.Radio=radio;
this.Perimetro=2 * PI * this.Radio;
this.Area=PI * Math.Pow(this.Radio,2);
}
public double Radio;
public double Perimetro;
public double Area;
public const double PI=3.1415926;
}
Bueno, ahora, como ves, hemos calculado los valores de todos los campos en el constructor. Así el cliente no tendrá que hacer cálculos por su cuenta para saber todos los datos de los objetos de esta clase, sino que cuando se instancie uno, las propiedades tendrán los valores adecuados. ¿Qué ocurriría si en el cliente escribiéramos este código?:
Circunferencia c=
new Circunferencia(4);c.Perimetro=1;
c.Area=2;
Al instanciar el objeto, se ejecutará su constructor dando los valores adecuados a los campos Area y Perimetro. Sin embargo, después el cliente puede modificar los valores de estos campos, asignándole valores a su antojo y haciendo, por lo tanto, que dichos valores no sean coherentes (claro, si el radio vale 4, el perímetro no puede ser 1, ni el área puede ser 2). ¿Cómo podemos arreglar esta falta de seguridad? Pues usando algo que no existía hasta ahora en ningún otro lenguaje: los campos de sólo lectura (ojo, que digo campos, no propiedades). Veámoslo:
class Circunferencia
{
public Circunferencia(double radio)
{
this.Radio=radio;
this.Perimetro=2 * PI * this.Radio;
this.Area=PI * Math.Pow(this.Radio,2);
}
public double Radio;
public readonly double Perimetro;
public readonly double Area;
public const double PI=3.1415926;
}
Bien, ahora tenemos protegidos los campos Perimetro y Area, pues son de sólo lectura, de modo que ahora el cliente no podrá modificar los valores de dichos campos. Para hacerlo fíjate que hemos puesto la palabra readonly delante del tipo del campo. Sin embargo, seguimos teniendo un problema: ¿qué pasa si, después de instanciar la clase, el cliente modifica el valor del radio? Pues que estamos en las mismas... El radio volvería a no ser coherente con el resto de los datos del objeto. ¿Qué se os ocurre para arreglarlo? Claro, podríamos poner el campo Radio también de sólo lectura, pero en este caso tendríamos que instanciar un nuevo objeto cada vez que necesitemos un radio distinto, lo cual puede resultar un poco engorroso. Quizá podríamos hacer un pequeño rodeo: ponemos el radio también como campo de sólo lectura y escribimos un método para que el cliente pueda modificar el radio, y escribimos en él el código para modificar los tres campos, de modo que vuelvan a ser coherentes. Sin embargo, esto no se puede hacer. ¿Por qué? Porque los campos readonly solamente pueden ser asignados una vez en el constructor, y a partir de aquí su valor es constante y no se puede variar en esa instancia. Y entonces, ¿por qué no usamos constantes en vez de campos de sólo lectura? Pero hombre..., cómo me preguntas eso... Para poder usar constantes hay que saber previamente el valor que van a tener (como la constante PI, que siempre vale lo mismo), pero, en este caso, no podemos usar constantes para radio, área y perímetro porque no sabremos sus valores hasta que no se ejecute el programa. Resumiendo: los campos de sólo lectura almacenan valores constantes que no se conocerán hasta que el programa esté en ejecución. Habrá que hacer otra cosa para que esto funcione mejor, pero la haremos después... Antes tengo que contaros más cosas sobre los campos.
Por otro lado, los campos, igual que los métodos y los constructores, también pueden ser static. Su comportamiento sería parecido: un campo static es aquel que tiene mucho más que ver con la clase que con una instancia particular de ella. Por ejemplo, si quisiéramos añadir una descripción a la clase circunferencia, podríamos usar un campo static, porque todas las instancias de esta clase se ajustarán necesariamente a dicha descripción. Si ponemos el modificador static a un campo de sólo lectura, este campo ha de ser inicializado en un constructor static. Ahora bien, recuerda que las constantes no aceptan el modificador de acceso static: si su modificador de acceso es public o internal ya se comportará como su fuera un campo static. Pongamos un ejemplo de esto:
class Circunferencia
{
static Circunferencia()
{
Descripcion="Polígono regular de infinitos lados";
}
public Circunferencia(double radio)
{
this.Radio=radio;
this.Perimetro=2 * PI * this.Radio;
this.Area=PI * Math.Pow(this.Radio,2);
}
public double Radio;
public readonly double Perimetro;
public readonly double Area;
public const double PI=3.1415926;
public static readonly string Descripcion;
}
Ahora la clase Circunferencia cuenta con un constructor static que se ocupa de inicializar el valor del campo Descripción, que también es static.
Bien, retomemos la problemática en la que estábamos sumidos con la clase Circunferencia. Veamos: el objetivo es que esta clase contenga siempre datos coherentes, dado que el área y el perímetro siempre están en función del radio, y que el radio se pueda modificar sin necesidad de volver a instanciar la clase. Por lo tanto, tenemos claro que no podemos usar campos ni campos de sólo lectura, ya que los primeros no nos permiten controlar los datos que contienen, y los segundos no nos permiten modificar su valor después de ser inicializados en el constructor.
Con lo que hemos aprendido hasta ahora ya tenemos herramientas suficientes como para solventar el problema, aunque, como veremos después, no sea el modo más idóneo de hacerlo. Veamos: podríamos cambiar los modificadores de acceso de los campos, haciéndolos private o protected en lugar de public, y después escribir métodos para retornar sus valores. Vamos a ver cómo se podría hacer esto:
using System;
namespace CircunferenciaMetodos
{
class Circunferencia
{
public Circunferencia(double rad)
{
this.radio=rad;
}
protected double radio;
const double PI=3.1415926;
public double Radio()
{
return this.radio;
}
public void Radio(double rad)
{
this.radio=rad;
}
public double Perimetro()
{
return 2 * PI * this.radio;
}
public double Area()
{
return PI * Math.Pow(this.radio,2);
}
}
class CircunferenciaApp
{
static void Main()
{
Circunferencia c=new Circunferencia(4);
Console.WriteLine("El radio de la circunferencia es {0}",c.Radio());
Console.WriteLine("El perímetro de la circunferencia es {0}",
c.Perimetro());
Console.WriteLine("El área de la circunferencia es {0}", c.Area());
Console.WriteLine("Pulsa INTRO para incrementar el radio en 1");
string a = Console.ReadLine();
c.Radio(c.Radio()+1);
Console.WriteLine("El radio de la circunferencia es {0}",c.Radio());
Console.WriteLine("El perímetro de la circunferencia es {0}",
c.Perimetro());
Console.WriteLine("El área de la circunferencia es {0}", c.Area());
a=Console.ReadLine();
}
}
}
Como ves, ahora la clase Circunferencia garantiza que sus datos contendrán siempre valores coherentes, además de permitir que se pueda modificar el radio, pues el método Radio está sobrecargado: una de las sobrecargas simplemente devuelve lo que vale la variable protected radio y la otra no devuelve nada, sino que da al radio un nuevo valor. Por otro lado, ya que hemos escrito métodos para devolver perímetro y área nos ahorramos las variables para estos datos, pues podemos calcularlos directamente en dichos métodos. Sin embargo, la forma de usar esta clase es muy forzada y muy poco intuitiva, es decir, poco natural. En efecto, no resulta natural tener que poner los paréntesis cuando lo que se quiere no es ejecutar una operación, sino simplemente obtener un valor. El colmo ya es cuando queremos incrementar el radio en una unidad, en la línea c.Radio(c.Radio()+1); esto es completamente antinatural, pues lo más lógico hubiera sido poder hacerlo con esta otra línea: c.Radio++. Pero, tranquilos, C# también nos soluciona estas pequeñas deficiencias, gracias a las propiedades.
PROPIEDADES
Como dije al principio, las propiedades también representan los datos de los objetos de una clase, pero lo hacen de un modo completamente distinto a los campos. Antes vimos que los campos no nos permitían tener el control de su valor salvo que fueran de sólo lectura, y si eran de sólo lectura solamente se podían asignar una vez en el constructor. Esto puede ser verdaderamente útil en muchas ocasiones (y por eso os lo he explicado), pero no en este caso en concreto. Pues bien, las propiedades solventan todos estos problemas: por un lado nos permiten tener un control absoluto de los valores que reciben o devuelven, y además no tenemos limitaciones para modificar y cambiar sus valores tantas veces como sea preciso.
Las propiedades funcionan internamente como si fueran métodos, esto es, ejecutan el código que se encuentra dentro de su bloque, pero se muestran al cliente como si fueran campos, es decir, datos. Soy consciente de que, dicho así, suena bastante raro, pero verás que es muy fácil. La sintaxis de una propiedad es la siguiente:
acceso [static] tipo NombrePropiedad
{
get
{
// Código para calcular el valor de retorno (si procede)
return ValorRetorno;
}
set
{
// Código para validar y/o asignar el valor de la propiedad
}
}
Veamos: primero el modificador de acceso, que puede ser cualquiera de los que se usan también para los campos. Si no se indica, será private. Después la palabra static si queremos definirla como propiedad estática, es decir, que sería accesible sin instanciar objetos de la clase, pero no accesible desde las instancias de la misma (como los campos static). Posteriormente se indica el tipo del dato que almacenará la propiedad (cualquier tipo valor o cualquier tipo referencia), seguido del nombre de la propiedad. Dentro del bloque de la propiedad ves que hay otros dos bloques: el bloque get es el bloque de retorno, es decir, el que nos permitirá ver lo que vale la propiedad desde la aplicación cliente; y el bloque set es el bloque de asignación de la propiedad, es decir, el que nos permitirá asignarle valores desde la aplicación cliente. El orden en que se pongan los bloques get y set es indiferente, pero, obviamente, ambos han de estar dentro del bloque de la propiedad. Por otro lado, si se omite el bloque de asignación (set) habremos construido una propiedad de sólo lectura. Veremos esto mucho mejor con un ejemplo. Vamos a modificar la clase Circunferencia para ver cómo podría ser usando propiedades:
using System;
namespace Circunferencia
{
class Circunferencia
{
public Circunferencia(double radio)
{
this.radio=radio;
}
private double radio;
const double PI=3.1415926;
public double Radio
{
get
{
return this.radio;
}
set
{
this.radio=value;
}
}
public double Perimetro
{
get
{
return 2 * PI * this.radio;
}
}
public double Area
{
get
{
return PI * Math.Pow(this.radio, 2);
}
}
}
}
Bueno, lo cierto es que, desde la primera clase Circunferencia que escribimos a esta hay un abismo... Ahora no hemos escrito métodos para modificar el radio ni para obtener los valores de las otros datos, sino que hemos escrito propiedades. Gracias a esto conseguimos que el cliente pueda acceder a los datos de un modo mucho más natural. Pongamos un método Main para que aprecies las diferencias, y luego lo explicamos con calma:
static void Main()
{
Circunferencia c=new Circunferencia(4);
Console.WriteLine("El radio de la circunferencia es {0}",c.Radio);
Console.WriteLine("El perímetro de la circunferencia es {0}",
c.Perimetro);
Console.WriteLine("El área de la circunferencia es {0}", c.Area);
Console.WriteLine("Pulsa INTRO para incrementar el Radio en 1");
string a = Console.ReadLine();
c.Radio++;
Console.WriteLine("El radio de la circunferencia es {0}",c.Radio);
Console.WriteLine("El perímetro de la circunferencia es {0}",
c.Perimetro);
Console.WriteLine("El área de la circunferencia es {0}", c.Area);
a=Console.ReadLine();
}
Ahora puedes apreciar claramente las diferencias: accedemos a las propiedades tal y como hacíamos cuando habíamos definido los datos de la clase a base de campos. Sin embargo tenemos control absoluto sobre los datos de la clase gracias a las propiedades. En efecto, podemos modificar el valor del Radio con toda naturalidad (en la línea c.Radio++) y esta modificación afecta también a las propiedades Perimetro y Area. Vamos a ver poco a poco cómo ha funcionado este programa: cuando instanciamos el objeto se ejecuta su constructor, asignándose el valor que se pasa como argumento al campo radio (que es protected y, por lo tanto, no accesible desde el cliente). Cuando recuperamos el valor de la propiedad Radio para escribirlo en la consola se ejecuta el bloque "get" de dicha propiedad, y este bloque devuelve, precisamente el valor de del campo radio, que era la variable donde se almacenaba este dato. Cuando se recuperan los valores de las otras dos propiedades también para escribirlos en la consola sucede lo mismo, es decir, se ejecutan los bloques get de cada una de ellas que, como veis, retornan el resultado de calcular dichos datos. Por último, cuando incrementamos el valor del radio (c.Radio++) lo que se ejecuta es el bloque set de la propiedad, es decir, que se asigna el nuevo valor (representado por "value") a la variable protected radio. ¿Y por qué las propiedades Area y Perimetro no tienen bloque set? Recuerda que el bloque set es el bloque de asignación; por lo tanto, si se omite, tendremos una propiedad de sólo lectura. ¿Y cuál es la diferencia con los campos de sólo lectura? Pues la diferencia es evidente: un campo de sólo lectura ha de estar representado necesariamente por una variable, y, además, solamente se le puede asignar el valor una vez en el constructor; por contra, el que una propiedad sea de sólo lectura no implica que su valor sea constante, sino única y exclusivamente que no puede ser modificado por el cliente. Si hubiéramos puesto campos de sólo lectura no los podría modificar ni el cliente, ni la propia clase ni el mismísimo Bill Gates en persona. ¿Y de dónde ha salido el value? Bien, value es una variable que declara y asigna implícitamente el compilador en un bloque set para que nosotros sepamos cuál es el valor que el cliente quiere asignar a la propiedad, es decir, si se escribe c.Radio=8, value valdría 8. Así podremos comprobar si el valor que se intenta asignar a la propiedad es adecuado. Por ejemplo, si el valor que se intenta asignar al radio fuera negativo habría que rechazarlo, puesto que no tendría sentido, pero como aún no hemos llegado a esa parte, lo dejamos para la próxima entrega.
No me gustaría acabar esta entrega sin evitar que alguien pueda tomar conclusiones equivocadas. Veamos, os he dicho que las propiedades funcionan internamente como si fueran métodos, pero que no es necesario poner los paréntesis cuando son invocadas, pues se accede a ellas como si fueran campos. Sin embargo esto no quiere decir que siempre sea mejor escribir propiedades en lugar de métodos (claro, si no requieren más de un argumento). Las propiedades se han inventado para hacer un uso más natural de los objetos, y no para otra cosa. ¿Entonces, cuándo es bueno escribir un método y cuándo es bueno escribir una propiedad? Pues bien, hay que escribir un método cuando este implique una acción, y una propiedad cuando esta implique un dato. Vamos a retomar una vez más el "remanido" ejemplo de la clase Coche. Podíamos haber escrito el método Frenar como una propiedad, con lo que, en la aplicación cliente tendríamos que invocarlo así: coche.Frenar=10. Sin embargo, aunque funcionaría exactamente igual, esto no tendría mucho sentido, pues frenar es una acción y no un dato, y el modo más natural de ejecutar una acción es con un método, o sea, coche.Frenar(10).
Bien, creo que, con todo lo que hemos aprendido hasta ahora, llega el momento de proponeros un "pequeño ejercicio". Aparecerá resuelto con la próxima entrega (no antes, que de lo contrario no tendría gracia), pero te recomiendo que intentes hacerlo por tu cuenta y mires la solución cuando ya te funcione o si te quedas atascado sin remedio ya que, de lo contrario, no aprenderás nunca a escribir código. Eso sí: mírate los apuntes y la teoría tanto como lo necesites, porque estos siempre los vas a tener a tu disposición. También es importante que no te rindas a las primeras de cambio: cuando te aparezcan errores de compilación intenta resolverlos tú mismo, porque cuando estés desarrrollando una aplicación propia tendrás que hacerlo así, de modo que lo mejor será que empieces cuanto antes. Por último, te aconsejo que antes de mirar el ejercicio resuelto si ves que no te sale eches un vistazo a las pistas que te voy poniendo, a ver si así lo vas sacando. Bueno, venga, vale de rollos y vamos a lo que vamos:
Ejercicio 1: Aunque soy consciente de que este ejercicio te parecerá un mundo si no habías programado antes, te aseguro que es muy fácil. Es un poco amplio para que puedas practicar casi todo lo que hemos visto hasta ahora. Vete haciéndolo paso por paso con tranquilidad, y usa el tipo uint para todos los datos numéricos: Escribe una aplicación con estos dos espacios de nombres: Geometria y PruebaGeometria. Dentro del espacio de nombres Geometria tienes que escribir dos clases: Punto y Cuadrado. La clase Punto ha de tener dos campos de sólo lectura: X e Y (que serán las coordenadas del punto). La clase Cuadrado ha de tener las siguientes propiedades del tipo Punto (de solo lectura): Vertice1, Vertice2, Vertice3 y Vertice4 (que corresponden a los cuatro vértices del cuadrado). La base de todos los cuadrados de esta clase será siempre horizontal. También ha de tener las propiedades Lado, Area y Perimetro, siendo la primera de lectura/escritura y las otras dos de sólo lectura. Por otro lado, debe tener dos constructores: uno para construir el cuadrado por medio de los vértices 1 y 3 y otro para construir el cuadrado a través del Vertice1 y la longitud del lado. En el espacio de nombres PruebaGeometria es donde escribirás una clase con un método Main para probar si funcionan las clases escritas anteriormente. En este espacio de nombres quiero que utilices la directiva using para poder utilizar todos los miembros del espacio de nombres Geometría directamente. En este espacio de nombres escribe también un método que muestre todos los datos de un cuadrado en la consola. Hala, al tajo...
Sigue este vínculo si quieres ver las pistas para resolver el ejercicio.
Sigue este vínculo para bajarte los ejemplos de esta entrega.