Se destruye el objeto de la clase hija Se destruye el objeto de la clase madre

Curso de C# Este mes vamos a destruir (en el buen sentido) y también vamos a hacer de basureros recogiendo la basura. El estudio de los métodos será ...
3 downloads 1 Views 214KB Size
Curso de C#

Este mes vamos a destruir (en el buen sentido) y también vamos a hacer de basureros recogiendo la basura. El estudio de los métodos será nuestro próximo paso. Para empezar veremos los destructores, los cuales nos permitirán destruir una instancia de una clase. Para continuar, explicaremos la sobrecarga de operadores y finalmente estudiaremos el paso de parámetros variables. En los ejemplos de programación, construiremos un pequeño servidor que escuchará por un puerto las peticiones de programas clientes. En el artículo del mes pasado estudiamos, entre otras cosas, los métodos constructores de las clases. Pues bien ahora veremos el método opuesto: los destructores. Como ya dijimos (si no recuerdo mal) en la primera entrega, también hablaremos del "recolector de basura" o Garbage Collector y lo invocaremos nosotros mismos a pesar de que es un proceso automatizado. Hasta ahora, hemos creado objetos mediante new y no nos hemos preocupado de la memoria que utilizaban esos objetos. Esta asignación dinámica de memoria necesita de un sistema de recuperación de la memoria libre dejada por los objetos que ya no se utilizan. En C++ creo recordar que se utiliza el operador delete, en C# se utiliza el método de recolección de elementos no utilizados. Este método (ó sistema. No confundir con método de una clase, por favor) lo que hace es recopilar objetos, de forma transparente al usuario y en segundo plano, que ya no se puedan utilizar. ¿Cómo se sabe si un objeto ya no se puede utilizar?. Pues fácil: si ya no hay ninguna referencia al objeto, se da por hecho que el objeto ya no se necesita y pasa a ser candidato a la recolección. La recolección de basura se hace por parte del sistema y no porque los objetos pasen a ser candidatos a la recolección. Tampoco es algo aleatorio. Debido a que la recolección requiere un tiempo, sólo se hace cuando es necesario o en cualquier otro momento oportuno. Es decir: no podemos saber cuando tendrá lugar a no ser que lo “forcemos” nosotros, y aún así tampoco sabremos en qué momento preciso se destruirá un objeto específico. Forzar la recolección sólo lo haremos para ilustrar el funcionamiento de los destructores (enseguida vemos un ejemplo) ya que es absurdo hacer “perder” el tiempo al sistema si no es necesario, y si lo es, ya lo hace él solito. Destructores.. Un destructor es un método (miembro) de una clase que incluye las acciones necesarias o requeridas por la aplicación para destruir un objeto o instancia de la clase a la que pertenece. Se le llama inmediatamente antes de que el recolector de elementos no utilizados haya destruido el objeto. El método destructor se utiliza para asegurar que el objeto finaliza de forma correcta, es decir, que realiza las acciones necesarias antes de destruirse. Imaginad un objeto que abra un archivo y lo maneje. Es conveniente asegurarse de que antes de su destrucción, el archivo sea cerrado. Estas acciones son las que podríamos poner en el destructor. Como ya hemos dicho antes, no basta con que un objeto deje de ser referenciado para que se invoque su destructor (a diferencia de C++) sino que serán llamados cuando se efectúe la recolección o, simplemente, antes de finalizar el programa. Sintácticamente, un destructor se especifica con el mismo nombre de la clase, pero precedido del símbolo ~. Si no lo hacemos así, obtendremos un error en tiempo de compilación. Por ejemplo, si tenemos una clase llamada "revista", su destructor debe llamarse "~revista" (recordad del mes pasado que su constructor se llamará "revista"). Muy importante: los destructores no se heredan, por lo que una clase tendrá únicamente el destructor que se defina en ella misma. Otra cosa es que si una clase hereda de otra, los destructores se invoquen en un orden determinado. ¿Adivináis cual?. Lo podemos comprobar con el listado del Listado 1. La salida de este programa es: Se destruye el objeto de la clase hija Se destruye el objeto de la clase madre Os pongo así la salida ya que la clase GC (Garbage Collector) no está implementada aún en mono, por lo que no puedo ejecutarlo en mi Linux. De todas formas, expliquemos el funcionamiento: Tenemos dos clases, la clase madre y la clase hija. Ésta última hereda de la clase madre. En la clase principal, creamos un objeto de la clase hija, para después dejarlo inútil (lo igualamos a null). Finalmente llamamos a los métodos Collect() y WaitForPendingFinalizers(). El primero inicia la recolección de elementos no utilizados (¿os acordáis de cuando dijimos que invocaríamos al recolector de basura?, pues llegó el momento) y el segundo detiene la ejecución del proceso hasta que se hayan invocado todos los destructores necesarios. Los métodos más interesantes definidos en GC los podemos ver a continuación: public static void Collect(): Comienza la recolección de elementos no utilizados. public static void Collect(int Maximo): Comienza la recolección de elementos no utilizados de la memoria con números iniciales comprendidos entre 0 y Maximo. public static long GetTotalMemory(bool recopilados): Devuelve el número total de bytes asignados. Si “recopilados” es true, se producirá la recopilación de elementos no utilizados en primer lugar. public static void KeepAlive(object o): Crea una referencia al objeto o, evitando que sea candidato a la recopilación. public static void WaitForPendingFinalizers(): Detiene la ejecución del proceso hasta que se hayan invocado todos los destructores.

Un método destructor no puede tener parámetros (esto es muy importante también), por lo que no se puede sobrecargar. Llegados a este punto es el momento de hablar ya de la sobrecarga de métodos. Sobrecarga de métodos. En la entrega anterior ya vimos cómo sobrecargar métodos. Lo utilizamos en una clase para definir un punto en el espacio bidimensional. Si no le pasábamos ningún parámetro, se ejecutaba el constructor sin parámetros, que inicializaba las corrdenadas a cero. Si le pasábamos las coordenadas, estos valores eran asumidos por el objeto al ejecutarse el constructor con parámetros. Dos o más métodos de la misma clase pueden tener el mismo nombre si, y sólo si, sus parámetros son diferentes. Entonces decimos que el método está sobrecargado. En definitiva, se trata de definir un método con distintos tipos ó número de parámetros, para utilizarlo según nuestra conveniencia. El compilador decidirá cual ha de usar. También pueden devolver distintos tipos de datos, aunque esto no es obligatorio, a diferencia de la condición anterior en cuanto a número o diferencia de tipos en los parámetros. Hay que tener en cuenta que un tipo de datos devuelto no es suficiente para poder elegir el método a utilizar (recordemos las conversiones implícitas, sin ir más lejos). Si llamamos a un método sobrecargado, se ejecutará aquel que coincida en sus parámetros (tipos y número) con los argumentos proporcionados por nosotros. Vamos a fijarnos en el Listado 2. Tenemos en nuestra clase, tres métodos con el mismo nombre “mensaje” y diferentes parámetros. No devuelven ningún valor y son estáticos por motivos evidentes: los vamos a usar como funciones normales y no queremos crear ningún objeto. Daros cuenta que en el primer método sólo tenemos un parámetro llamado texto. En el segundo ya tenemos dos (titulo y texto, los dos string) y en el tercero tenemos dos parámetros, pero el primero es un entero. Con esto hemos querido implementar un método para escribir mensajes en nuestro programa, pero con la flexibilidad de mandar distintos tipos de argumentos según queramos en cada momento: escribir un texto, dar un aviso ó mandar un mensaje de error con su código.

Figura1 Viendo el resultado de la ejecución del programa (Figura 1) comprobamos que, efectivamente, el compilador ha hecho su trabajo: Para cada llamada al método, ha elegido el que le correspondía (¿acaso lo dudabais? jejejeje). La sobrecarga de métodos es una de las formas que se utiliza en C# para implementar el polimorfismo, ya que de esta forma se cumple con el paradigma “una interfaz, varios métodos”. El ejemplo más clarificador si lo comparamos con C es el de la función Abs que devuelve el valor absoluto. En C no se permite la sobrecarga, por lo que para obtener el valor absoluto existen varias funciones (abs(), labs(), fabs()...) dependiendo del tipo de datos del argumento. Es un “obstáculo” para el programador y un “atraso” conceptual el tener varias funciones para lo mismo. Como ya sabemos, en la clase Math de C#, tenemos el método Abs() que, podéis comprobar, se sobrecarga adecuadamente para manejar los distintos argumentos numéricos recibidos. La sobrecarga además permite agrupar métodos relacionados con un nombre común a todos, permitiendo así simplificar el manejo de conceptos complejos. Para terminar con la sobrecarga, definiremos el concepto de firma. La firma de un método en C# es su nombre más la lista de parámetros. Es evidente pues que, debido a la sobrecarga de operadores, dos métodos de la misma clase nunca podrán tener la misma firma. Insisto en que el tipo devuelto por un método no es relevante para definir qué método sobrecargado utilizar. Por eso no se incluye en la firma. Tampoco se incluye en la firma un paramétro “params”, por lo que no influye en la resolución de la sobrecarga, a pesar de ser un parámetro, pero ¿qué es eso de un parámetro “params”?. Uso de params como parámetro. Número variable de argumentos. Cuando escribimos un método, normalmente sabemos el número de argumentos que va a recibir, pero no siempre es así. Imaginemos que queremos escribir un método que devuelva el valor máximo de unos parámetros que reciba. Es evidente que mediante el paso de parámetros típico estaríamos limitados. Nosotros queremos pasar un número indeterminado de ellos. Esto lo hacemos mediante un parámetro “params”. Este es un modificador que nos permite declarar un parámetro array (tabla, matriz, etc...) que puede recibir un número arbitrario de argumentos (incluido ninguno). No se trata de pasarle una matriz como argumento al método. No. Se trata de pasar n argumentos separados por comas. Y ese número n puede ser cualquiera. Interesante ¿eh?. Veamos un ejemplo. Imaginemos un método que, como hemos dicho antes, calcule el valor máximo de una serie de números. Podríamos declararlo así: public int maximo_numero(params int[] valores)

Observad que el modificador params va delante del tipo de datos y la matriz que almacenará los números (sean los que sean) se llama “valores”. La longitud de la matriz será el número de parámetros pasados y podremos acceder mediante su propiedad Length, y para acceder a los parámetros lo haríamos con valores[i], desde 0 hasta valores.Length – 1. Es muy importante recalcar, aunque ya lo habréis supuesto, que todos los argumentos deben ser del mismo tipo o compatible con el de la matriz params. Por último, señalar que un método puede combinar un parámetro params (de longitud variable) con otros parámetros normales como los que habíamos visto hasta ahora. En este caso, el parámetro params se suele poner en último lugar. El mes que viene, seguiremos con el paso de parámetros. Veremos los parámetros por valor y por referencia. Además, en cada uno de estos casos, veremos el paso de variables de tipo valor y de tipo referencia. Es decir, podemos pasar por valor un tipo de datos valor ó un tipo de datos referencia. Y podemos pasar por referencia un tipo de datos valor o un tipo de datos referencia. Vamos, que pasaremos datos de todo tipo y veremos también como devolver más de un valor. Ejemplos útiles sobre la librería de clases. Este mes vamos a programar un sencillo servidor que acepta conexiones por un puerto determinado (el 13 en el ejemplo). Como ejercicio podéis modificarlo para que acepte conexiones por el puerto que le demos desde la linea de comandos (muy fácil de hacer y muy útil). Antes de que se me olvide: Los lectores observadores se darán cuenta que el prompt de la consola de la Figura 3 indica que estoy logueado como root. Sí: hay que ejecutarlo con una cuenta privilegiada, imagino que sabréis por qué ¿verdad? ;-) El código está en el Listado 3. Comentemos el programa: Lo primero es definir el número de puerto por donde vamos a permitir conexiones. Esto lo hacemos mediante una constante privada. Dentro de la función principal, definimos una variable lógica que nos permitiría salir del bucle de espera de conexiones (modificación que también os dejo a vosotros, luego veremos porqué no ponemos un true directamente de momento). Seguidamente definimos un “escuchador” del tipo TcpListener en el puerto 13 y después iniciamos el “escuchador” con el método Start del listener. El bucle siguiente permite aceptar diferentes conexiones. En principio no saldremos del bucle nunca excepto si pulsamos Ctrl-C. Aquí es donde uno se puede preguntar porqué no poner un while (true) en lugar de usar la variable bool que no vamos a modificar. Pues es sencillo de explicar y además ya hemos comentado anteriormente la situación. ¿Habéis probado a cambiarlo en el código?. Hacedlo y veréis que os da un error de compilación. Dice aldo sobre ¿código inalcanzable?. Exacto. El compilador detecta que las instrucciones de fuera del bucle que tenemos al final nunca se ejecutarían, y nos da un error avisando.

Figura2 Bien, una vez dentro del bucle, mostramos un mensaje y creamos un objeto TcpClient mediante el método AcceptTcpClient() del objeto “escuchador” (listener). Este objeto será el cliente de la conexión y será al que le enviemos la respuesta mediante un canal de datos de red (NetworkStream). El método AcceptTcpClient() espera una conexión, y cuando la recibe, muestra un mensaje de “Conexión aceptada”. El canal de datos (ns) es el que creamos en la siguiente linea usando el método GetStream() del objeto TcpClient (cliente).Luego preparamos el mensaje que le enviaremos al cliente. El mensaje debe estar codificado en bytes por lo que hacemos uso del espacio de nombres System.Text para poder utilizar Encoding.ASCII.GetBytes pasándole la cadena que le vamos a enviar al cliente. Lo siguiente es intentar enviar el mensaje al cliente a través del canal de datos (ns y el método Write). Si ocurriera algún error en la comunicación, se lanza una excepción mostrando un mensaje con el error. Ya veremos más adelante el tema de las excepciones (errores). De momento saber que try define un bloque a ejecutar y si ocurre algún fallo, se ejecutan los bloques que hay en catch. En este caso mostrar el texto del error (e.ToString()). Ejecutamos el programa una vez compilado y veremos que se nos muestra el mensaje “Esperando una conexión”. Y ¿qué pasa con el cliente?. Observad la Figura 2 (conexión al servidor) y la Figura 3 (lo que vemos en la consola del servidor). Con un simple telnet al puerto 13, obtenemos la conexión y el servidor nos manda el mensaje “Conexión aceptada”, el cual vemos en la pantalla del cliente.

Figura3 Como veis, el servidor sigue esperando otra conexión. Esto es debido al bucle while comentado antes. Antes de esperar una nueva conexión, el canal de datos y el cliente son cerrados. Si saliéramos del bucle, detenemos el listener y salimos del programa. Hasta el mes que viene. No quiero despedirme sin insistir en que la documentación es esencial para el uso del lenguaje. En esta ocasión, os remito a la documentación de mono, así estaremos al día de como va el desarrollo de las clases en este interesante proyecto. En este caso, os recomiendo revisar en Class Library, el System.Net.Sockets (ver Figura 4) ya que ahí tenéis la documentación sobre el ejemplo del servidor y además tiene mucha “miga”, al igual que el espacio de nombres System.Net que ya utilizamos el mes pasado. La podéis encontrar en http://www-go-mono.com/docs

Figura4 El mes que viene seguiremos con el curso, seguiremos con las clases, y seguiremos con los métodos. Pasaremos objetos como parámetros y sobre todo, empezaremos con las matrices. Sí, ya se que íbamos a empezar este mes, pero no ha podido ser. Hay tantas cosas por ver que a veces hay que “mover” las cosas de sitio. Tranquilos, todo llegará. Hasta la próxima y gracias por leerme. Yorkshire [email protected]

DESPIECE Listado 1. Ejecución de destructores. using System; class madre { ~madre() { Console.WriteLine("Se destruye el objeto de la clase madre"); } } class hija: madre { ~hija() { Console.WriteLine("Se destruye el objeto de la clase hija"); } } class ejemplo1 { static void Main() { hija h = new hija(); h = null; GC.Collect(); GC.WaitForPendingFinalizers(); } } DESPIECE Listado 2. Sobrecarga de métodos. using System; class sobrecarga1 { static void mensaje(string texto) { Console.WriteLine(texto); } static void mensaje(string titulo, string texto) { Console.WriteLine("{0}: {1}",titulo,texto); } static void mensaje(int numerror, string texto) { Console.WriteLine("ERROR {0}: {1}",numerror,texto); } static void Main() { mensaje("Hola a tod@s"); mensaje("AVISO","Este es un aviso"); mensaje(2,"Fichero no encontrado"); } } DESPIECE Listado 3. Servidor que acepta conexiones. using System; using System.Net.Sockets; using System.Text; public class servidor_conex { private const int puerto = 13; public static int Main() { bool hecho = false;

TcpListener listener = new TcpListener(puerto); listener.Start(); while (!hecho) { Console.Write("Esperando una conexion..."); TcpClient cliente = listener.AcceptTcpClient(); Console.WriteLine("Conexion aceptada."); NetworkStream ns = cliente.GetStream(); byte[] byteTexto = Encoding.ASCII.GetBytes("Conectado al servidor"); try { ns.Write(byteTexto, 0, byteTexto.Length); } catch (Exception e) { Console.WriteLine(e.ToString()); } ns.Close(); cliente.Close(); }

} }

listener.Stop(); return 0;