lunes, 4 de diciembre de 2017

Notas básicas de programación en C#

  He querido recopilar un conjunto de notas básicas de programación en C#, con el objetivo de hacer mi pequeña compilación de introducción al lenguaje. Para ello, me he basado en algunos libros como "C# Manual de referencia" Herbert Schildt (McGraw Hill), "Programming in C# Exam Ref. 70-483" Wouter de Kort (Microsoft) o el curso MOOC en la plataforma Edx "Microsoft DEV204x Programming with C#", además de la mucha información que hay en línea como por ejemplo en esta guía de programación de la documentación oficial de Microsoft.



  Inicialmente podríamos decir, que C# combina la portabilidad (mediante código intermedio) y la seguridad de tipos de Java, con la eficacia de C++. Este podríamos decir que es el gran objetivo que persiguieron el equipo diseñadores con Anders Hejlsberg a la cabeza.




Por tanto, sus características principales son:
  • Fuertemente tipado. Comprobación constante de tipos para evitar errores.
  • Orientado a objetos.
  • Orientado a componentes.
  • Sistema de tipos unificado. Todos los tipos heredan de la clase Object.
  • Concepto de código manejado. Gestión de memoria y de recursos no utilizados.

Aplicaciones Administradas vs No Adminstradas

  Existen dos tipos de aplicaciones: de código administrado y no administrado. La diferencia está en que una aplicación administrada se ejecuta sobre un ambiente de ejecución (Runtime environment) que le proporciona una serie de servicios, favoreciendo la ejecución multiplataforma.

  Una aplicación administrada se ejecuta haciendo uso de Common Language Runtime (CLR), lo que significa que C# es un lenguaje compilado Just-In-Time. CLR forma parte de .NET Framework, y es responsable de administrar el código ejecutado proveyendo servicios de manejo de memoria, de ejecución multi-hilo y seguridad de tipos. Además se dispone de una librería estándar de clases utilizable directamente o extensible para poder adaptarla a las necesidades particulares.



  El código no administrado es aquel en el que la compilación genera un binario que se ejecuta directamente en el sistema operativo de la máquina fuera del ambiente de ejecución Runtime. 
Esto significa, que si el desarrollador necesita utilizar algunos de los servicios proporcionados por el ambiente de ejecución Runtime, éste debe de invocarlos explícitamente al sistema operativo.

En principio, es recomendable utilizar por defecto código administrado, ya que ofrece robustez, portabilidad y mejor time-to-market repercutiendo en una mayor productividad, sin embargo, el código no admistrado permite un mayor control permitiendo mejorar los resultados para una aplicación específica. 

Por tanto, parece lógico desarrollar solamente los componentes sensibles de manera customizada haciendo uso de código no administrado.


Identificadores

Los identificadores son los nombres asignados a los elementos que conforman las sentencias de un programa.

Existen:
  • Namespace: Son espacios de nombres destinados a categorizar ficheros de clases, evitando colisiones de nombres.
  • Clases: Se trata de la plantilla que permite especificar o configurar instancias de objeto.
  • Métodos: Recogen la funcionalidad que es capaz de realizar una aplicación, distribuida en los diferentes objetos que la forman.
  • Variables: Son nombres asignados a localizaciones de memoria, donde se guardan valores y referencias a objetos.
  Con respecto a los métodos, conviene comentar los diferentes modificadores que podemos introducir en los parámetros que recibe para la ejecución de la tarea.

  En primer lugar, es posible pasar dichos parámetros por valor o por referencia, lo que significa que dicha información será una copia de la original o una referencia que permitirá modificarla desde el interior del método.
 
  Los parámetros que permiten modificar la información desde el interior del método son out y ref, cuya diferencia estriba en que quien tiene la responsabilidad de inicializar el valor del parámetro pasado. Utilizar ref significa que la variable pasada como argumento ha sido previamente inicializada, sin embargo, utilizar out exige que sea el propio método el que realice la asignación.

  Un método es posible sobrecargarlo, lo que significa que varios métodos pueden tener el mismo nombre e incluso el mismo valor de retorno, encontrándose las diferencias en la lista de argumentos de manera obligatoria.

  Respecto a la lista de argumentos, existe la posibilidad de definir algunos de ellos como opcionales. Se utilizan cuando la sobrecarga de métodos no es funcional, en la que la lista de parámetros no varía lo suficiente para que el compilador pueda distinguir entre implementaciones distintas.
Los parámetros opcionales van al final de la lista en la firma del método y tienen un valor de inicialización para disponer de un valor en caso de no invocarse.

  Como detalle final, es posible el utilizar parámetros con nombre. Si se asocia un nombre a los parámetros, no será necesario utilizar el orden de invocación dado por la firma del método. Si se intercalan parámetros con nombre y parámetros posicionales, estos últimos deben situarse primero en la firma del método.


Control de Excepciones

  El entorno de ejecución Runtime proporciona una gestión de las excepciones basadas en la representación de las mismas mediante objetos, separando el código de programa del control de las excepciones mediante bloques try-catch. Pueden existir uno o varios bloques catch diseñados para gestionar un determinado tipo de excepción.

  Cuando una excepción tiene lugar dentro de un bloque try, el sistema busca la clausula catch asociada. Por este motivo, es necesario listar en primer lugar la gestión de excepciones a nivel más particular dejando finalmente la gestión de excepciones estándar o de tipo base (System.Exception).

  Si ninguno de los bloques catch es capaz de manejar la excepción producida, ésta es enviada por el sistema al nivel de anidamiento de llamadas superior con la esperanza que sea gestionada. Si el nivel de anidamiento de llamadas en su nivel más superior es alcanzado sin gestionarse dicha excepción, la aplicación termina.

  Ante una excepción producida, es posible volver a lanzarla para que sea tratada por en otros objetos superiores. Para lanzar una excepción se utiliza la palabra clave throw.
Es necesario indicar que hay que asegurarse que no se pierde información relacionada con la excepción producida cuando se realizan relanzamientos. Se aconseja, o bien lazar una nueva excepción apuntando la excepción original que se ha producido y quizás añadir más información que pudiese ser de utilidad, o utilizar simplemente la palabra clave throw sin ningún identificador para transmitir de manera intacta la excepción generada a objetos superiores.

  Es posible definir excepciones personalizadas (las cuales heredan de la clase Exception además de implementar el interface ISerializable, para poder ser enviada entre distintas aplicaciones como app-webservice). Las excepciones personalizadas tienen sentido cuando serán atendidas de una manera particular por algún motivo, de lo contrario, es mejor utilizar las excepciones estándar del framework .NET para mejorar el rendimiento.

Estructuras Complejas de Datos

  En la programación orientada a objetos, los tipos y los objetos son los elementos básicos utilizados para construir las aplicaciones más complejas. Podríamos decir que un tipo es un anteproyecto de un objeto. Dependiendo de la situación concreta, C# permite elegir el tipo que es necesario crear y que encaja mejor por funcionalidad y rendimiento.

  Principalmente existen dos clases de tipos denominados por valor o por referencia, los cuales son usados de manera extensiva en .NET, por lo que es importante conocer las diferencias para poder tomar una decisión adecuada.

  Un tipo referencia contiene la dirección o referencia al valor almacenado, mientras que un tipo valor, contiene el valor directamente. Así, existen dos tipos de localizaciones donde puede almacenarse un tipo:
  • Heap: Los tipos referencia son almacenados en esta localización de memoria. Es aquí donde el Garbage Collector tiene su zona de influencia.
  • Stack: Los tipos valor, son almacenados principalmente aquí. En esta zona de memoria, además de no necesitar de la atención del Garbage Collector, es más rápido el acceso a la información, lo que mejora el rendimiento. 
  Los tipos valor, son óptimos cuando se trata de construir estructuras de datos que son pequeñas, lógicamente inmutables, y de las que necesitemos manejar en grandes cantidades. Un ejemplo sería una estructura que representase un punto en un gráfico con las coordenadas X,Y del mismo.

  Podemos resumir:
  • Tipo Valor: Debería utilizarse para objetos pequeños e inmutables que se necesiten utilizar en grandes cantidades.
  • Tipo Referencia: Utilizar para el resto de los casos.
 Array

Un array es un conjunto de objetos agrupados y manejados como una unidad.
  • Cada elemento del array contiene un valor.
  • Zero based, es decir, el primer elemento es referencia por el número cero.
  • Es posible construir un array de una o varias dimensiones.
  • El tamaño es el número de objetos que puede contener, y el rango el número de dimensiones disponibles.
           int[] nombreArray = new int[10];
           int[,] nombreArray = new int[10,10];

Enumeraciones

  Una enumeración es una estructura que permite crear una variable con un número fijo de valores, y además se tiene la seguridad de que no variarán.

            enum Dia { Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo }

Es posible indicar el tipo de valor intrínseco almacenado por cada valor de la enumeración. Por ejemplo:

            enum Dia { Lunes = 1, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo }

La ventaja principal estriba en la mejora de manejabilidad, evitando utilizar valores no válidos y ayuda a una mejor legibilidad del código.

En el siguiente ejemplo, vemos como es posible trabajar con las enumeraciones a nivel de bit:

[Flags] 
enum Dias { Lun = 0x1, Mar = 0x2, Mie = 0x4, Jue = 0x8, Vie = 0x10, Sab = 0x20, Dom = 0x40 

Dias SeleccionarDias = Dias.Lun | Dias.Vie; 

Estructuras

  Una estructura es un elemento en programación que permite agrupar pequeños elementos de información con un sentido lógico afín. Por ejemplo la estructura Punto, con un valor de coordenada X e Y. Un estructura podría explicarse como una versión minimalista de una clase.
En línea con la similitud que tiene con una clase, una estructura puede contener un método constructor que permita inicializar los campos que contiene.

Clases

  Una clase es un tipo complejo que agrupa un conjunto de datos y un conjunto de funcionalidades, las cuales están disponible para que sean utilizadas por otros objetos.
Se suele asociar una clase con el concepto de "plantilla" (blueprint), a partir de la cual es posible construir objetos en base a a ésta, los cuales trabajan de modo independiente.

  Las clases disponen de un método básico principal denominado constructor, el cual tiene la tarea principal de inicializar el objeto de una manera concreta. Buenas prácticas para configurar un método constructor correctamente son las siguientes:

  • Declarar solamente un constructor de manera explícita si es necesario. Por defecto, cada clase tiene asociado uno, con lo que no es necesario construir uno nuevo.
  • Asegurarse que un constructor toma la menor cantidad de argumentos posible.
  • Enlazar los argumentos con propiedades de la clase.
  • Es correcto lanzar excepciones desde el constructor si es necesario.
  • No utilizar miembros virtuales en un constructor. (Miembros virtuales son aquellos que pueden ser modificados en una clase derivada de la que los contiene).

  A la hora de diseñar una clase, es necesario tener en cuenta un conjunto de principios, los cuales nos ayudarán a crear software más fácil de utilizar, mantener y extender. Estos principios se basan en una alta cohesión y un bajo acoplamiento. Esto significa, que el código no debe ser dependiente de otro código cuando no sea absolutamente necesario, lo que posibilitará el realizar ajustes y cambios sin preocuparse de consecuencias inesperadas de manera colateral.

  Estos principios se agrupan en torno al acrónimo SOLID:

  • Single Responsability Principle. Cada clase debe tener un único cometido claro y conciso.
  • Open/Close Principle. Un objeto de estar abierto a extenderse pero no a modificarse. Por ejemplo, utilizando Interfaces comunes, los objetos nuevos pueden integrarse con el código existente, pero sin modificar el previamente construido.
  • Liskov Susbstitution Principle. Significa, que es posible utilizar una clase base o una clase derivada de forma que sea totalmente transparente y funcional pudiendo sustituir una a la otra.
  • Interface Segregation Principle. Es conveniente utilizar interfaces concretos y específicos en lugar de un único interface general. El consumidor de un interface debería solamente poder centrarse en los métodos que realmente necesita utilizar. Es necesario evitar que los clientes de una clase o interface deban cargar con funcionalidades que no les interesan.
  • Dependecy Inversion Principle. Indica que es necesario orientar el diseño hacia las abstracciones (interfaces) y no hacia las implementaciones (clases concretas). Este principio permite conseguir un mayor desacoplamiento y una menor dependencia, facilitando la adaptación a los cambios.

Programación Orientada a Objetos en C# (POO)

  En C#, una clase es una construcción programática utilizada para definir tipos propios personalizados. Se trata de una plantilla con la que será posible el instanciar objetos desde la misma.  Sólo en este momento se podrá utilizar toda la funcionalidad definida en una clase.

  Una clase define métodos y características que son compartidos por todas las instancias de la clase. Estas características son representadas mediante métodos, campos, propiedades y eventos.

  Existe una diferencia conceptual entre funciones y métodos, que se explica a continuación:
  • Función: Trabaja con la información contenida en el objeto sin modificarla (realiza una lectura de la misma), y genera una salida con el resultado. 
  • Método: Permite modificar la información contenida en un objeto, sin devolver un resultado concreto. 
Miembros Estáticos

  Una clase puede tener miembros estáticos, ya sean variables, métodos, propiedades o eventos, lo que significa que es posible utilizarlos sin realizar instancias de la clase propiamente. Así, los campos y métodos estáticos son utilizados para obtener información que tiene su sentido en términos de clase y no de instancia de clase.

  No importa cuantas instancias de una clase existan, solamente existirá una clase estática donde se recogerán sus miembros estáticos, a los que se podrá acceder directamente mediante el nombre de la clase y no mediante un determinado objeto de clase.


Herencia

  La herencia es otro de los pilares básicos de la POO. Es posible utilizar la herencia para definir una jerarquía de clases de manera que podemos llamar a la clase padre como superclase y los clases que heredan de ésta se suelen denominar subclases. De manera que la clase padre contiene todos los atributos y métodos comunes, mientras que las subclases contienen la información que la define de manera individual.

  C# no soporta herencia múltiple (heredar funcionalidad de diferentes superclases).
Si se da la situación de que solamente utilizaremos clases heredadas de una dada, y que esta superclase solo es utilizada para heredar de ella, una opción interesante es utilizar las clases abstractas. Una clase abstracta es un concepto muy parecido a un interface, del que no es posible instanciar clases, sino solamente heredar de ellas.

  En una clase abstracta podemos declarar métodos sin implementación o con implementación. Si un método tiene implementación, para que pueda ser sobreescrito en una subclase debe decorarse con la palabra clave virtual.

             public virtual nombreMetodo() { }

  En una clase abstracta, si decoramos un método con la palabra clave abstract, lo que se está indicando es que es un método que solamente existe en la propia clase abstracta o en una clase abstracta heredada, pero no puede existir en una clase no abstracta. Obligatoriamente, este método debe ser implementado en una subclase, de lo contrario el compilador generará un error.
Por tanto, si tenemos una clase abstracta y queremos forzar a las subclases a implementarlo, tenemos que decorar el método con la palabra clave abstract.

Para evitar que una clase sea heredada, utilizaremos la palabra clave sealed.

  Un interface es una clase sin implementación. Especifica un conjunto de características y comportamientos definiendo las firmas de los métodos, propiedades, eventos, indices.. sin especificar como éstos miembros están implementados. Cuando una clase implementa una interface dicha clase provee la implementación de cada método, de manera que se garantiza que se cumple la funcionalidad especificada en un interface.

  Un interface puede modificar su ámbito mediante la palabra public o internal. Esta última es el valor por defecto, e indicaría que el interface está disponible en el mismo assembly, sin embargo, public indica que el interface está disponible  para cualquier assembly.

  Un interface puede ser implementado en una clase de manera implícita o explícita, es decir, anteponiendo el nombre del interfaz a cada uno de los métodos que contiene. La implementación explícita es útil cuando se implementan varios interfaces al mismo tiempo y ambos comparten un mismo nombre para un miembro.

  En relación a los interfaces, se utiliza la convención de nombres comenzando por I.

  Una buena práctica de programación es constantemente pensar en interfaces, dado que dota al programa de mayor flexibilidad y modularidad. Además, permite trabajar con objetos que implementan un determinado interfaz desde el punto de vista de dicho interfaz, lo que se conoce como poliformismo de interfaz:

            Cafe cafe = new Cafe();
            IBebida cafe = new Cafe();

  Una clase que implementa varios interfaces se indica separándolos por comas.

  El Framwork .NET, dispone de un conjunto de interfaces básicos que sería muy interesante incorporados en muchas de las clases que podamos construir. Implementando estos interfaces, nuestras clases podrán ser usadas en la infraestructura .NET. Estos interfaces son:

  • IComparable: Se utiliza para realizar ordenación de elementos.
  • IEnumerable: Permite incorporar el patrón iterador, lo que permite acceder a una colección sin preocuparse de cómo está implementada.
  • IDisposable: Facilita el trabajo con recursos externos no manejados (Manejo de ficheros, conexiones a bases de datos). 

Liberar objetos de memoria

Para gestionar de manera adecuada la liberación de memoria utilizada por los objetos de una aplicación, se recomiendas estas buenas prácticas:
  • Implementar el  interfaz IDisposable en objetos que utilicen recursos no menejados (Ficheros, conexiones a BBDD..).
  • Bloques try-finally o try-cach-finally, ya que en finally podemos liberar los recursos no manejados.
  • Setencias using{}, que asegura que si se produce una excepción en el bloque using, el runtime llamará al método Dispose del objeto.

A continuación se expone el patrón a seguir para liberar recursos mediante el interface IDisposable:
public class DisposableResourceHolder : IDisposable 
{
    bool _isDisposed;
    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder(){
        this.resource = ... // allocates the resource
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); //Indica a GC que no pierda tiempo finlizando el objeto
    }

    protected virtual void Dispose(bool disposing)
    {
        if (this._isDisposed)
            return;
        if (disposing)
        {
           //Liberar aquí los objetos manejados .NET 
           if (resource!= null) resource.Dispose();
        }
        //Siempre liberar los recursos no manejados

        //Indica que el objeto ha sido liberado
        this._isDisposed = true;
    }
}


Colecciones

Se disponen de las siguientes categorías:
  • Listas:  ArrayList, BitArray, BitVector32, StringCollection.
  • Diccionarios Clave-Valor:  HashTable, SortedList, ListDictionary, HybridDictionary, OrderedDictionary, NameValueCollection, StringDictionary.
  • Colas FIFO
  • Stacks LIFO
Para iterar a lo largo de los elemento de una colección, podremos utilizar principalmente bucles foreach, iteradores o expresions lambda.

Una expresión lambda son expresiones similares a métodos normales (con parámetros de entrada y cuerpo, pero no tiene nombre. Son descripciones de métodos en su mínima expresión. El tipo retornado es determinado por el contexto de uso de la expresión.


Trabajando con Genéricos

  Todos los objetos en .NET tienen como clase principal Object. Cuando almacenamos objetos en una colección tipo ArrayList, cada objeto que añadimos se almacena en formato Object, haciendo uso del poliformismo cuando trabaja con clases base. Para utilizar nuevamente la información almacenada es necesario hacer un "unbox" o casting hacia el tipo de objeto original. Esta práctica puede ser fuente de errores, para ello, se han diseñado los tipos genéricos.

  Se trata de tipos fuertemente tipados de manera que se elmina la posibilidad de cometer errores al hacer operaciones de casting.

  Cuando se usan los tipos genéricos en colecciones, se consiguen una serie de ventajas como son la seguridad de tipos y la eliminación de operaciones de casting (que computacionalmente tienen un coste alto). 

  Un objeto genérico, podemos condicionarlo en su construcción, de manera que podemos definir ciertas condiciones que debe de cumplir. Estas condiciones pueden ser:
  • Un interface
  • Una clase 
  • Un argumento tipo valor
  • Un argumento tipo referencia
  Por ejemplo, si se dispone de una clase genérica, en la que interesa que los objetos de cada clase implemente un determinado interfaz, se indicaría por ejemplo:

           public class CustomList<T> where T : IBeverage, ICompatible<T>
           {

           }

  Uno de los usos más importantes que se dan a los genéricos es con las colecciones de clases, agrupadas en dos categorías:
  • Listas genéricas: List<T>, LinkedList<T>, Queue<T>, Stack<T>.
  • Diccionarios genéricos:  Dictionary<Tkey, Tvalue>, SortedList<Tkey, Tvalue>, SortedDictionary<Tkey, Tvalue>.

Eventos y Delegados

  Los eventos son mecanismos que permiten a los objetos notificar situaciones evitando el tener que estar consultando el estado del mismo de manera recurrente. Por ejemplo la pulsación de un botón.
Un evento implementa en patrón publicación-suscripción, donde solamente la clase que lo declara puede lanzarlo, mientras que los subscriptores solamente pueden añadirse o eliminarse de la lista de invocación.

  Un delegado define la firma de un método, es decir los parámetros del método y el tipo de retorno. Un delegado se comporta como un representante de métodos que comparten la misma firma, pueden ser instanciados, pasados como argumento o invocados.

Para crear un evento es necesario:
  • Declarar una variable pública event en el objeto que genera el evento.
  • Declara un delegado que defina como debe de ser la firma de la función que deben implementar los objetos que se subscriban al evento.
  Para lanzar el evento, deberemos ejecutar el nombre del evento declarado pasando como argumentos los indicados en la firma del delegado. Éstos argumentos son una referencia al objeto que lanza el evento (this) y un objeto EventArgs (o un objeto que lo extienda).

Para manejar un evento al que un objeto se ha subscrito debe:
  • Crear un método (handler) con una firma que encaje con el delegado del evento.
  • Subscribirse al evento (+=) para relacionar el método handler con el evento.
Si interesa dejar de estar subscrito a un determinado evento, la manera de hacerlo es utilizando el operador de asignación (-=).



MultiTarea


  Una aplicación gráfica típica consiste en un bloque de código que se ejecuta cuando ocurre un evento. Estos eventos se disparan en respuesta a las acciones del usuario (por ejemplo las pulsaciones de botón). Por defecto, este código se ejecuta en el hilo de trabajo del interfaz de usuario, sin embargo, una buena práctica consiste en evitar que operaciones largas y pesadas sean ejecutadas en este hilo ya que pueden ocasionar falta de respuesta de dicho interfaz.

  Por otro lado, ejecutar todo el código en un único hilo o thread, no permite sacar el máximo partido a la potencia de ejecución de un microprocesador con varios núcleos. Es posible por tanto, utilizar la programación en paralelo para distribuir el código de ejecución en diferentes hilos de trabajo, asignando dichos hilos a diferentes núcleos del procesador.

  Para ello, el framework .NET incluye la librería Task Parallel, donde es posible instanciar objetos de la clase Task ( Una tarea es por tanto una unidad de trabajo.). De este modo, el runtime será capaz de optimizar el número de hilos o threads requeridos para tareas concurrentes en la aplicación.

  Mediante esta librería es posible el implementar sofisticadas funcionalidades, posibilitando el poder gestionar la interacción entre tareas (pausar tareas, esperar finalización de tareas, encadenamiento de tareas..) con el objetivo de cumplir con los requerimientos de rendimiento de la aplicación. 

  Para crear una tarea (Task) es necesario indicar mediante parámetro, el código que se deberá ejecutar (a través de un delegado). Para ello es posible hacerlo mediante las siguientes opciones:

  • Action delegate: La clase Action permite convertir cualquier método (que no devuelva ningún valor) en un delegado. La clase Func permite definir un delegado que pueda devolver un valor.
  • Método Anónimo: Se trata de métodos sin nombre, que en conbinación con la palabra clave delegate, es posible asignarlo a una tarea.
  • Expresiones Lambda (=>): Se trata de expresiones, cuya sintaxis permite definir de forma concisa delegados anónimos. Permite el recibir parámetros y devolver un resultado. Constructor de expresión lambda: (parámetros) => expresión. Ejemplo: (x,y)=> {x=y;} Las expresiones lambda son la opción recomendada cuando se trabaja con tareas.

Es posible el control de la ejecución de las tareas pudiendo inicializarlas, pausarlas o esperar la ejecución de las mismas.

  Cuando una tarea se inicia (Task.Start), un thread (hilo) es asignado a dicha tarea, ejecutándose de manera separada a la tarea principal que la inició, no siendo necesario esperar su finalización.

  En otros casos, se requerirá el pausar la ejecución del código hasta que una tarea en particular haya sido completada. Esto es interesante cuando el código depende del resultado de una o más tareas (Método Task.Wait).

  En situaciones reales, se da la circunstancia de que es necesario que la tarea que se ha ejecutado en segundo plano, sea capaz de devolver un valor de resultado a la tarea principal. Para ello es posible la utilización de la clase Task<TResult>. Esta clase dispone de una propiedad de sólo lectura llamada Result, la cual es posible utilizar tras la finalización de la misma.
Un detalle importante es que si se accede a la propiedad Result antes de que la tarea secundaria haya finalizado, el código de la tarea principal esperará la finalización de la tarea secundaria antes de continuar.

  Dado que los hilos de ejecución secundarios son utilizados para llevar a cabo tareas pesadas en términos de ejecución, de manera que no se bloquee el hilo principal, en algunas ocasiones será necesario cancelar la ejecución del hilo secundario una vez iniciado. Para ello, es conveniente utilizar los tokens de cancelación.

  La manera de realizar una cancelación adecuada de una tarea secundaria, de manera que se asegurase la integridad de los datos manejados sería:

  • Al crear una Task, crear tambien un token de cancelación.
  • Pasar como argumento al delegado de la Task el token de cancelación creado.
  • En el thread asociado a la Task se procederá a consultar el estado del token para comprobar si se ha solicitado la cancelación. Si es así, realizar las operaciones oportunas para finalizar correctamente la tarea secundaria (Posiblemente deshaciendo los cambios efectuados durante la ejecución de la tarea secundaria).
  • Desde la ejecución de la tarea secundaria, es posible utilizar Token.ThrowIfCancellationRequested, de manera que si se ha solicitado la cancelación se lazará la excepción TaskCanceledException, que deberá ser capturada por la tarea principal.
  En ocasiones es interesante el enlazar tareas de manera consecutiva. Por ejemplo, si una tarea termina satisfactoriamente, otra tarea inicia su ejecución inmediatamente. O por el contrario si la tarea falla, se inicia otra tarea que realiza un proceso de recuperación.
Además, una tarea puede generar otras tareas si el trabajo que necesita llevar a cabo demanda el trabajar en paralelo. Así, la tarea padre puede esperar a que las tareas anidadas terminen, antes de terminar ella misma, o puede finalizar y dejar que las tareas hijas continúen de manera asíncrona. Las tareas que pueden hacer esperar a la tarea padre se denominan tareas hijas.

  Las tareas de continuación permiten el enlazar múltiples tareas juntas de manera que se ejecutan una después de otra (Task.ContinueWith). La tarea que invoca a la tarea siguiente en la cadena se denomina tarea antecedente, y la tarea que le sigue se denomina tarea de continuación.
Es posible pasar información de la tarea antecedente a la tarea de continuación y es posible también controlar la ejecución de la cadena de tareas.
Un ejemplo de tareas encadenadas sería el iniciar una tarea para recoger datos, y continuar con otra tarea que realizaría el procesado de los mismos.

  Las tareas anidadas (nested tasks) son tareas creadas dentro del delegado de otra tarea. En este caso, ambas tareas son esencialmente independientes, y la tarea principal no tiene porqué esperar la finalización de la tarea anidada. Las tareas anidadas son útiles dado que permiten dividir el trabajo en pequeñas unidades de trabajo que pueden ser distribuidas en diferentes hilos.

  Las tareas hijas (child tasks) son un tipo de tareas anidadas en la que se especifica la opción "AttachedToParent" al crearse. De esta manera, una tarea padre no puede completar hasta que todas sus tareas hijas han finalizado su trabajo. La tarea padre propagará cualquiera de las excepciones enviadas por las tareas hijas. Las tareas hijas permiten el llevar un control sincronizado asegurando que ciertas tareas hijas finalizaron antes de continuar.

  Cuando una tarea lanza una excepción, ésta es propagada hacia la tarea que la inició previamente. Esto incluye tareas de continuación, tareas anidadas y tareas hijas. Para asegurar que todas las excepciones son propagadas hacia el hilo principal, la librería Task Parallel unifica todas las excepciones en un objeto llamado AggregateException.
Para capturar todas las excepciones en el hilo principal, es necesario esperar la finalización de las tareas en el bloque Try y capturar las excepciones AggregateException en el correspondiente bloque Catch.

  Una operación asíncrona es aquella que se ejecuta en un hilo separado. El hilo que inicializa la operación asíncrona, no necesita esperar a que finalice. Las operaciones asíncronas están muy relacionadas con las tareas. Las operaciones asíncronas permiten crear nuevas tareas y coordinar sus acciones, permitiendo que el programador se pueda centrar en la lógica de la propia aplicación.

  Cada hilo, se encuentra asociado a un objeto denominado Dispacher, el cual es responsable de mantener una cola de trabajo para que sea ejecutado por el hilo asociado.
Cuando se trabaja con varias hilos de trabajo asíncronos, es posible utilizar el objeto dispacher para invocar la lógica subyacente de un hilo concreto. Por ejemplo, si un control en un formulario WPF recibe un evento de pulsación de un botón, el cual inicia una tarea asíncrona, desde esta tarea no es posible actualizar los controles del formulario, ya que éstos pertenecen al hilo del interfaz de usuario. Para ello será necesario el usar el método Dispatcher.BeginInvoke para realizar la actualización el UI con la nueva información generada.

  Las palabras clave async and await, permiten facilitar el trabajo relacionado con las operaciones asíncronas. Se utiliza el modificador async para indicar que un método es asíncrono, y se utilizar el operador await para indicar en qué punto en concreto la ejecución del código será suspendido hasta que las operaciones del alto coste de ejecución terminan. Mientras el método es suspendido, el hilo que invocó el método asíncrono puede realizar otro trabajo en paralelo.
Es decir, async y await permiten realizar operaciones asíncronas en un único hilo de trabajo

  El operador await se utilizar para esperar la finalización de una tarea de un modo que no bloquee el hilo de trabajo. Para crear un método asíncrono de manera que sea posible esperar su finalización utilizando el operador await, dicho método debe retornar un objeto Task.

 Para crear un método síncrono en asíncrono, realizaremos los siguiente:
  • Si el método asíncrono no devuelve ningún valor (void), el método asíncrono debe devolver un objeto Task.
  • Si el método asíncrono devuelve un determinado tipo (TResult), el método asíncrono debe devolver un objeto Task<TResult>.
  • Añadir el modificador async en la declaración del método asíncrono.
  • Modificar la lógica del método utilizando el operador await en la operación que consuma los recursos.
  Por ejemplo, si estamos trabajando en un aplicación de escritorio que ejecuta una operación en la que se obtienen datos desde un servicio web para posteriormente almacenarlos en una base de datos, es posible valerse de los operadores async/await para implementar esta funcionalidad.
Se trata de un caso de uso, en el que hay dependencia de factores externos como son la conexión a una base de datos y a un servicio web remoto. Se pueden utilizar estos operadores y liberar al hilo principal de esperar la finalización de esta tarea permitiendo mientras tanto realizar otras tareas distintas. De esta manera se mejora la capacidad de respuesta, dado que el sistema operativo será el encargado de esperar la finalización de la petición I/O, activando nuevamente el hilo que procesa dicha petición en el momento adecuado. (async/await también puede ser usados en aplicaciones de servidor donde se carece de interfaz de usuario, asociándolos con tareas de tipo I/O).

  Para los casos en los que es necesario llevar a cabo una compleja tarea de procesamiento después de que un método asíncrono finaliza, es posible configurar dicho método para que invoque un método callback y así se realice el procesado de dicha información (por ejemplo, actualizar el UI con el resultado de procesado de la información).

 Para configurar un método asíncrono que ejecute un método callback, es necesario incluir un delegado para el método callback, como parámetro del método asíncrono. Un método callback normalmente puede aceptar varios argumentos y no devuelve ningún valor. Esto hace el delegado Action<T> un buena opción para representar al método callback, donde T es el tipo de argumento (pudiéndose aceptar hasta 16).

  Cuando se realiza una operación asíncrona utilizando las palabras clave async-await, es posible el controlar las excepciones que se pudiesen producir, de la misma manera que se realizan en un código síncrono, es decir, utilizando los bloques try/catch.
Aunque la operación que causa una excepción se produzca en un método asíncrono, el control se devuelve al método que inició la operación asíncrona, capturándose la excepción correctamente. Esto es debido a que la librería Task Parallel, de manera interna, captura la excepción y la lanza nuevamente sobre el hilo que inició la operación asíncrona.

  La multitarea puede mejorar la capacidad de respuesta de una aplicación. El interfaz de usuario puede procesar peticiones del usuario, mientras en segundo plano se ejecutan otras operaciones.

  Una operación que necesitemos sea asociada a un determinado núcleo de CPU (CPU-bound operations), necesita de un hilo o thread para ejecutarse. En una aplicación cliente, puede tener sentido para mejorar la experiencia de usuario y la capacidad de respuesta. Sin embargo, en una aplicación de servidor esto deja de tener importancia, ya que lo que es necesario en este contexto es mejorar la escalabilidad, y para ello, las operaciones asíncronas (Asynchronous I/O operations), que no requieren de nuevos hilos o threads, cobran más importancia. Este tipo de operaciones liberan al hilo principal el realizar ciertos trabajos, lo que mejora dicha escalabilidad.

  Una operación I/O (async/await) no detiene la ejecución del hilo de trabajo, sino que dicho hilo puede procesar otras tareas mientras el sistema operativo monitoriza la finalización de la operación I/O. En cambio, una operación "CPU-bound", en la que un hilo auxiliar ejecuta la operación, hace necesario que el hilo principal sea quien espere la finalización de la tarea.

  Así pues, el utilizar la multitarea permite el distribuir el trabajo entre las diferentes CPU's, mejorando el rendimiento, sin embargo, es necesario tener en cuenta que utilizar la librería TPL para crear threads alternativos que ejecuten operaciones a las cuales el hilo principal deba espera su finalización, no mejorará el rendimiento.

  Conviene recordar:

  • Un hilo puede ser entendido como una CPU virtual.
  • Utilizar múltiples hilos puede mejorar la capacidad de respuesta de un UI.
  • Es posible utilizar la clase Thread para crear los hilos de manera explícita, aunque es conveniente utilizar ThreadPool para gestionar y reutilizar hilos de manera más eficiente, dejando que este trabajo "behind-the-scenes" sea llevado a cabo por el propio Runtime.
  • Un objeto Task, representa un trabajo que necesita ser ejecutado. Se recomienda utilizar estos objetos para llevar a cabo la multitarea.
  • La clase Parallel, se utiliza para ejecutar código en paralelo, asociado por ejemplo a bucles de trabajo.
  • PLINQ es una extensión de LINQ que ejecuta consultas en paralelo.
  • Los operadores async y await, se utilizan para escribir de manera más fácil código asíncrono.
  • Las colecciones concurrentes (BlockingCollection, ConcurrentBag, ConcurrentDictionary, ConcurrentQueue o ConcurrentStack) se utilizan para trabajar de manera segura (thread-safe) con datos en el contexto de la multitarea, en el que se accede a los mismo desde distintos hilo de forma simultánea.
  • En entornos multitarea, es necesario sincronizar el acceso a datos compartidos para evitar errores y la corrupción de datos. Para ello, es posible utilizar la sentencia lock en variables de tipo objeto, o utilizar la clase Interlocked, la cual permite realizar operaciones atómicas (Esto último muy útil en máquinas de estados donde se comprueba el estado actual para moverse al siguiente). 

Notas finales de utilidad:

  • Un assembly en C# almacena código y metadatos.
  • Se denomina Atributos a los metadatos que pueden ser utilizados desde código en tiempo de ejecución.
  • Reflection, es el proceso de inspeccionar los atributos de una aplicación C#.
  • La memoria en C# se divide en dos zonas denominadas stack y heap. El heap es gestionado por el garbage collector, el cual libera las referencias a los objetos que ya no se utilizan. Es una zona de memoria más segura, pero más lenta y costosa en cuanto a rendimiento. El stack es utilizado para almacenar tipos no refernciados (valor) y por tanto es más rápida. 
  • Un finalizador (~finalizer), es código ejecutado por el garbage collector cuando libera la memoria asociada a un objeto. Es ejecutado de manera no determinista cuando el garbage collector decide que es un buen momento. IDisposable es una manera de liberar recursos de manera determinista. Cuando se utilizan finalizers e IDisposable en un mismo tipo, es importante recordar que una clase con finalizer es tratada de manera especial en el sentido que es mantenida en memoria hasta que el garbage collector lo ejecuta. Si se utiliza el camino de liberar recursos a través de IDisposable, es necesario indicar explícitamente al garbage collector que no será necesario ejecutar el finalizer de dicho objeto (SuppressFinalize).   


  Hasta aquí las notas básicas de programación en lenguaje C#. Esto es solo una primera aproximación, a partir de aquí toca estudiarlo y profundizar un poco más. Dejo la referencia de algunos libros que entiendo pueden ser de interés:
  • CLR via C# (Fourth Edition) Jeffrey Richter, Microsoft Press.
  • Developer Step by Step. Microsoft Visual C# 2013. John Sharp. Microsoft Press.
  • Clean Code. Robert C. Martin. 
  • Programming in C#. Exam Ref 70-483. Wourter de Kort. Microsoft Press.

No hay comentarios:

Publicar un comentario

Por favor, si consideras necesario realizar algún aporte, feel free to do it!!