C sharp NET/Capítulo 5
Introducción a las clases y objetos
[editar]
Introducción a las clases en C#
[editar]Como hemos dicho, C# es un lenguaje orientado a objetos. A diferencia de lenguajes como C++ o Python en los que la orientación a objetos es opcional, en C# y al igual que en Java, la orientación a objetos es ineludible, de hecho cualquier método o variable está contenida dentro de un objeto. Y el concepto fundamental en torno a la orientación a objetos es la clase.
Una clase es como una plantilla que describe cómo deben ser las instancias de dicha clase, de forma que cuando creamos una instancia, ésta tendrá exactamente los mismos métodos y variables que los que tiene la clase. Los datos y métodos contenidos en una clase se llaman miembros de la clase y se accede a ellos siempre mediante el operador "." . En el siguiente ejemplo, se definirá una clase, Clase1 y en el método Main se creará una instancia de Clase1 llamada MiClase. Una buena idea es jugar un poco con el código para ver que la instancia de la clase efectivamente tiene los mismos miembros que la clase Clase1 (que sería la plantilla de la que hablábamos antes)
using System; //definimos nuestra clase class Clase1{ public int a = 1; private double b = 3; public char c = 'a'; } //usamos la clase que hemos creado class UsoClase{ public static void Main() { Clase1 MiClase = new Clase1(); // asi creamos una instancia de Clase1 Console.WriteLine( MiClase.c ); //podemos llamar a los tipos que hay dentro de Clase1 } }
los identificadores public delante de los tipos que hay dentro de Clase1 son necesarios para luego poder ser llamados desde otra clase, como en este caso, que estamos llamando a los miembros de una instancia de Clase1 desde UsoClase. Pero en las clases no solo hay variables, también podemos incluir métodos.
using System; //definimos nuestra clase class Clase1{ public int a = 1; public double b = 3; public char c = 'a'; public void Descripcion() { Console.WriteLine("Hola, soy una clase"); } } //usamos la clase que hemos creado class UsoClase{ public static void Main() { Clase1 MiClase = new Clase1(); // asi creamos una instancia de Clase1 Console.WriteLine( MiClase.c ); //podemos usar todos los tipos que hay dentro de Clase1 MiClase.Descripcion(); } }
Podemos hacer más cosas con las clases, como heredar otras clases o implementar interfaces, pero en este capítulo nos centraremos en el uso de métodos y variables.
Métodos
[editar]Los métodos, también llamados funciones, son trozos de código que reciben unos datos, hacen algo con esos datos, y a veces devuelven algún valor. En C#, todos los métodos se encuentran contenidos dentro de un objeto.
La estructura mínima de un método tiene las siguientes partes:
* Tipo devuelto * Nombre del método * Parámetros (puede ser vacío) * Cuerpo del método
de forma que el siguiente método:
double Divide( double a, double b ) { return a/b; }
devuelve un tipo double, tiene por nombre Divide, los parámetos son a y b, ambos del tipo double, y el cuerpo del método es simplemente "return a/b;".
Cuando queramos llamar a un método, debemos simplemente poner el nombre del método y sus argumentos dentro de un paréntesis separados por comas. Para llamar al método Divide declarado antes, simplemente debemos escribir Divide(8, 2);
Según lo que hemos visto, el ejemplo del método Divide() completo necesita tener una clase donde definirse y un método Main() donde ejecutarse.
using System; class Metodo{ public double Divide( double a, double b ) { return a/b; } } class Principal{ public static void Main() { Metodo m = new Metodo(); Console.WriteLine( m.Divide(8, 2) ); } }
Pasando valores a los métodos
[editar]Parámetros
[editar]La declaración formal de parámetros también define variables. Hay cuatro tipos de parámetros: parámetros por valor, por referencia, parámetros de salida, y arreglos de parámetros.
Paso por valor
[editar]El paso de parámetros por valor es usado por defecto para pasar parámetros a métodos. Cuando se pasa un parámetro por valor a una función realmente se está pasando una copia de dicho parámetro, por lo que las modificaciones que le hagamos al parámetro dentro del método no afectarán al parámetro original. El ejemplo
using System; class Test { static void F(int p) { p++; Console.WriteLine("p = {0}", p); } static void Main() { int a = 1; Console.WriteLine("pre: a = {0}", a); F(a); Console.WriteLine("post: a = {0}", a); } }
muestra un método F que tiene un parámetro por valor llamado p. El ejemplo produce la salida:
pre: a = 1 p = 2 post: a = 1
aunque el valor del parámetro p haya sido modificado dentro del método, éste parámetro solamente tenía una copia del valor del parámetro a que pasamos al método; por lo que cuando imprimimos el parámetro a vemos que éste parámetro ha mantenido su valor original.
Paso por referencia
[editar]El paso de parámetros por referencia es la contraposición lógica al paso por valor. En el paso por referencia no se realiza ninguna copia del objeto, sino que lo que se le pasa a la función es una referencia del objeto, de forma que el parámetro pasa directamente a la función y cualquier modificación sobre el parámetro dentro de la función afectará al parámetro original
using System; class Test { static void Swap(ref int a, ref int b) { // intercambia los dos valores int t = a; a = b; b = t; } static void Main() { int x = 1; int y = 2; Console.WriteLine("pre: x = {0}, y = {1}", x, y); Swap(ref x, ref y); Console.WriteLine("post: x = {0}, y = {1}", x, y); } }
muestra un método swap que tiene dos parámetros por referencia. La salida producida es:
pre: x = 1, y = 2 post: x = 2, y = 1
La palabra clave ref debe de ser usada tanto en la declaración formal de la función como en los usos que se hace de ésta.
Parámetro de salida
[editar]El parámetro de salida es similar al parámetro por referencia, salvo que el valor inicial de dicho argumento carece de importancia. Un argumento de salida se declara con el modificador out. El ejemplo
using System; class Test { static void Divide(int num1, int num2, out int result, out int resid) { result = num1 / num2; resid = num1 % num2; } static void Main() { int valor1 = 10; int valor2 = 3; int respuesta, residuo; Divide(valor1, valor2, out respuesta, out residuo); Console.WriteLine("La división de {0} para {1} = {2} con un residuo de {3}", valor1, valor2, respuesta, residuo); } }
muestra un método Divide que incluye dos parámetros de salida. Uno para el resultado (variable result) de la división y otro para el resto (variable resid). Vemos que estos resultados son asignados a las variables respuesta y residuo respectivamente.
Arreglo de parámetros
[editar]Habrá ocasiones que necesitemos pasar varios parámetros a un método (o función) pero no sabremos con anticipación cuantos parámetros tendremos que pasar; para esto podremos usar un arreglo de parámetros. Un arreglo de parámetros permite guardar una relación de varios a uno: varios argumentos pueden ser representados por un único arreglo de parámetros. En otras palabras, los arreglos de parámetros permiten listas de argumentos de tamaño variable.
Un arreglo de parámetros se declara con el modificador params. Sólo puede haber un arreglo de parámetros en cada método, y siempre debe ser el último parámetro especificado. El tipo del arreglo de parámetros debe ser siempre un tipo arreglo unidimensional. Al llamar a la función se puede pasar uno o varios argumentos del tipo del arreglo. El ejemplo
using System; class Test { static void F(params int[] args) { Console.WriteLine("nº de argumentos: {0}", args.Length); for (int i = 0; i < args.Length; i++) Console.WriteLine("args[{0}] = {1}", i, args[i]); } static void Main() { F(); F(1); F(1, 2); F(1, 2, 3); F(new int[] {1, 2, 3, 4}); } }
muestra un método F que toma un número variable de argumentos int, y varias llamadas a este método. La salida es:
nº de argumentos: 0 nº de argumentos: 1 args[0] = 1 nº de argumentos: 2 args[0] = 1 args[1] = 2 nº de argumentos: 3 args[0] = 1 args[1] = 2 args[2] = 3 nº de argumentos: 4 args[0] = 1 args[1] = 2 args[2] = 3 args[3] = 4
La mayoría de los ejemplos presentes en este capítulo utilizan el método WriteLine de la clase Console. El comportamiento para las sustituciones, como muestra el ejemplo
int a = 1, b = 2; Console.WriteLine("a = {0}, b = {1}", a, b);
se consigue usando un arreglo de parámetros. El método WriteLine proporciona varios métodos sobrecargados para el caso común en el que se pasa un pequeño número de argumentos, y un método que usa un arreglo de parámetros.
using System; namespace System { public class Console { public static void WriteLine(string s) {...} public static void WriteLine(string s, object a) {...} public static void WriteLine(string s, object a, object b) {...} ... public static void WriteLine(string s, params object[] args) {...} } }
Modificadores public y static
[editar]El modificador public lo hemos utilizado anteriormente. Se puede utilizar en la declaración de cualquier método o variable, y como es de esperar, produce el efecto de que el campo afectado se vuelve público, esto es, se puede utilizar desde otras clases
using System; class Metodo{ public double Divide( double a, double b ) { return a/b; } } class Principal{ public static void Main() { Metodo m = new Metodo(); Console.WriteLine( m.Divide(8, 2) ); } }
Si por ejemplo intentamos declarar el método Divide sin el modificador public, obtendremos un error en tiempo de compilación. El modificador complementario de public es private, que provoca que el método o dato solo sea accesible desde la clase en la que está declarado. Si no se especifica nada, se toma por defecto el modificador private
De esta forma podríamos separar las clases Metodo y Principal en dos archivos separados, llamados por ejemplo metodo.cs y principal.cs . Para compilar esto, bastará compilar ambos archivos al mismo tiempo, de forma similar a esto: mcs principal.cs metodo.cs
Además, tampoco es necesario crear una instancia de la clase sólo para acceder a un método declarado en ella. Para eso debemos anteponer a la declaración del método el modificador static. Los métodos estáticos se caracterizan por no necesitar una instancia de la clase para cumplir su función, pero como contrapartida, no pueden acceder a datos propios de la clase.
using System; class Metodo{ public static double Divide( double a, double b ) { return a/b; } } class Principal{ public static void Main() { Console.WriteLine( Metodo.Divide(8, 2) ); } }
Los métodos estáticos se utilizan en multitud de situaciones. Por ejemplo, el método Console.WriteLine() o las funciones de la librería matemática estándar no son más que métodos estáticos de sus respectivas clases.
Constructores e instancias de una clase
[editar]Como hemos visto, las instancias de una clase se crean con la sintaxis
nombreclase objeto = new nombreclase( argumentos );
donde nombreclase es el nombre que le hemos dado a la definición de la clase, argumentos es una lista de argumentos posiblemente vacía y objeto es el nombre que queremos darle a la instancia de la clase.
Una vez creada una clase, sus miembros se inicializan a sus valores predeterminados ( cero para valores numéricos, cadena vacía para el tipo string, etc. ). La siguiente clase representa un punto sobre el plano, de forma que tiene dos valores públicos X e Y, y un método que calcula la distancia al origen del punto (módulo)
using System; class Punto{ public double X; public double Y; public double Modulo() { double d; d = Math.Sqrt(X*X + Y*Y); //Sqrt = raiz cuadrada return d; } } class Principal{ public static void Main() { Punto A = new Punto(); A.X = 1; A.Y = 1; Console.WriteLine("El modulo del punto (1,1) es: {0}", A.Modulo() ); } }
Ahora bien, la forma en la que se crea la instancia, es decir, inicializando los datos a cero (ejercicio: comprobar esto), se puede personalizar, de forma que podemos construir nuestro propio constructor que le diga a la clase los valores por defecto que debe tomar. Esto se realiza simplemente escribiendo dentro de la clase un método que tenga el mismo nombre que la clase y en el que no se especifica el valor devuelto. La clase Punto con un constructor sería así:
using System; class Punto{ public double X; public double Y; public Punto() //constructor { X = 1; Y = 1; } public double Modulo() { double d; d = Math.Sqrt(X*X + Y*Y); //Sqrt = raiz cuadrada return d; } }
de forma que ahora al crear una instancia de la clase se crea el punto (1,1) en lugar del (0,0), que era el que se creaba por defecto. De esta forma, al crear la instancia, par ya contendrá los valores (1,1) .
En la práctica se utilizan mucho constructores con parámetos, de forma que al crear la instancia se le asignan valores según los parámetros. La siguiente implementación de Par contiene un constructor que acepta un par de valores, que servirán para inicializar los valores A y B
class Punto{ public Punto( double val1, double val2) { X = val1; Y = val2; } ... }
También tenemos la posibilidad de declarar una clase con varios constructores (cada uno con diferentes parámetros) Lo que hará el compilador de C# es buscar el constructor que se adecúe a los parámetros que le llegan, y ejecutarlo como si fuera un método más. Dependiendo de la llamada que se haga en el "new", usaremos un constructor u otro.
Sobrecarga de métodos
[editar]En C#, al igual que en C++ y en Java es posible definir varios métodos con el mismo nombre pero con distintos parámetros, de forma que el compilador decide a cuál se llama dependiendo de los parámetros que le lleguen.
Esto es muy práctico, pues no tienes que renombrar cada función según el tipo de valor que acepta. El siguiente ejemplo implementa un par de métodos que elevan al cuadrado el valor que reciben, y se implementan para tipos double y para int. En C, que es un lenguaje que no soporta sobrecarga de métodos, se tendría que haber llamado distinto a ambos métodos, por ejemplo alcuadrado_double y alcuadrado_int
using System; class Eleva{ public static double AlCuadrado( int a ) { return a*a; } public static double AlCuadrado( double a ) { return a*a; } } class Principal{ public static void Main() { Console.WriteLine("4 al cuadrado es {0}", Eleva.AlCuadrado(4) ); Console.WriteLine("3.2 al cuadrado es {0}", Eleva.AlCuadrado(3.2) ); } }
La palabra reservada this
[editar]La palabra reservada this sirve para hacer referencia a miembros de la clase en caso de que se quiera especificar, ya sea por motivos de colisión de nombres o por la claridad del código. Su sintaxis es
this.campo
donde campo es la variable de la clase a la que queremos hacer referencia.
En el siguiente ejemplo, declaramos un constructor para la clase Punto, que toma dos argumentos X e Y. Entonces es obligado el uso de this para distinguir entre el X de la clase y el X tomado como parámetro
class Complejo { double X; double Y; Complejo(double X, double Y) { this.X = X; this.Y = Y; } }
Propiedades e indizadores
[editar]Propiedades
[editar]Las propiedades son una característica de C# que permiten aparentemente el acceso a un miembro de la clase mientras mantiene el control asociado al acceso mediante métodos.
Para los programadores de Java hay que decir que esto no es más que la formalización del patrón de asignación (setter) y método de lectura (getter)
Las propiedades son como métodos que se declaran dentro de un bloque asociado a una variable mediante las palabras reservadas get (se encarga de devolver algo cuando se llama al tipo que lo contiene ) y set (que hace algo cuando se le asigna un valor a la variable que lo contiene. Este valor viene especificado en la variable value )
using System; class TestProperties { private static string clave; public string Clave { get { Console.WriteLine ("Acceso a la propiedad clave"); return clave; } set { Console.WriteLine ("Cambio del valor de clave"); clave = value; } } } class Test { public static void Main () { TestProperties tp = new TestProperties(); string c = "ClaveClave"; tp.Clave = c; Console.WriteLine (tp.Clave); } }
En realidad, lo que se hace es declarar una variable privada de forma que no se puede acceder de forma directa, y se crean dos métodos ( o uno si solo se requiere acceso de lectura) que permiten acceder al contenido de la variable y tal vez modificarla. Si no queremos que se pueda modificar la variable, no incluímos el método "set" y ya tendríamos propiedades de sólo lectura.
Indexadores
[editar]Hemos visto, en el apartado en el que tratamos las propiedades, que podemos acceder a una variable privada de una clase a través de eventos que nos permiten controlar la forma en la que accedemos a dicha variable.
Los indexadores nos van a permitir hacer algo parecido. Nos van a permitir acceder a una clase como si se tratara de un arreglo. Lo vemos de forma más sencilla con un ejemplo:
using System; class PruebaIndexadores { private int[] tabla = {1, 2, 3, 4}; public int this [int indice] { get { Console.WriteLine ("La posicion {0} de la tabla tiene el valor {1}", indice, tabla[indice]); return tabla[indice]; } set { Console.WriteLine ("Escrito el valor {0} en la posición {1} de la tabla", value, indice); tabla[indice] = value; } } }
Tenemos una clase PruebaIndexadores en la que hay un array llamado "tabla", declarado como privado, por lo que no podremos acceder a él desde fuera de nuestra clase. Pero hemos declarado también un indexador (public int this [int indice]), que nos permitirá acceder a él de forma más controlada.
Para probar esta clase, creamos otra clase con un punto de entrada (public static void Main ()), que será donde hagamos las pruebas.
Primero creamos un objeto de la clase PruebaIndexadores:
PruebaIndexadores obj = new PruebaIndexadores ();
Luego accedemos a una posición del indexador:
int a = obj[3];
Esta línea lo que hace es llamar al indexador, pasándole como parámetro el índice, en este caso 3. Al ser una consulta de lectura, se ejecuta el código que haya en la parte "get" del indexador. Una vez ejecutado, lo que nos aparece por pantalla es esto:
La posicion 3 de la tabla tiene el valor 4
Vamos ahora a hacer un cambio en la tabla:
obj[3] = 6;
Lo que se ejecuta ahora es la parte "set" del indexador. Lo que aparecerá en pantalla una vez ejecutado esto será:
Escrito el valor 6 en la posición 3 de la tabla
Nótese que tenemos que hacer explícitamente el acceso al array (tabla[indice]=value) en el set, ya que el indexador no tiene forma de saber qué variable se supone que tiene que manejar. Si no pusiéramos esa línea, en realidad el indexador no cambiaría el valor del array.
Para comprobar que realmente se ha hecho el cambio, volvemos a acceder al indexador:
a = obj[3];
Y esta vez nos aparecerá esto:
La posicion 3 de la tabla tiene el valor 6.