+ All Categories
Home > Documents > Unidad II

Unidad II

Date post: 03-Jan-2016
Category:
Upload: klaus
View: 27 times
Download: 2 times
Share this document with a friend
54
Unidad II (5. Introducción a las funciones) 5.1. Diseño modular de programas: Descomposición modular Hasta ahora hemos estado pensando los pasos que deberíamos dar para resolver un cierto problema, y hemos creado programas a partir de cada uno de esos pasos. Esto es razonable cuando los problemas son sencillos, pero puede no ser la mejor forma de actuar cuando se trata de algo más complicado. A partir de ahora vamos a empezar a intentar descomponer los problemas en trozos más pequeños, que sean más fáciles de resolver. Esto nos puede suponer varias ventajas: Cada "trozo de programa" independiente será más fácil de programar, al realizar una función breve y concreta. El "programa principal" será más fácil de leer, porque no necesitará contener todos los detalles de cómo se hace cada cosa. Podremos repartir el trabajo, para que cada persona se encargue de realizar un "trozo de programa", y finalmente se integrará el trabajo individual de cada persona. Esos "trozos" de programa son lo que suele llamar "subrutinas", "procedimientos" o "funciones". En el lenguaje C y sus derivados, el nombre que más se usa es el de funciones. 5.2. Conceptos básicos sobre funciones En C#, al igual que en C y los demás lenguajes derivados de él, todos los "trozos de programa" son funciones, incluyendo el propio cuerpo de programa, Main. De hecho, la forma básica de definir una función será indicando su nombre seguido de unos paréntesis vacíos, como hacíamos con "Main", y precediéndolo por ciertas palabras reservadas, como "public static void", cuyo significado iremos viendo muy pronto. Después, entre llaves indicaremos todos los pasos que queremos que dé ese "trozo de programa". Por ejemplo, podríamos crear una función llamada "saludar", que escribiera varios mensajes en la pantalla: public static void saludar() { Console.WriteLine("Bienvenido al programa"); Console.WriteLine(" de ejemplo"); Console.WriteLine("Espero que estés bien"); } Ahora desde dentro del cuerpo de nuestro programa, podríamos "llamar" a esa función:
Transcript
Page 1: Unidad II

Unidad II (5. Introducción a las funciones)

5.1. Diseño modular de programas: Descomposición modular

Hasta ahora hemos estado pensando los pasos que deberíamos dar para resolver un cierto problema, y hemos creado programas a partir de cada uno de esos pasos. Esto es razonable cuando los problemas son sencillos, pero puede no ser la mejor forma de actuar cuando se trata de algo más complicado.

A partir de ahora vamos a empezar a intentar descomponer los problemas en trozos más pequeños, que sean más fáciles de resolver. Esto nos puede suponer varias ventajas:

Cada "trozo de programa" independiente será más fácil de programar, al realizar una función breve y concreta.

El "programa principal" será más fácil de leer, porque no necesitará contener todos los detalles de cómo se hace cada cosa.

Podremos repartir el trabajo, para que cada persona se encargue de realizar un "trozo de programa", y finalmente se integrará el trabajo individual de cada persona.

Esos "trozos" de programa son lo que suele llamar "subrutinas", "procedimientos" o "funciones". En el lenguaje C y sus derivados, el nombre que más se usa es el de funciones.

5.2. Conceptos básicos sobre funciones

En C#, al igual que en C y los demás lenguajes derivados de él, todos los "trozos de programa" son funciones, incluyendo el propio cuerpo de programa, Main. De hecho, la forma básica de definir una función será indicando su nombre seguido de unos paréntesis vacíos, como hacíamos con "Main", y precediéndolo por ciertas palabras reservadas, como "public static void", cuyo significado iremos viendo muy pronto. Después, entre llaves indicaremos todos los pasos que queremos que dé ese "trozo de programa".

Por ejemplo, podríamos crear una función llamada "saludar", que escribiera varios mensajes en la pantalla:

public static void saludar() { Console.WriteLine("Bienvenido al programa"); Console.WriteLine(" de ejemplo"); Console.WriteLine("Espero que estés bien"); }

Ahora desde dentro del cuerpo de nuestro programa, podríamos "llamar" a esa función:

Page 2: Unidad II

public static void Main() { saludar(); … }

Así conseguimos que nuestro programa principal sea más fácil de leer.

Un detalle importante: tanto la función habitual "Main" como la nueva función "Saludar" serían parte de nuestra "class", es decir, el fuente completo sería así:

/*---------------------------*/ /* Ejemplo en C# nº 47: */ /* ejemplo47.cs */ /* */ /* Funcion "saludar" */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo47 { public static void Saludar() { Console.WriteLine("Bienvenido al programa"); Console.WriteLine(" de ejemplo"); Console.WriteLine("Espero que estés bien"); } public static void Main() { Saludar(); Console.WriteLine("Nada más por hoy..."); } }

Como ejemplo más detallado, la parte principal de una agenda podría ser simplemente:

leerDatosDeFichero(); do { mostrarMenu(); pedirOpcion(); switch( opcion ) { case 1: buscarDatos(); break;

Page 3: Unidad II

case 2: modificarDatos(); break; case 3: anadirDatos(); break; …

5.3. Parámetros de una función

Es muy frecuente que nos interese además indicarle a nuestra función ciertos datos especiales con los que queremos que trabaje. Por ejemplo, si escribimos en pantalla números reales con frecuencia, nos puede resultar útil que nos los muestre con el formato que nos interese. Lo podríamos hacer así:

public static void escribeNumeroReal( float n ) { Console.WriteLine( n.ToString("#.###") ); }

Y esta función se podría usar desde el cuerpo de nuestro programa así:

escribeNumeroReal(2.3f);

(recordemos que el sufijo "f" es para indicar al compilador que trate ese número como un "float", porque de lo contrario, al ver que tiene cifras decimales, lo tomaría como "double", que permite mayor precisión... pero a cambio nosotros tendríamos un mensaje de error en nuestro programa, diciendo que estamos dando un dato "double" a una función que espera un "float").

El programa completo podría quedar así:

/*---------------------------*/ /* Ejemplo en C# nº 48: */ /* ejemplo48.cs */ /* */ /* Funcion */ /* "escribeNumeroReal" */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo48 { public static void escribeNumeroReal( float n ) { Console.WriteLine( n.ToString("#.###") );

Page 4: Unidad II

} public static void Main() { float x; x= 5.1f; Console.WriteLine("El primer numero real es: "); escribeNumeroReal(x); Console.WriteLine(" y otro distinto es: "); escribeNumeroReal(2.3f); } }

Estos datos adicionales que indicamos a la función es lo que llamaremos sus "parámetros". Como se ve en el ejemplo, tenemos que indicar un nombre para cada parámetro (puede haber varios) y el tipo de datos que corresponde a ese parámetro. Si hay más de un parámetro, deberemos indicar el tipo y el nombre para cada uno de ellos, y separarlos entre comas:

public static void sumar ( int x, int y ) { ... }

5.4. Valor devuelto por una función. El valor "void".

Cuando queremos dejar claro que una función no tiene que devolver ningún valor, podemos hacerlo indicando al principio que el tipo de datos va a ser "void" (nulo), como hacíamos hasta ahora con "Main" y como hicimos con nuestra función "saludar".

Pero eso no es lo que ocurre con las funciones matemáticas que estamos acostumbrados a manejar: sí devuelven un valor, el resultado de una operación.

De igual modo, para nosotros también será habitual que queramos que nuestra función realice una serie de cálculos y nos "devuelva" (return, en inglés) el resultado de esos cálculos, para poderlo usar desde cualquier otra parte de nuestro programa. Por ejemplo, podríamos crear una función para elevar un número entero al cuadrado así:

public static int cuadrado ( int n ) { return n*n; }

Page 5: Unidad II

y podríamos usar el resultado de esa función como si se tratara de un número o de una variable, así:

resultado = cuadrado( 5 );

Un programa más detallado de ejemplo podría ser:

/*---------------------------*/ /* Ejemplo en C# nº 49: */ /* ejemplo49.cs */ /* */ /* Funcion "cuadrado" */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo49 { public static int cuadrado ( int n ) { return n*n; } public static void Main() { int numero; int resultado; numero= 5; resultado = cuadrado(numero); Console.WriteLine("El cuadrado del numero {0} es {1}", numero, resultado); Console.WriteLine(" y el de 3 es {0}", cuadrado(3)); } }

Podemos hacer una función que nos diga cual es el mayor de dos números reales así:

public static float mayor ( float n1, float n2 ) { if (n1 > n2) return n1; else return n2; }

Page 6: Unidad II

Ejercicios propuestos:

Crear una función que borre la pantalla dibujando 25 líneas en blanco. No debe devolver ningún valor.

Crear una función que calcule el cubo de un número real (float). El resultado deberá ser otro número real. Probar esta función para calcular el cubo de 3.2 y el de 5.

Crear una función que calcule cual es el menor de dos números enteros. El resultado será otro número entero.

Crear una función llamada "signo", que reciba un número real, y devuelva un número entero con el valor: -1 si el número es negativo, 1 si es positivo o 0 si es cero.

Crear una función que devuelva la primera letra de una cadena de texto. Probar esta función para calcular la primera letra de la frase "Hola".

Crear una función que devuelva la última letra de una cadena de texto. Probar esta función para calcular la última letra de la frase "Hola".

Crear una función que reciba un número y muestre en pantalla el perímetro y la superficie de un cuadrado que tenga como lado el número que se ha indicado como parámetro.

5.5. Variables locales y variables globales

Hasta ahora, hemos declarado las variables dentro de "Main". Ahora nuestros programas tienen varios "bloques", así que se comportarán de forma distinta según donde declaremos las variables.

Las variables se pueden declarar dentro de un bloque (una función), y entonces sólo ese bloque las conocerá, no se podrán usar desde ningún otro bloque del programa. Es lo que llamaremos "variables locales".

Por el contrario, si declaramos una variable al comienzo del programa, fuera de todos los "bloques" de programa, será una "variable global", a la que se podrá acceder desde cualquier parte.

Vamos a verlo con un ejemplo. Crearemos una función que calcule la potencia de un número entero (un número elevado a otro), y el cuerpo del programa que la use.

La forma de conseguir elevar un número a otro será a base de multiplicaciones, es decir:

Page 7: Unidad II

3 elevado a 5 = 3 • 3 • 3 • 3 • 3

(multiplicamos 5 veces el 3 por sí mismo). En general, como nos pueden pedir cosas como "6 elevado a 100" (o en general números que pueden ser grandes), usaremos la orden "for" para multiplicar tantas veces como haga falta:

/*---------------------------*/ /* Ejemplo en C# nº 50: */ /* ejemplo50.cs */ /* */ /* Ejemplo de función con */ /* variables locales */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo50 { public static int potencia(int nBase, int nExponente) { int temporal = 1; /* Valor que voy hallando */ int i; /* Para bucles */ for(i=1; i<=nExponente; i++) /* Multiplico "n" veces */ temporal *= nBase; /* Y calculo el valor temporal */ return temporal; /* Tras las multiplicaciones, */ } /* obtengo el valor que buscaba */ public static void Main() { int num1, num2; Console.WriteLine("Introduzca la base: "); num1 = Convert.ToInt32( Console.ReadLine() ); Console.WriteLine("Introduzca el exponente: "); num2 = Convert.ToInt32( Console.ReadLine() ); Console.WriteLine("{0} elevado a {1} vale {2}", num1, num2, potencia(num1,num2)); } }

Page 8: Unidad II

En este caso, las variables "temporal" e "i" son locales a la función "potencia": para "Main" no existen. Si en "Main" intentáramos hacer i=5; obtendríamos un mensaje de error.

De igual modo, "num1" y "num2" son locales para "main": desde la función "potencia" no podemos acceder a su valor (ni para leerlo ni para modificarlo), sólo desde "main".

En general, deberemos intentar que la mayor cantidad de variables posible sean locales (lo ideal sería que todas lo fueran). Así hacemos que cada parte del programa trabaje con sus propios datos, y ayudamos a evitar que un error en un trozo de programa pueda afectar al resto. La forma correcta de pasar datos entre distintos trozos de programa es usando los parámetros de cada función, como en el anterior ejemplo.

Ejercicios propuestos:

Crear una función "pedirEntero", que reciba como parámetros el texto que se debe mostrar en pantalla, el valor mínimo aceptable y el valor máximo aceptable. Deberá pedir al usuario que introduzca el valor tantas veces como sea necesario, volvérselo a pedir en caso de error, y devolver un valor correcto. Probarlo con un programa que pida al usuario un año entre 1800 y 2100.

Crear una función "escribirTablaMultiplicar", que reciba como parámetro un número entero, y escriba la tabla de multiplicar de ese número (por ejemplo, para el 3 deberá llegar desde 3x0=0 hasta 3x10=30).

Crear una función "esPrimo", que reciba un número y devuelva el valor booleano "true" si es un número primo o "false" en caso contrario.

Crear una función que reciba una cadena y una letra, y devuelva la cantidad de veces que dicha letra aparece en la cadena. Por ejemplo, si la cadena es "Barcelona" y la letra es 'a', debería devolver 2 (aparece 2 veces).

Crear una función que reciba un numero cualquiera y que devuelva como resultado la suma de sus dígitos. Por ejemplo, si el número fuera 123 la suma sería 6.

Crear una función que reciba una letra y un número, y escriba un "triángulo" formado por esa letra, que tenga como anchura inicial la que se ha indicado. Por ejemplo, si la letra es * y la anchura es 4, debería escribir

Page 9: Unidad II

****

***

**

*

5.6. Los conflictos de nombres en las variables

¿Qué ocurre si damos el mismo nombre a dos variables locales? Vamos a comprobarlo con un ejemplo:

/*---------------------------*/ /* Ejemplo en C# nº 51: */ /* ejemplo51.cs */ /* */ /* Dos variables locales */ /* con el mismo nombre */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo51 { public static void cambiaN() { int n = 7; n ++; } public static void Main() { int n = 5; Console.WriteLine("n vale {0}", n); cambiaN(); Console.WriteLine("Ahora n vale {0}", n); } }

El resultado de este programa es:

n vale 5 Ahora n vale 5

Page 10: Unidad II

¿Por qué? Sencillo: tenemos una variable local dentro de "duplica" y otra dentro de "main". El hecho de que las dos tengan el mismo nombre no afecta al funcionamiento del programa, siguen siendo distintas.

Si la variable es "global", declarada fuera de estas funciones, sí será accesible por todas ellas:

/*---------------------------*/ /* Ejemplo en C# nº 52: */ /* ejemplo52.cs */ /* */ /* Una variable global */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo52 { static int n = 7; public static void cambiaN() { n ++; } public static void Main() { Console.WriteLine("n vale {0}", n); cambiaN(); Console.WriteLine("Ahora n vale {0}", n); } }

Dentro de poco, hablaremos de por qué cada uno de los bloques de nuestro programa, e incluso las "variables globales", tienen delante la palabra "static". Será cuando tratemos la "Programación Orientada a Objetos", en el próximo tema.

5.7. Modificando parámetros

Podemos modificar el valor de un dato que recibamos como parámetro, pero posiblemente el resultado no será el que esperamos. Vamos a verlo con un ejemplo:

/*---------------------------*/ /* Ejemplo en C# nº 53: */

Page 11: Unidad II

/* ejemplo53.cs */ /* */ /* Modificar una variable */ /* recibida como parámetro */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo53 { public static void duplica(int x) { Console.WriteLine(" El valor recibido vale {0}", x); x = x * 2; Console.WriteLine(" y ahora vale {0}", x); } public static void Main() { int n = 5; Console.WriteLine("n vale {0}", n); duplica(n); Console.WriteLine("Ahora n vale {0}", n); } }

El resultado de este programa será:

n vale 5 El valor recibido vale 5 y ahora vale 10 Ahora n vale 5

Vemos que al salir de la función, los cambios que hagamos a esa variable que se ha recibido como parámetro no se conservan.

Esto se debe a que, si no indicamos otra cosa, los parámetros "se pasan por valor", es decir, la función no recibe los datos originales, sino una copia de ellos. Si modificamos algo, estamos cambiando una copia de los datos originales, no dichos datos.

Si queremos que los cambios se conserven, basta con hacer un pequeño cambio: indicar que la variable se va a pasar "por referencia", lo que se indica usando la palabra "ref", tanto en la declaración de la función como en la llamada, así:

/*---------------------------*/ /* Ejemplo en C# nº 54: */

Page 12: Unidad II

/* ejemplo54.cs */ /* */ /* Modificar una variable */ /* recibida como parámetro */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo54 { public static void duplica(ref int x) { Console.WriteLine(" El valor recibido vale {0}", x); x = x * 2; Console.WriteLine(" y ahora vale {0}", x); } public static void Main() { int n = 5; Console.WriteLine("n vale {0}", n); duplica(ref n); Console.WriteLine("Ahora n vale {0}", n); } }

En este caso sí se modifica la variable n:

n vale 5 El valor recibido vale 5 y ahora vale 10 Ahora n vale 10

El hecho de poder modificar valores que se reciban como parámetros abre una posibilidad que no se podría conseguir de otra forma: con "return" sólo se puede devolver un valor de una función, pero con parámetros pasados por referencia podríamos devolver más de un dato. Por ejemplo, podríamos crear una función que intercambiara los valores de dos variables:

public static void intercambia(ref int x, ref int y)

La posibilidad de pasar parámetros por valor y por referencia existe en la mayoría de lenguajes de programación. En el caso de C# existe alguna posibilidad adicional que no existe en otros lenguajes, como los "parámetros de salida". Las veremos más adelante.

Page 13: Unidad II

Ejercicios propuestos:

Crear una función "intercambia", que intercambie el valor de los dos números enteros que se le indiquen como parámetro.

Crear una función "iniciales", que reciba una cadena como "Nacho Cabanes" y devuelva las letras N y C (primera letra, y letra situada tras el primer espacio), usando parámetros por referencia.

5.8. El orden no importa

En algunos lenguajes, una función debe estar declarada antes de usarse. Esto no es necesario en C#. Por ejemplo, podríamos rescribir el fuente anterior, de modo que "Main" aparezca en primer lugar y "duplica" aparezca después, y seguiría compilando y funcionando igual:

/*---------------------------*/ /* Ejemplo en C# nº 55: */ /* ejemplo55.cs */ /* */ /* Función tras Main */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo55 { public static void Main() { int n = 5; Console.WriteLine("n vale {0}", n); duplica(ref n); Console.WriteLine("Ahora n vale {0}", n); } public static void duplica(ref int x) { Console.WriteLine(" El valor recibido vale {0}", x); x = x * 2; Console.WriteLine(" y ahora vale {0}", x); } }

5.9. Algunas funciones útiles

Page 14: Unidad II

5.9.1. Números aleatorios

En un programa de gestión o una utilidad que nos ayuda a administrar un sistema, no es habitual que podamos permitir que las cosas ocurran al azar. Pero los juegos se encuentran muchas veces entre los ejercicios de programación más completos, y para un juego sí suele ser conveniente que haya algo de azar, para que una partida no sea exactamente igual a la anterior.

Generar números al azar ("números aleatorios") usando C# no es difícil: debemos crear un objeto de tipo "Random", y luego llamaremos a "Next" para obtener valores entre dos extremos:

// Creamos un objeto random Random r = new Random(); // Generamos un número entre dos valores dados int aleatorio = r.Next(1, 100);

Podemos hacer que sea realmente un poco más aleatorio si en la primera orden le indicamos que tome como semilla el instante actual:

Random r = new Random(DateTime.Now.Millisecond);

De hecho, una forma muy simple de obtener un número "casi al azar" entre 0 y 999 es tomar las milésimas de segundo de la hora actual:

int falsoAleatorio = DateTime.Now.Millisecond;

Vamos a ver un ejemplo, que muestre en pantalla un número al azar entre 1 y 10:

/*---------------------------*/ /* Ejemplo en C# nº 56: */ /* ejemplo56.cs */ /* */ /* Números al azar */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/

Page 15: Unidad II

using System; public class Ejemplo56 { public static void Main() { Random r = new Random(DateTime.Now.Millisecond); int aleatorio = r.Next(1, 10); Console.WriteLine("Un número entre 1 y 10: {0}", aleatorio); } }

Ejercicios propuestos:

Crear un programa que genere un número al azar entre 1 y 100. El usuario tendrá 6 oportunidades para acertarlo.

Mejorar el programa del ahorcado propuesto en el apartado 4.4.8, para que la palabra a adivinar no sea tecleado por un segundo usuario, sino que se escoja al azar de un "array" de palabras prefijadas (por ejemplo, nombres de ciudades).

5.9.2. Funciones matemáticas

En C# tenemos muchas funciones matemáticas predefinidas, como:

Abs(x): Valor absoluto Acos(x): Arco coseno Asin(x): Arco seno Atan(x): Arco tangente Atan2(y,x): Arco tangente de y/x (por si x o y son 0) Ceiling(x): El valor entero superior a x y más cercano a él Cos(x): Coseno Cosh(x): Coseno hiperbólico Exp(x): Exponencial de x (e elevado a x) Floor(x): El mayor valor entero que es menor que x Log(x): Logaritmo natural (o neperiano, en base "e") Log10(x): Logaritmo en base 10 Pow(x,y): x elevado a y Round(x, cifras): Redondea un número Sin(x): Seno Sinh(x): Seno hiperbólico

Page 16: Unidad II

Sqrt(x): Raíz cuadrada Tan(x): Tangente Tanh(x): Tangente hiperbólica

(casi todos ellos usan parámetros X e Y de tipo "double")

y una serie de constantes como

E, el número "e", con un valor de 2.71828... PI, el número "Pi", 3.14159...

Todas ellas se usan precedidas por "Math."

La mayoría de ellas son específicas para ciertos problemas matemáticos, especialmente si interviene la trigonometría o si hay que usar logaritmos o exponenciales. Pero vamos a destacar las que sí pueden resultar útiles en situaciones más variadas:

La raíz cuadrada de 4 se calcularía haciendo x = Math.Sqrt(4); La potencia: para elevar 2 al cubo haríamos y = Math.Pow(2, 3); El valor absoluto: para trabajar sólo con números positivos

usaríamos n = Math.Abs(x);

Ejercicios propuestos:

Crear un programa que halle cualquier raíz de un número. El usuario deberá indicar el número (por ejemplo, 2) y el índice de la raíz (por ejemplo, 3 para la raíz cúbica). Pista: hallar la raíz cúbica de 2 es lo mismo que elevar 2 a 1/3.

Crear un programa que resuelva ecuaciones de segundo grado, del tipo ax2 + bx + c = 0 El usuario deberá introducir los valores de a, b y c. Pista: la solución se calcula con x = -b ? raíz (b2 – 4•a•c) / 2•a

5.9.3. Pero hay muchas más funciones…

PPero en C# hay muchas más funciones de lo que parece. De hecho, salvo algunas palabras reservadas (int, float, string, if, switch, for, do, while...), gran parte de lo que hasta ahora hemos llamado "órdenes", son realmente "funciones", como Console.ReadLine o Console.WriteLine. Nos iremos encontrando con otras funciones a medida que avancemos.

5.10. Recursividad

Page 17: Unidad II

Una función recursiva es aquella que se define a partir de ella misma. Dentro de las matemáticas tenemos varios ejemplos. Uno clásico es el "factorial de un número":

n! = n • (n-1) • (n-2) • ... • 3 • 2 • 1

(por ejemplo, el factorial de 4 es 4 • 3 • 2 • 1 = 24)

Si pensamos que el factorial de n-1 es

(n-1)! = (n-1) • (n-2) • (n-3) • ... • 3 • 2 • 1

Entonces podemos escribir el factorial de un número a partir del factorial del siguiente número:

n! = n • (n-1)!

Esta es la definición recursiva del factorial, ni más ni menos. Esto, programando, se haría:

/*---------------------------*/ /* Ejemplo en C# nº 57: */ /* ejemplo57.cs */ /* */ /* Funciones recursivas: */ /* factorial */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo57 { public static long fact(int n) { if (n==1) // Aseguramos que termine return 1; return n * fact (n-1); // Si no es 1, sigue la recursión } public static void Main() { int num; Console.WriteLine("Introduzca un número entero: "); num = System.Convert.ToInt32(System.Console.ReadLine()); Console.WriteLine("Su factorial es: {0}", fact(num)); } }

Page 18: Unidad II

Dos consideraciones importantes:

Atención a la primera parte de la función recursiva: es MUY IMPORTANTE comprobar que hay salida de la función, para que nuestro programa no se quede dando vueltas todo el tiempo y deje el ordenador (o la tarea actual) "colgado".

Los factoriales crecen rápidamente, así que no conviene poner números grandes: el factorial de 16 es 2.004.189.184, luego a partir de 17 podemos obtener resultados erróneos, si usamos números enteros "normales".

¿Qué utilidad tiene esto? Pues más de la que parece: muchos problemas complicados se pueden expresar a partir de otro más sencillo. En muchos de esos casos, ese problema se podrá expresar de forma recursiva. Más adelante veremos algún otro ejemplo.

Ejercicios propuestos:

Crear una función que calcule el valor de elevar un número entero a otro número entero (por ejemplo, 5 elevado a 3 = 53 = 5 •5 •5 = 125). Esta función se debe crear de forma recursiva.

Como alternativa, crear una función que calcule el valor de elevar un número entero a otro número entero de forma NO recursiva (lo que llamaremos "de forma iterativa"), usando la orden "for".

Crear un programa que emplee recursividad para calcular un número de la serie Fibonacci (en la que los dos primeros elementos valen 1, y para los restantes, cada elemento es la suma de los dos anteriores).

Crear un programa que emplee recursividad para calcular la suma de los elementos de un vector.

Crear un programa que emplee recursividad para calcular el mayor de los elementos de un vector.

Crear un programa que emplee recursividad para dar la vuelta a una cadena de caracteres (por ejemplo, a partir de "Hola" devolvería "aloH").

Crear, tanto de forma recursiva como de forma iterativa, una función diga si una cadena de caracteres es simétrica (un palíndromo). Por ejemplo, "DABALEARROZALAZORRAELABAD" es un palíndromo.

Crear un programa que encuentre el máximo común divisor de dos números usando el algoritmo de Euclides: Dados dos números enteros positivos m y n, tal que m > n, para encontrar su máximo común divisor, es decir, el mayor entero positivo que divide a ambos:

Page 19: Unidad II

- Dividir m por n para obtener el resto r (0 <= r < n) ; - Si r = 0, el MCD es n.; - Si no, el máximo común divisor es MCD(n,r).

5.11. Parámetros y valor de retorno de "Main"

Es muy frecuente que un programa que usamos desde la "línea de comandos" tenga ciertas opciones que le indicamos como argumentos. Por ejemplo, bajo Linux o cualquier otro sistema operativo de la familia Unix, podemos ver la lista detallada de ficheros que terminan en .c haciendo

ls –l *.c

En este caso, la orden sería "ls", y las dos opciones (argumentos o parámetros) que le indicamos son "-l" y "*.c".

La orden equivalente en MsDos y en el intérprete de comandos de Windows sería

dir *.c

Ahora la orden sería "dir", y el parámetro es "*.c".

Pues bien, estas opciones que se le pasan al programa se pueden leer desde C#. Se hace indicando un parámetro especial en Main, un array de strings:

static void Main(string[] args)

Para conocer esos parámetros lo haríamos de la misma forma que se recorre habitualmente un array cuyo tamaño no conocemos: con un "for" que termine en la longitud ("Length") del array:

for (int i = 0; i < args.Length; i++) { System.Console.WriteLine("El parametro {0} es: {1}", i, args[i]); }

Por otra parte, si queremos que nuestro programa se interrumpa en un cierto punto, podemos usar la orden "Environment.Exit". Su manejo habitual es algo como

Environment.Exit(1);

Page 20: Unidad II

Es decir, entre paréntesis indicamos un cierto código, que suele ser (por convenio) un 0 si no ha habido ningún error, u otro código distinto en caso de que sí exista algún error.

Este valor se podría comprobar desde el sistema operativo. Por ejemplo, en MsDos y Windows se lee con "IF ERRORLEVEL", así:

IF ERRORLEVEL 1 ECHO Ha habido un error en el programa

Una forma alternativa de que "Main" indique errores al sistema operativo es no declarándolo como "void", sino como "int", y empleando entonces la orden "return" cuando nos interese:

public static int Main(string[] args) { ... return 1; }

Un ejemplo que pusiera todo esto en prueba podría ser:

/*---------------------------*/ /* Ejemplo en C# nº 58: */ /* ejemplo58.cs */ /* */ /* Parámetros y valor de */ /* retorno de "Main" */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Ejemplo58 { public static int Main(string[] args) { Console.WriteLine("Parámetros: {0}", args.Length); for (int i = 0; i < args.Length; i++) { Console.WriteLine("El parámetro {0} es: {1}", i, args[i]); } if (args.Length == 0) { Console.WriteLine("Escriba algún parámetro!");

Page 21: Unidad II

Environment.Exit(1); } return 0; } }

Ejercicios propuestos:

Crear un programa llamado "suma", que calcule (y muestre) la suma de dos números que se le indiquen como parámetro. Por ejemplo, si se teclea "suma 2 3" deberá responder "5", y si se teclea "suma 2" deberá responder "no hay suficientes datos y devolver un código de error 1.

Crear una calculadora básica, llamada "calcula", que deberá sumar, restar, multiplicar o dividir los dos números que se le indiquen como parámetros. Ejemplos de su uso sería "calcula 2 + 3" o "calcula 5 * 60".

6. Programación orientada a objetos

6.1. ¿Por qué los objetos?

Hasta ahora hemos estado "cuadriculando" todo para obtener algoritmos: tratábamos de convertir cualquier cosa en una función que pudiéramos emplear en nuestros programas. Cuando teníamos claros los pasos que había que dar, buscábamos las variables necesarias para dar esos pasos.

Pero no todo lo que nos rodea es tan fácil de cuadricular. Supongamos por ejemplo que tenemos que introducir datos sobre una puerta en nuestro programa. ¿Nos limitamos a programar los procedimientos AbrirPuerta y CerrarPuerta? Al menos, deberíamos ir a la zona de declaración de variables, y allí guardaríamos otras datos como su tamaño, color, etc.

No está mal, pero es "antinatural": una puerta es un conjunto: no podemos separar su color de su tamaño, o de la forma en que debemos abrirla o cerrarla. Sus características son tanto las físicas (lo que hasta ahora llamábamos variables) como sus comportamientos en distintas circunstancias (lo que para nosotros eran las funciones). Todo ello va unido, formando un "objeto".

Page 22: Unidad II

Por otra parte, si tenemos que explicar a alguien lo que es el portón de un garaje, y ese alguien no lo ha visto nunca, pero conoce cómo es la puerta de su casa, le podemos decir "se parece a una puerta de una casa, pero es más grande para que quepan los coches, está hecha de metal en vez de madera...". Es decir, podemos describir unos objetos a partir de lo que conocemos de otros.

Finalmente, conviene recordar que "abrir" no se refiere sólo a una puerta. También podemos hablar de abrir una ventana o un libro, por ejemplo.

Con esto, hemos comentado casi sin saberlo las tres características más importantes de la Programación Orientada a Objetos (OOP):

Encapsulación: No podemos separar los comportamientos de las características de un objeto. Los comportamientos serán funciones, que en OOP llamaremos métodos. Las características de un objeto serán variables, como las que hemos usado siempre (las llamaremos atributos). La apariencia de un objeto en C#, como veremos un poco más adelante, recordará a un registro o "struct".

Herencia: Unos objetos pueden heredar métodos y datos de otros. Esto hace más fácil definir objetos nuevos a partir de otros que ya teníamos anteriormente (como ocurría con el portón y la puerta) y facilitará la reescritura de los programas, pudiendo aprovechar buena parte de los anteriores... si están bien diseñados.

Polimorfismo: Un mismo nombre de un método puede hacer referencia a comportamientos distintos (como abrir una puerta o un libro). Igual ocurre para los datos: el peso de una puerta y el de un portón los podemos llamar de igual forma, pero obviamente no valdrán lo mismo.

Otro concepto importante es el de "clase": Una clase es un conjunto de objetos que tienen características comunes. Por ejemplo, tanto mi puerta como la de mi vecino son puertas, es decir, ambas son objetos que pertenecen a la clase "puerta". De igual modo, tanto un Ford Focus como un Honda Civic o un Toyota Corolla son objetos concretos que pertenecen a la clase "coche".

6.2. Objetos y clases en C#

Page 23: Unidad II

Vamos con los detalles. Las clases en C# se definen de forma parecida a los registros (struct), sólo que ahora también incluirán funciones. Así, la clase "Puerta" que mencionábamos antes se podría declarar así:

public class Puerta { int ancho; // Ancho en centimetros int alto; // Alto en centimetros int color; // Color en formato RGB bool abierta; // Abierta o cerrada public void Abrir() { abierta = true; } public void Cerrar() { abierta = false; } public void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); } } // Final de la clase Puerta

Como se puede observar, los objetos de la clase "Puerta" tendrán un ancho, un alto, un color, y un estado (abierta o no abierta), y además se podrán abrir o cerrar (y además, nos pueden "mostrar su estado, para comprobar que todo funciona correctamente).

Para declarar estos objetos que pertenecen a la clase "Puerta", usaremos la palabra "new", igual que hacíamos con los "arrays":

Puerta p = new Puerta(); p.Abrir(); p.MostrarEstado();

Vamos a completar un programa de prueba que use un objeto de esta clase (una "Puerta"): /*---------------------------*/ /* Ejemplo en C# nº 59: */ /* ejemplo59.cs */ /* */ /* Primer ejemplo de clases */ /* */ /* Introduccion a C#, */

Page 24: Unidad II

/* Nacho Cabanes */ /*---------------------------*/ using System; public class Puerta { int ancho; // Ancho en centimetros int alto; // Alto en centimetros int color; // Color en formato RGB bool abierta; // Abierta o cerrada public void Abrir() { abierta = true; } public void Cerrar() { abierta = false; } public void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); } } // Final de la clase Puerta public class Ejemplo59 { public static void Main() { Puerta p = new Puerta(); Console.WriteLine("Valores iniciales..."); p.MostrarEstado(); Console.WriteLine("\nVamos a abrir..."); p.Abrir(); p.MostrarEstado(); } }

Este fuente ya no contiene una única clase (class), como todos nuestros ejemplos anteriores, sino dos clases distintas:

Page 25: Unidad II

La clase "Puerta", que son los nuevos objetos con los que vamos a practicar.

La clase "Ejemplo59", que representa a nuestra aplicación.

El resultado de ese programa es el siguiente:

Valores iniciales... Ancho: 0 Alto: 0 Color: 0 Abierta: False Vamos a abrir... Ancho: 0 Alto: 0 Color: 0 Abierta: True

Se puede ver que en C#, al contrario que en otros lenguajes, las variables que forman parte de una clase (los "atributos") tienen un valor inicial predefinido: 0 para los números, una cadena vacía para las cadenas de texto, o "False" para los datos booleanos.

Vemos también que se accede a los métodos y a los datos precediendo el nombre de cada uno por el nombre de la variable y por un punto, como hacíamos con los registros (struct). Aun así, en nuestro caso no podemos hacer directamente "p.abierta = true", por dos motivos:

El atributo "abierta" no tiene delante la palabra "public"; por lo que no es público, sino privado, y no será accesible desde otras clases (en nuestro caso, desde Ejemplo59).

Los puristas de la Programación Orientada a Objetos recomiendan que no se acceda directamente a los atributos, sino que siempre se modifiquen usando métodos auxiliares (por ejemplo, nuestro "Abrir"), y que se lea su valor también usando una función. Esto es lo que se conoce como "ocultación de datos". Supondrá ventajas como que podremos cambiar los detalles internos de nuestra clase sin que afecte a su uso.

También puede desconcertar que en "Main" aparezca la palabra "static", mientras que no lo hace en los métodos de la clase "Puerta". Veremos este detalle un poco más adelante.

Ejercicio propuesto : Crear una clase llamada Persona, en el fichero "persona.cs". Esta

clase deberá tener un atributo "nombre", de tipo string. También deberá tener un método "SetNombre", de tipo void y con un

Page 26: Unidad II

parámetro string, que permita cambiar el valor del nombre. Finalmente, también tendrá un método "Saludar", que escribirá en pantalla "Hola, soy " seguido de su nombre. Crear también una clase llamada PruebaPersona, en el fichero "pruebaPersona.cs". Esta clase deberá contener sólo la función Main, que creará dos objetos de tipo Persona, les asignará un nombre y les pedirá que saluden.

6.3. La herencia. Visibilidad

Vamos a ver ahora cómo definir una nueva clase de objetos a partir de otra ya existente. Por ejemplo, vamos a crear una clase "Porton" a partir de la clase "Puerta". Un portón tendrá las mismas características que una puerta (ancho, alto, color, abierto o no), pero además se podrá bloquear, lo que supondrá un nuevo atributo y nuevos métodos para bloquear y desbloquear:

public class Porton: Puerta { bool bloqueada; public void Bloquear() { bloqueada = true; } public void Desbloquear() { bloqueada = false; }

Con "public class Porton: Puerta" indicamos que Porton debe "heredar" todo lo que ya habíamos definido para Puerta. Por eso, no hace falta indicar nuevamente que un Portón tendrá un cierto ancho, o un color, o que se puede abrir: todo eso lo tiene por ser un "descendiente" de Puerta.

No tenemos por qué heredar todo; también podemos "redefinir" algo que ya existía. Por ejemplo, nos puede interesar que "MostrarEstado" ahora nos diga también si la puerta está bloqueada. Para eso, basta con volverlo a declarar y añadir la palabra "new" para indicar al compilador de C# que sabemos que ya existe ese método y que sabemos seguro que lo queremos redefinir:

Page 27: Unidad II

public new void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); Console.WriteLine("Bloqueada: {0}", bloqueada); }

Aun así, esto todavía no funciona: los atributos de una Puerta, como el "ancho" y el "alto" estaban declarados como "privados" (es lo que se considera si no decimos los contrario), por lo que no son accesibles desde ninguna otra clase, ni siquiera desde Porton.

La solución más razonable no es declararlos como "public", porque no queremos que sean accesibles desde cualquier sitio. Sólo querríamos que esos datos estuvieran disponibles para todos los tipos de Puerta, incluyendo sus "descendientes", como un Porton. Esto se puede conseguir usando otro método de acceso: "protected". Todo lo que declaremos como "protected" será accesible por las clases derivadas de la actual, pero por nadie más:

public class Puerta { protected int ancho; // Ancho en centimetros protected int alto; // Alto en centimetros protected int color; // Color en formato RGB protected bool abierta; // Abierta o cerrada public void Abrir() ...

(Si quisiéramos dejar claro que algún elemento de una clase debe ser totalmente privado, podemos usar la palabra "private", en vez de "public" o "protected").

Un fuente completo que declarase la clase Puerta, la clase Porton a partir de ella, y que además contuviese un pequeño "Main" de prueba podría ser:

/*---------------------------*/ /* Ejemplo en C# nº 60: */ /* ejemplo60.cs */ /* */ /* Segundo ejemplo de */

Page 28: Unidad II

/* clases: herencia */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; // ------------------------------- public class Puerta { protected int ancho; // Ancho en centimetros protected int alto; // Alto en centimetros protected int color; // Color en formato RGB protected bool abierta; // Abierta o cerrada public void Abrir() { abierta = true; } public void Cerrar() { abierta = false; } public void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); } } // Final de la clase Puerta // ------------------------------- public class Porton: Puerta { bool bloqueada; public void Bloquear() { bloqueada = true; } public void Desbloquear() { bloqueada = false; } public new void MostrarEstado() {

Page 29: Unidad II

Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); Console.WriteLine("Bloqueada: {0}", bloqueada); } } // Final de la clase Porton // ------------------------------- public class Ejemplo60 { public static void Main() { Porton p = new Porton(); Console.WriteLine("Valores iniciales..."); p.MostrarEstado(); Console.WriteLine("\nVamos a bloquear y a abrir..."); p.Bloquear(); p.MostrarEstado(); Console.WriteLine("\nVamos a desbloquear y a abrir..."); p.Abrir(); p.Desbloquear(); p.MostrarEstado(); } }

6.4. ¿Cómo se diseñan las clases?

En estos primeros ejemplos, hemos "pensado" qué objetos necesitaríamos, y hemos empezado a teclear directamente para implementarlos. Esto no es lo habitual. Normalmente, se usan herramientas gráficas que nos ayuden a visualizar las clases y las relaciones que existen entre ellas. También se puede dibujar directamente en papel para aclararnos las ideas, pero el empleo de herramientas informáticas tiene una ventaja adicional: algunas de ellas nos permiten generar automáticamente un esqueleto del programa.

La metodología más extendida actualmente para diseñar estos objetos y sus interacciones (además de otras muchas cosas) se conoce como UML (Unified Modelling Language, lenguaje de modelado unificado). El estándar UML propone distintos tipos de diagramas para representar los posibles "casos de uso" de una aplicación, la secuencia de acciones que se debe seguir, las clases que la van a integrar (es lo que a nosotros nos interesa en este momento), etc.

Page 30: Unidad II

Vamos a ver la apariencia que tendría un "diagrama de clases". En concreto, vamos a ver un ejemplo usando ArgoUML, que es una herramienta gratuita de modelado UML, que está creada en Java, por lo que se puede utilizar desde multitud de sistemas operativos.

Ampliando el ejemplo anterior, vamos a suponer que queremos hacer un sistema "domótico", para automatizar ciertas funciones en una casa: apertura y cierre de ventanas y puertas, encendido de calefacción, etc.

Las ideas iniciales de las que partiremos son:

La casa es el conjunto ("agregación") de varios elementos: puertas, ventanas y calefactores.

Cada puerta se puede abrir y cerrar. Cada ventana se puede abrir, cerrar. Además, las ventanas tienen

persianas, que se pueden subir y bajar. Cada calefactor puede encenderse, apagarse o se puede programar

para que trabaje a una cierta temperatura.

Con estas posibilidades básicas, el diagrama de clases podría ser así:

Este diagrama es una forma más simple de ver las clases existentes y las relaciones entre ellas. Si generamos las clases a partir del diagrama, tendremos parte del trabajo hecho: ya "sólo" nos quedará rellenar los

Page 31: Unidad II

detalles de métodos como "Abrir", pero el esqueleto de todas las clases ya estará "escrito" para nosotros.

6.5. La palabra "static"

Desde un principio, nos hemos encontrado con que "Main" siempre iba acompañado de la palabra "static". En cambio, los métodos (funciones) que pertenecen a nuestros objetos no los estamos declarando como "static". Vamos a ver el motivo:

La palabra "static" delante de un atributo (una variable) de una clase, indica que es una "variable de clase", es decir, que su valor es el mismo para todos los objetos de la clase. Por ejemplo, si hablamos de coches convencionales, podríamos suponer que el atributo "numeroDeRuedas" va a valer 4 para cualquier objeto que pertenezca a esa clase (cualquier coches). Por eso, se podría declarar como "static".

De igual modo, si un método (una función) está precedido por la palabra "static", indica que es un "método de clase", es decir, un método que se podría usar sin necesidad de declarar ningún objeto de la clase. Por ejemplo, si queremos que se pueda usar la función "BorrarPantalla" de una clase "Hardware" sin necesidad de crear primero un objeto perteneciente a esa clase, lo podríamos conseguir así:

public class Hardware { ... public static void BorrarPantalla () { ... }

que desde dentro de "Main" (incluso perteneciente a otra clase) se usaría con el nombre de la clase delante:

public class Juego { ... public ComienzoPartida() { Hardware.BorrarPantalla (); ...

Page 32: Unidad II

Desde una función "static" no se puede llamar a otras funciones que no lo sean. Por eso, como nuestro "Main" debe ser static, deberemos siempre elegir entre:

Que todas las demás funciones de nuestro fuente también estén declaradas como "static", por lo que podrán ser utilizadas desde "Main".

Declarar un objeto de la clase correspondiente, y entonces sí podremos acceder a sus métodos desde "Main":

public class Ejemplo { ... public LanzarJuego () { Juego j = new Juego(); j.ComienzoPartida (); ...

6.6. Constructores y destructores.

Hemos visto que al declarar una clase, se dan valores por defecto para los atributos. Por ejemplo, para un número entero, se le da el valor 0. Pero puede ocurrir que nosotros deseemos dar valores iniciales que no sean cero. Esto se puede conseguir declarando un "constructor" para la clase.

Un constructor es una función especial, que se pone en marcha cuando se crea un objeto de una clase, y se suele usar para dar esos valores iniciales, para reservar memoria si fuera necesario, etc.

Se declara usando el mismo nombre que el de la clase, y sin ningún tipo de retorno. Por ejemplo, un "constructor" para la clase Puerta que le diera los valores iniciales de 100 para el ancho, 200 para el alto, etc., podría ser así:

public Puerta() { ancho = 100; alto = 200; color = 0xFFFFFF; abierta = false; }

Page 33: Unidad II

Podemos tener más de un constructor, cada uno con distintos parámetros. Por ejemplo, puede haber otro constructor que nos permita indicar el ancho y el alto:

public Puerta(int an, int al) { ancho = an; alto = al; color = 0xFFFFFF; abierta = false; }

Ahora, si declaramos un objeto de la clase puerta con "Puerta p = new Puerta();" tendrá de ancho 100 y de alto 200, mientras que si lo declaramos con "Puerta p2 = new Puerta(90,220);" tendrá 90 como ancho y 220 como alto.

Un programa de ejemplo que usara estos dos constructores para crear dos puertas con características iniciales distintas podría ser:

/*---------------------------*/ /* Ejemplo en C# nº 61: */ /* ejemplo61.cs */ /* */ /* Tercer ejemplo de clases */ /* Constructores */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Puerta { int ancho; // Ancho en centimetros int alto; // Alto en centimetros int color; // Color en formato RGB bool abierta; // Abierta o cerrada public Puerta() { ancho = 100; alto = 200; color = 0xFFFFFF; abierta = false; } public Puerta(int an, int al)

Page 34: Unidad II

{ ancho = an; alto = al; color = 0xFFFFFF; abierta = false; } public void Abrir() { abierta = true; } public void Cerrar() { abierta = false; } public void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); } } // Final de la clase Puerta public class Ejemplo61 { public static void Main() { Puerta p = new Puerta(); Puerta p2 = new Puerta(90,220); Console.WriteLine("Valores iniciales..."); p.MostrarEstado(); Console.WriteLine("\nVamos a abrir..."); p.Abrir(); p.MostrarEstado(); Console.WriteLine("Para la segunda puerta..."); p2.MostrarEstado(); } }

Nota: al igual que existen los "constructores", también podemos indicar un "destructor" para una clase, que se encargue de liberar la memoria que pudiéramos haber reservado en nuestra clase (no es nuestro caso,

Page 35: Unidad II

porque aún no sabemos manejar memoria dinámica) o para cerrar ficheros abiertos (que tampoco sabemos).

Un "destructor" se llama igual que la clase, pero precedido por el símbolo "~", no tiene tipo de retorno, y no necesita ser "public", como ocurre en este ejemplo:

~Puerta() { // Liberar memoria // Cerrar ficheros }

6.7. Polimorfismo y sobrecarga

Esos dos constructores "Puerta()" y "Puerta(int ancho, int alto)", que se llaman igual pero reciben distintos parámetros, y se comportan de forma que puede ser distinta, son ejemplos de "polimorfismo" (funciones que tienen el mismo nombre, pero distintos parámetros, y que quizá no se comporten de igual forma).

Un concepto muy relacionado con el polimorfismo es el de "sobrecarga": dos funciones están sobrecargadas cuando se llaman igual, reciben el mismo número de parámetros, pero se aplican a objetos distintos, así:

puerta.Abrir (); libro.Abrir ();

En este caso, la funci&oacute;n &quot;Abrir&quot; est&aacute; sobrecargada: se usa tanto para referirnos a abrir un libro como para abrir una puerta. Se trata de dos acciones que no son exactamente iguales, que se aplican a objetos distintos, pero que se llaman igual.

6.8. Orden de llamada de los constructores

Cuando creamos objetos de una clase derivada, antes de llamar a su constructor se llama a los constructores de las clases base, empezando por la más general y terminando por la más específica. Por ejemplo, si creamos una clase "GatoSiamés", que deriva de una clase "Gato", que a su vez procede de una clase "Animal", el orden de ejecución de los constructores sería: Animal, Gato, GatoSiames, como se ve en este ejemplo:

Page 36: Unidad II

/*---------------------------*/ /* Ejemplo en C# nº 62: */ /* ejemplo62.cs */ /* */ /* Cuarto ejemplo de clases */ /* Constructores y herencia */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Animal { public Animal() { Console.WriteLine("Ha nacido un animal"); } } // ------------------ public class Perro: Animal { public Perro() { Console.WriteLine("Ha nacido un perro"); } } // ------------------ public class Gato: Animal { public Gato() { Console.WriteLine("Ha nacido un gato"); } } // ------------------ public class GatoSiames: Gato { public GatoSiames() { Console.WriteLine("Ha nacido un gato siamés"); } }

Page 37: Unidad II

// ------------------ public class Ejemplo62 { public static void Main() { Animal a1 = new Animal(); GatoSiames a2 = new GatoSiames(); Perro a3 = new Perro(); Gato a4 = new Gato(); } }

El resultado de este programa es:

Ha nacido un animal Ha nacido un animal Ha nacido un gato Ha nacido un gato siamés Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un gato

Ejercicio propuesto:

Crear un único fuente que contenga las siguientes clases: o Una clase Trabajador, cuyo constructor escriba en pantalla "Soy un

trabajador". o Una clase Programador, que derive de Trabajador, cuyo constructor

escriba en pantalla "Soy programador". o Una clase Analista, que derive de Trabajador, cuyo constructor

escriba en pantalla "Soy analista". o Una clase Ingeniero, que derive de Trabajador, cuyo constructor

escriba en pantalla "Soy ingeniero". o Una clase IngenieroInformatico, que derive de Ingeniero, cuyo

constructor escriba en pantalla "Soy ingeniero informático". o Una clase "PruebaDeTrabajadores", que cree un objeto

perteneciente a cada una de esas clases.

6.9. Arrays de objetos Es muy frecuente que no nos baste con tener un objeto de cada

clase, sino que necesitemos manipular varios objetos pertenecientes a la misma clase.

Page 38: Unidad II

En ese caso, deberemos reservar memoria primero para el array, y luego para cada uno de los elementos. Por ejemplo, podríamos tener un array de 5 perros, que crearíamos de esta forma:

Perro[] misPerros = new Perro[5]; for (byte i = 0; i < 5; i ++) misPerros[i] = new Perro();

Un fuente completo de ejemplo podría ser /*---------------------------*/ /* Ejemplo en C# nº 63: */ /* ejemplo63.cs */ /* */ /* Quinto ejemplo de clases */ /* Array de objetos */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Animal { public Animal() { Console.WriteLine("Ha nacido un animal"); } } // ------------------ public class Perro: Animal { public Perro() { Console.WriteLine("Ha nacido un perro"); } } // ------------------ public class Ejemplo63 { public static void Main() { Perro[] misPerros = new Perro[5]; for (byte i = 0; i < 5; i ++)

Page 39: Unidad II

misPerros[i] = new Perro(); } }

y su salida en pantalla, parecida a la del ejemplo anterior, sería Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro

Ejercicio propuesto: • Crea una versión ampliada del anterior ejercicio propuesto, en la

que no se cree un único objeto de cada clase, sino un array de tres objetos.

Además, existe una peculiaridad curiosa: podemos crear un array de "Animales", pero luego indicar que unos de ellos son perros, otros gatos, etc.,

Animal[] misAnimales = new Animal[3]; misAnimales[0] = new Perro(); misAnimales[1] = new Gato(); misAnimales[2] = new GatoSiames();

Un ejemplo más detallado: /*---------------------------*/ /* Ejemplo en C# nº 64: */ /* ejemplo64.cs */ /* */ /* Ejemplo de clases */ /* Array de objetos de */ /* varias subclases */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Animal {

Page 40: Unidad II

public Animal() { Console.WriteLine("Ha nacido un animal"); } } // ------------------ public class Perro: Animal { public Perro() { Console.WriteLine("Ha nacido un perro"); } } // ------------------ public class Gato: Animal { public Gato() { Console.WriteLine("Ha nacido un gato"); } } // ------------------ public class GatoSiames: Gato { public GatoSiames() { Console.WriteLine("Ha nacido un gato siamés"); } } // ------------------ public class Ejemplo64 { public static void Main() { Animal[] misAnimales = new Animal[8]; misAnimales[0] = new Perro(); misAnimales[1] = new Gato(); misAnimales[2] = new GatoSiames();

Page 41: Unidad II

for (byte i=3; i<7; i++) misAnimales[i] = new Perro(); misAnimales[7] = new Animal(); } }

La salida de este programa sería: Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un gato Ha nacido un animal Ha nacido un gato Ha nacido un gato siamés Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal Ha nacido un perro Ha nacido un animal

6.10. Funciones virtuales. La palabra "override" En el ejemplo anterior hemos visto cómo crear un array de objetos,

usando sólo la clase base, pero insertando realmente objetos de cada una de las clases derivadas que nos interesaba, y hemos visto que los constructores se llaman correctamente... pero con los métodos puede haber problemas.

Vamos a verlo con un ejemplo, que en vez de tener constructores va a tener un único método "Hablar", que se redefine en cada una de las clases hijas, y después comentaremos qué ocurre al ejecutarlo:

/*---------------------------*/ /* Ejemplo en C# nº 65: */ /* ejemplo65.cs */ /* */ /* Ejemplo de clases */ /* Array de objetos de */ /* varias subclases con */ /* metodos */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/

Page 42: Unidad II

using System; public class Animal { public void Hablar() { Console.WriteLine("Estoy comunicándome..."); } } // ------------------ public class Perro: Animal { public new void Hablar() { Console.WriteLine("Guau!"); } } // ------------------ public class Gato: Animal { public new void Hablar() { Console.WriteLine("Miauuu"); } } // ------------------ public class Ejemplo65 { public static void Main() { // Primero creamos un animal de cada tipo Perro miPerro = new Perro(); Gato miGato = new Gato(); Animal miAnimal = new Animal(); miPerro.Hablar(); miGato.Hablar(); miAnimal.Hablar(); // Linea en blanco, por legibilidad

Page 43: Unidad II

Console.WriteLine(); // Ahora los creamos desde un array Animal[] misAnimales = new Animal[3]; misAnimales[0] = new Perro(); misAnimales[1] = new Gato(); misAnimales[2] = new Animal(); misAnimales[0].Hablar(); misAnimales[1].Hablar(); misAnimales[2].Hablar(); } }

La salida de este programa es: Guau! Miauuu Estoy comunicándome... Estoy comunicándome... Estoy comunicándome... Estoy comunicándome...

La primera parte era de esperar: si creamos un perro, debería decir "Guau", un gato debería decir "Miau" y un animal genérico debería comunicarse. Eso es lo que se consigue con este fragmento:

Perro miPerro = new Perro(); Gato miGato = new Gato(); Animal miAnimal = new Animal(); miPerro.Hablar(); miGato.Hablar(); miAnimal.Hablar();

En cambio, si creamos un array de animales, no se comporta correctamente, a pesar de que después digamos que el primer elemento del array es un perro:

Animal[] misAnimales = new Animal[3]; misAnimales[0] = new Perro(); misAnimales[1] = new Gato(); misAnimales[2] = new Animal(); misAnimales[0].Hablar(); misAnimales[1].Hablar(); misAnimales[2].Hablar();

Page 44: Unidad II

Es decir, como la clase base es "Animal", el primer elemento hace lo que corresponde a un Animal genérico (decir "Estoy comunicándome"), a pesar de que hayamos dicho que se trata de un Perro.

Generalmente, no será esto lo que queramos. Sería interesante no necesitar crear un array de perros y otros de gatos, sino poder crear un array de animales, y que contuviera animales de distintos tipos.

Para conseguir este comportamiento, debemos indicar a nuestro compilador que el método "Hablar" que se usa en la clase Animal puede que sea redefinido por otras clases hijas, y que en ese caso debe prevalecer lo que indiquen las clases hijas.

La forma de hacerlo es declarando ese método "Hablar" como "virtual", y empleando en las clases hijas la palabra "override" en vez de "new", así:

/*---------------------------*/ /* Ejemplo en C# nº 66: */ /* ejemplo66.cs */ /* */ /* Ejemplo de clases */ /* Array de objetos de */ /* varias subclases con */ /* metodos virtuales */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Animal { public virtual void Hablar() { Console.WriteLine("Estoy comunicándome..."); } } // ------------------ public class Perro: Animal { public override void Hablar() {

Page 45: Unidad II

Console.WriteLine("Guau!"); } } // ------------------ public class Gato: Animal { public override void Hablar() { Console.WriteLine("Miauuu"); } } // ------------------ public class Ejemplo66 { public static void Main() { // Primero creamos un animal de cada tipo Perro miPerro = new Perro(); Gato miGato = new Gato(); Animal miAnimal = new Animal(); miPerro.Hablar(); miGato.Hablar(); miAnimal.Hablar(); // Linea en blanco, por legibilidad Console.WriteLine(); // Ahora los creamos desde un array Animal[] misAnimales = new Animal[3]; misAnimales[0] = new Perro(); misAnimales[1] = new Gato(); misAnimales[2] = new Animal(); misAnimales[0].Hablar(); misAnimales[1].Hablar(); misAnimales[2].Hablar(); } }

Page 46: Unidad II

El resultado de este programa ya sí es el que posiblemente deseábamos: tenemos un array de animales, pero cada uno "Habla" como corresponde a su especie:

Guau! Miauuu Estoy comunicándome... Guau! Miauuu Estoy comunicándome...

6.11. Llamando a un método de la clase "padre"

Puede ocurrir que en un método de una clase hija no nos interese redefinir por completo las posibilidades del método equivalente, sino ampliarlas. En ese caso, no hace falta que volvamos a teclear todo lo que hacía el método de la clase base, sino que podemos llamarlo directamente, precediéndolo de la palabra "base". Por ejemplo, podemos hacer que un Gato Siamés hable igual que un Gato normal, pero diciendo "Pfff" después, así:

public new void Hablar() { base.Hablar(); Console.WriteLine("Pfff"); }

Este podría ser un fuente completo:

/*---------------------------*/ /* Ejemplo en C# nº 67: */ /* ejemplo67.cs */ /* */ /* Ejemplo de clases */ /* Llamar a la superclase */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Animal { } // ------------------

Page 47: Unidad II

public class Gato: Animal { public void Hablar() { Console.WriteLine("Miauuu"); } } // ------------------ public class GatoSiames: Gato { public new void Hablar() { base.Hablar(); Console.WriteLine("Pfff"); } } // ------------------ public class Ejemplo67 { public static void Main() { Gato miGato = new Gato(); GatoSiames miGato2 = new GatoSiames(); miGato.Hablar(); Console.WriteLine(); // Linea en blanco miGato2.Hablar(); } }

Su resultado sería

Miauuu Miauuu Pfff

6.12. Sobrecarga de operadores

Los "operadores" son los símbolos que se emplean para indicar ciertas operaciones. Por ejemplo, el operador "+" se usa para indicar que queremos sumar dos números.

Page 48: Unidad II

Pues bien, en C# se puede "sobrecargar" operadores, es decir, redefinir su significado, para poder sumar (por ejemplo) objetos que nosotros hayamos creado, de forma más cómoda y legible. Por ejemplo, para sumar dos matrices, en vez de hacer algo como "matriz3 = suma( matriz1, matriz2 )" podríamos hacer simplemente " matriz3 = matriz1 + matriz2"

No entraremos en detalle, pero la idea está en que redefiniríamos un método llamado "operador +", y que devolvería un dato del tipo con el que estamos (por ejemplo, una Matriz) y recibiría dos datos de ese mismo tipo como parámetros:

public static Matriz operator +(Matriz mat1, Matriz mat2) { Matriz nuevaMatriz = new Matriz(); for (int x=0; x < tamanyo; x++) for (int y=0; y < tamanyo; y++) nuevaMatriz[x, y] = mat1[x, y] + mat2[x, y]; return nuevaMatriz; }

Desde "Main", calcularíamos una matriz como suma de otras dos haciendo simplemente

Matriz matriz3 = matriz1 + matriz2;

Ejercicios propuestos:

• Desarrolla una clase "Matriz", que represente a una matriz de 3x3, con métodos para indicar el valor que hay en una posición, leer el valor de una posición, escribir la matriz en pantalla y sumar dos matrices.

6.13. Proyectos a partir de varios fuentes

En los programas de gran tamaño, lo habitual es que no se plantee como un único fichero fuente de gran tamaño, sino como una serie de objetos que colaboran entre ellos, lo que supone descomponer nuestro fuente en varias clases (y, por tanto, en varios ficheros).

En estos casos, un editor como el Bloc de Notas, o incluso como Notepad++, se queda corto. Sería preferible un entorno que nos

Page 49: Unidad II

permitiera editar nuestros fuentes, compilarlos sin necesidad de salir de él, que nos destacara las líneas que contienen errores, que nos mostrara ayuda sobre la sintaxis de cada función, que nos permitiera depurar nuestros programas avanzando paso a paso…

Existen entornos que nos permiten hacer todo eso, y además hacerlo gratis. El más conocido es el Visual Studio de Microsoft, que en su versión Express incluye todo lo un programador novel como nosotros puede necesitar. Una alternativa muy similar, pero algo más sencilla (lo que supone que funcione más rápido en ordenadores no demasiado potentes) es SharpDevelop.

Vamos a ver las pautas básicas de manejo de SharpDevelop (que se aplicarían con muy pocos cambios al caso de Visual Studio):

Comenzamos por descargar el fichero de instalación del entorno, desde su página oficial (http://www.icsharpcode.net/opensource/sd/). La versión 3.1, para las versiones 2.0 y 3.5 de la plataforma .Net, ocupa unos 15 Mb.

La instalación comenzará simplemente con hacer doble clic. Deberíamos ver una ventana parecida a ésta:

Como es habitual, el siguiente paso será aceptar el contrato de licencia, después deberemos decir en qué carpeta queremos instalarlo,

Page 50: Unidad II

comenzará a copiar archivos y al cabo de un instante, tendremos un nuevo icono en nuestro escritorio:

La instalación debería ser muy sencilla en Windows Vista y superiores, pero en Windows XP quizá necesite que instalemos la versión 3.5 de la plataforma .Net (se puede hacer gratuitamente desde su página oficial).

Cuando lanzamos nuestro nuevo icono, veremos la pantalla principal de SharpDevelop, que nos muestra la lista de los últimos proyectos ("soluciones") que hemos realizado, y nos permite crear uno nuevo:

En nuestro caso, comenzaremos por crear una "Nueva solución", y se nos mostrará los tipos de proyectos para los que se nos podría crear un esqueleto vacío que después iríamos rellenando:

Page 51: Unidad II

De estos tipos, el único que conocemos es una "Aplicación de Consola" (en C#, claro). Deberemos escribir también el nombre., y aparecerá un esqueleto de aplicación que nosotros sólo tendríamos que completar:

Page 52: Unidad II

Cuando hayamos terminado de realizar nuestros cambios, podemos compilar el programa con el botón:

Si hubiera algún error, se nos avisaría en la parte inferior de la pantalla, y se subrayarían en rojo las líneas correspondientes de nuestro programa; si todo ha ido bien, podremos ejecutar nuestro programa para verlo funcionando:

(Si la ventana de nuestro programa se cierra tan rápido que no tenemos tiempo de leerla, nos puede interesar añadir provisionalmente una línea ReadLine() al final del fuente, para que éste se detenga hasta que pulsemos la tecla Intro)

Así prepararíamos y lanzaríamos un programa formado por un solo fuente. Si se trata de varios fuentes, basta con ir añadiendo nuevas

Page 53: Unidad II

clases al proyecto. Lo conseguimos pulsando el botón derecho sobre el nombre del proyecto (en la ventana izquierda, "Proyectos") y escogiendo las opciones Agregar / Nuevo Elemento:

Normalmente, el tipo de elemento que nos interesará será una clase, cuyo nombre deberemos indicar:

y obtendríamos un nuevo esqueleto vacío (esta vez sin "Main"), que deberíamos completar.

Page 54: Unidad II

Nuestro programa, que ahora estaría formado por dos clases, se compilaría y se ejecutaría de la misma forma que cuando estaba integrado por una única clase.

Ejercicio propuesto: Crear un proyecto que contenga las siguientes clases (cada una en un fichero distinto): o Una clase Trabajador, cuyo constructor escriba en pantalla "Soy un trabajador". o Una clase Programador, que derive de Trabajador, cuyo constructor escriba en pantalla "Soy programador". o Una clase Analista, que derive de Trabajador, cuyo constructor escriba en pantalla "Soy analista". o Una clase Ingeniero, que derive de Trabajador, cuyo constructor escriba en pantalla "Soy ingeniero". o Una clase IngenieroInformatico, que derive de Ingeniero, cuyo constructor escriba en pantalla "Soy ingeniero informático". o Una clase "PruebaDeTrabajadores", que cree un objeto perteneciente a cada una de esas clases.


Recommended