Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

domingo, 23 de enero de 2011




Manual del

Lenguaje de programación

C#

Parte V-A


Programación con hilos múltiples (Threads) y uso de Patrones de Diseño



OBJETIVO DEL MANUAL 3

Un motor de procesos batch 3

La Interfaz Comando 4

Las clases que implementan la interfaz Comando 4

La clase miClase 4

La clase miOtraClase 6

El motor batch de tareas asíncronas 8

Un programa que utiliza el motor batch 10

Una mejor versión del motor de procesos batch 12

La Interfaz de Comandos 12

La clase abstracta AbstractComando 15

Una mejora en el motor batch de tareas asincrónicas 16




OBJETIVO DEL MANUAL


Es intención en este manual presentar un modelo de implementación de threads así como la implementación de dos patrones de desarrollo clásicos: Command y Strategy.-

Para eso vamos a plantearnos un motor batch , es decir un proceso que sea capaz de disparar procesos batch (no interactivos ) en diferentes hilos de ejecucion


Un motor de procesos batch

Un motor de procesos batch es un software que es capaz de ejecutar procesos batch de modo concurrente y controlado.-

Lo que expondremos acá es un modelo simple del mismo del que después pueda derivarse un motor de uso profesional.-

Lo importante del motor desde el punto de vista de la arquitectura es que se trata de una aplicación que es capaz de manejar múltiples hilos de ejecución, colocar en cada uno de esos hilos un proceso a ejecutar , lanzar dichos procesos , esperar su conclusión y reportar que el proceso finalizo.-

Lo importante del motor desde el punto de vista de la técnica de diseño es que cumple con un patrón de los enunciados por Gamma y otros (GoF []). Este patrón se denomina Command (Comando).-

La idea de Comando como patrón de nuestro diseño es un objeto que tenga 3 estadios clásicos: Inicio, ejecución y cierre. Llamaremos a esos métodos como Inicio(), run() y CleanUp().-



La Interfaz Comando


using System;

using System.Collections.Generic;

using System.Text;


namespace ThreadPoolTest_5

{


public
interface
Comando

{


void inicio();


void run();


void cleanup();

}



}


Las clases que implementan la interfaz Comando

Veamos ahora clases concretas de la Interfaz comando. Estas clases implementan los métodos declarados en la interfaz.-

Daremos dos implementaciones una llamada miClase y la otra llamada miOtraClase.-

No tienen nada de particular salvo que ambas implementan a la interfaz Comando.-

La clase miClase


using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;


namespace ThreadPoolTest_5

{

public class miClase : Comando

{

private string _nombre;

private bool _terminado;


public string nombre

{

get { return _nombre; }

set { _nombre = value; }

}


public bool terminado

{

get { return _terminado; }

set { _terminado = value; }

}


public void inicio()

{

_terminado = false;

}




public void run()

{

Console.WriteLine(this.GetType().ToString() + " :Procesando requerimiento '{0}'." +

" Hash: {1}",

(string)nombre, Thread.CurrentThread.GetHashCode());



// Simulacion de tiempo de procesamiento

int ticks = Environment.TickCount;

//Trabajar mientras no hayan transcurrido los 2 segundos

while (Environment.TickCount - ticks < 2000) ;


Console.WriteLine("Requerimiento '{0}' procesado",

(string)nombre);

}


public void cleanup()

{

_terminado = true;

}


}//clase

}//namespace


La clase miOtraClase


using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;



namespace ThreadPoolTest_5

{

public class miOtraClase : Comando

{

private string _nombre;

private bool _terminado;


public string nombre

{

get { return _nombre; }

set { _nombre = value; }

}


public bool terminado

{

get { return _terminado; }

set { _terminado = value; }

}


public void inicio()

{

_terminado = false;

}



public void run()

{

Console.WriteLine(this.GetType().ToString() +": Procesando requerimiento '{0}'." +

" Hash: {1}",

(string)nombre, Thread.CurrentThread.GetHashCode());



//Calcular

CalcularCosenos();


Console.WriteLine("Requerimiento '{0}' procesado",

(string)nombre);

}


private void CalcularCosenos()

{


const double EPSILON = 0.000000001;

const double DESDE = 0.5346222;

const double HASTA = 3.1415926;

const double PASO = 0.000001999;


double lRadianes = DESDE;

double RadCuadrado;

double s;

double t;

double dFabsLRas;

double dFabsProd;

int k;

string Aux = "";



while (lRadianes < HASTA)

{

k = 0;

t = 1;

s = 1;

RadCuadrado = lRadianes * lRadianes;

dFabsLRas = Math.Abs(t);

dFabsProd = (EPSILON * Math.Abs(s));


while (dFabsLRas > dFabsProd)

{

k = k + 2;

t = (-1) * t * RadCuadrado / (k * (k - 1));


// 1 - x^2/2! ...

s = s + t;

dFabsProd = (EPSILON * Math.Abs(s));

dFabsLRas = Math.Abs(t);

}


Aux = lRadianes.ToString() + ":" + s.ToString();

//Mostrar Aux


lRadianes += PASO;

}





}

public void cleanup()

{

_terminado = true;

}

}

}


El motor batch de tareas asíncronas

Ahora vamos a implementar el motor.-

El motor de tareas asíncronas representa una implementación de un modelo de procesamiento al que también llamamos monitor.

Se trata de un software capaz de disparar OTRAS tareas según una configuración de tiempos.-

En el ejemplo estamos utilizando 4 conceptos sobre los que el motor esta diseñado:

  1. Para que el motor deba ejecutar un proceso en hilos (threads) múltiples usaremos un namespace llamado System.Threading en el que encontramos una clase llamada WaitCallback que es la implementación de un prototipo de función CallBack.-
  2. La clase WaitCallback, necesita en su constructor un método que cumpla con este prototipo :


private
static
void MiFuncion(object miObjeto);

De modo que nosotros creamos un método dentro de nuestra clase de implementación del motor que pueda cumplir con este prototipo.-

  1. La clase ThreadPool, con métodos estáticos públicos es la que nos permitirá levantar cada uno de los procesos que el motor batch dispara. ThreadPool nos permite definir la cantidad de threads que se abrirán en la sesión así como encolar los procesos que disparamos de modo de ejecutarlos secuencialmente.-
  2. EL motor tiene un método llamado Trabaja() que es el que ejecuta la parte central de la tarea. Ahora bien : ¿ Que necesita Trabaja() para hacer su tarea ?.

Necesita una lista de objetos. De alli que Trabaja() tiene como parámetro un contenedor genérico <List> de objetos a procesar.-


using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;



namespace ThreadPoolTest_5

{

class MotorBatch

{


public static void trabaja(List<object> cm)

{


//EL motor tiene su funcion de disparo y arma un

// handler a ella

WaitCallback callBack;

callBack = new WaitCallback(FuncionAEjecutar);



//El motor ajusta la cantidad de threads por procesador

int minWorker, minIOC;

ThreadPool.GetMinThreads(out minWorker, out minIOC);

// 4 threads asincronicos como minimo, por procesador

ThreadPool.SetMinThreads(4, minIOC);


//El motor lanza las tareas

foreach(object com in cm)

{

ThreadPool.QueueUserWorkItem(callBack, com);

}


//El motor sincroniza el pool de threads

//Hasta terminar

Console.ReadLine();

}


private static void FuncionAEjecutar(object mc)

{

Comando xco = (Comando)mc;

xco.inicio();

xco.run();

xco.cleanup();

}


}//clase


}//namespace


Un programa que utiliza el motor batch

Corresponde ahora que hagamos un programa que utiliza el motor batch para dispara procesos. Lo que haremos será que el motor dispare las implementaciones de Comando que hicimos con miClase y miOtraClase.-

Observemos que es lo que nuestro programa hace ahora:

Siendo que el motor necesita que a su método publico y estático Trabaja() se le pase como parámetro un genérico de tipo <List> con objetos, lo primero que hace la aplicación que llamara al motor es generar una lista de dicho tipo.

Podemos ir viendo ya que esta generación es una tarea que debería estar controlada en el sentido que no podemos colocar cualquier tipo de objetos en esa lista sino solamente aquellos que deriven de Comando. Ese punto lo revisaremos en las mejoras que haremos al motor batch.-

using System;

using System.Threading;

using System.Collections.Generic;


namespace ThreadPoolTest_5

{


//

class MainApp

{

static void Main()

{

//Alguien carga los comandos en una lista y el motor

//la invoca

List<object> cm = ListaDeComandos();


MotorBatch.trabaja(cm);


}


static public List<object> ListaDeComandos()

{

miClase mc;

miOtraClase moc;

List<object> comandos = new List<object>();


for (int i = 0; i < 10; i++)

{

if ((i % 2) != 0)

{

mc = new miClase();

mc.nombre = "Hilo nro: " + i;

comandos.Add(mc);

}

else

{

moc = new miOtraClase();

moc.nombre = "Hilo nro: " + i;

comandos.Add(moc);

}

}


return comandos;

}//


}

}



Una mejor versión del motor de procesos batch

La Interfaz de Comandos

Si hacemos un análisis más meticuloso de la estructura que tendrá el motor batch, vemos que el diseño no solamente se adecua al uso de una interfaz (que es la representación de una plantilla con métodos comunes para polimorfismo) sino que podemos tener una clase abstracta con implementaciones de dichos métodos comunes.-

Estas implementaciones en algunos de los casos serán luego sobrescritas por las implementaciones de la clase abstracta y en otros casos serán implementados ya que algunos métodos los declararemos también como abstractos.-



Usando la utilidad View Class Diagram de Visual Studio, podemos ver las relaciones entre la Interfaz (y su declaración de métodos) con la clase abstracta y una clase (miClase) que implementa a esta ultima.-

Esta relación entre Interfaz-Clase Abstracta-Clase Concreta es un ejemplo de cómo debemos diseñar este tipo de problemas de ingeniería:

  1. Un problema requiere de la confección de un algoritmo que ejecuta acciones de objetos que tienen métodos comunes, pero se instancian de CLASES DISTINTAS. En nuestro caso el problema es la ejecución de los métodos de un comando.-
  2. Esto nos lleva a la idea de polimorfismo. Para eso entonces diseñamos la interfaz y pensamos los métodos comunes.-
  3. Existen además métodos que admiten cierta implementación o directamente necesitan de ella. Otros métodos, en cambio, deben ser definido para cada clase concreta
  4. Esto nos lleva a la idea de clase abstracta y métodos virtuales. La clase será la base de otras que heredaran su comportamiento pero lo especializaran.-



  1. El diseño de una clase abstracta es una tarea de pasos sucesivos. No es el primer eslabón de la cadena sino algún punto de culminación ya que implica que hemos visualizado un patrón de comportamiento del sistema y que hay tareas que sabemos que son comunes.-
  2. Si hemos llegado a ese punto del diseño, podremos diferenciar entre aquellos métodos que serán implementados en la clase abstracta y posiblemente ampliados en la subclase, y aquellos otros métodos a los que NECESARIAMNETE implementara la subclase.

    En nuestro caso, Inicio() y YaTermino() por ejemplo, han sido implementadas en la interfaz, en tanto presentan una lógica que entendemos común a todos los derivados.

public virtual void inicio()

{

_terminado = false;

}


public bool YaTermino()

{

return (_terminado == true);

}


EN cambio run() que es el proceso de ejecución en si de la clase, será necesariamente implementado por la subclase. De allí que coloquemos a este ultimo método como abstract.


public abstract void run();


La clase abstracta AbstractComando

Dentro del archivo donde declaramos la Interfaz Comando, colocaremos una clase Abstracta que contenga los métodos básicos y una implementación mínima de los mismos.-

Llamamos a esta clase AbstractComando


using System;

using System.Collections.Generic;

using System.Text;


namespace threadPoolTest_6

{

public interface Comando

{

void inicio();

void run();

void cleanup();

bool YaTermino();

String elNombre();

}



public abstract class AbstactComando:Comando

{

public static string CLASE_BASE = "threadPoolTest_6.AbstactComando";

private string _nombre;

private bool _terminado;


public string nombre

{

get { return _nombre; }

set { _nombre = value; }

}


public String elNombre()

{

return _nombre;

}


public virtual void inicio()

{

_terminado = false;

}


public bool YaTermino()

{

return (_terminado == true);

}


public abstract void run();



public virtual void cleanup()

{

_terminado = true;

}

}

}


Una mejora en el motor batch de tareas asincrónicas


using System;

using System.Collections.Generic;

using System.Collections;

using System.Text;

using System.Threading;

using System.Reflection;



namespace threadPoolTest_6

{

public class MotorBatch

{


public static void trabaja(List<object> cm)

{

//EL motor tiene su funcion de disparo y arma un

// handler a ella

WaitCallback callBack;

callBack = new WaitCallback(FuncionAEjecutar);


//El motor ajusta la cantidad de threads por procesador

int minWorker, minIOC;

ThreadPool.GetMinThreads(out minWorker, out minIOC);


// 4 threads asincronicos como minimo, por procesador

ThreadPool.SetMinThreads(4, minIOC);



//El motor lanza las tareas

foreach(object com in cm)

{

ThreadPool.QueueUserWorkItem(callBack, com);

}


//El motor sincroniza la finalizacion el pool de threads

do

{

foreach (object com in cm)

{

Comando xco = (Comando)com;

if (xco.YaTermino())

{


//TODO: Logger

Console.WriteLine("-> Comando '{0}' procesado. Restan '{1}'", xco.elNombre(), cm.Count);

cm.Remove(xco);

break;

}

}

} while (cm.Count > 0);


//Hasta terminar

//TODO:Logger

Console.WriteLine("Motor Batch Termino");

}


//Esta es la funcion a la que se apunta en el callback

//llama a los 3 metodos de un comando

private static void FuncionAEjecutar(object mc)

{

Comando xco = (Comando)mc;

xco.inicio();

xco.run();

xco.cleanup();

}


}//clase


public class unPar

{

String _nombreClase;

String _nombreYPasoAssembly;


public String nombreYPasoAssembly

{

get { return _nombreYPasoAssembly; }

set { _nombreYPasoAssembly = value;}

}


public String nombreClase

{

get { return _nombreClase; }

set { _nombreClase = value; }

}//-


public unPar(String nombreClase, String nombreYPasoAssembly)

{

_nombreClase = nombreClase;

_nombreYPasoAssembly = nombreYPasoAssembly;

}//-


}//clase


public class ParAssemblyClase

{


static List <unPar> lPar = null;


public static void Agregar(String nombreClase,String nombreYPasoAssembly)

{

if (lPar == null)

lPar = new List<unPar>();


unPar p = new unPar(nombreClase,nombreYPasoAssembly);

lPar.Add(p);

}//


public static List<unPar> TraerLista()

{

return lPar;

}//



//Retornar una lista de comandos a partir de una lista

// de pares Assembly-clase que se busca cargado en la misma sesion

static public List<object> ListaComandos()

{

List<object> comandos = null;


if (lPar != null)

comandos= ListaComandos(lPar);


return comandos;

}



//Retornar una lista de comandos a partir de una lista

// de pares Assembly-clase que se recibe

static public List<object> ListaComandos(List<unPar> tabla)

{


Assembly assem;

Object o;


List<object> comandos = new List<object>();


IEnumerator i = tabla.GetEnumerator();

unPar m;


while (i.MoveNext())

{

m = (unPar)i.Current;


try

{

assem = Assembly.LoadFrom(m.nombreYPasoAssembly);


//Los constructores de las clases que implementan

//Comando no tienen parametros. De alli el parametro

//new Object[] { }

o = assem.CreateInstance(m.nombreClase, false,

BindingFlags.ExactBinding, null, new Object[] { }, null, null);

//TODO: Logger

Console.WriteLine("Clase Base de {0} : {1}",m.nombreClase, o.GetType().BaseType.ToString());


//solo los que extienden AbstractComando son considerados

//procesos a aejecutar

if (o.GetType().BaseType.ToString().Equals( AbstactComando.CLASE_BASE))

comandos.Add(o);

}

catch (Exception ex)

{ //TODO: Logger

Console.WriteLine("Excepcion " + ex.StackTrace);

}


}


return comandos;


}//-

}//clase


}//namespace


Mas sobre reflection




Manual del

Lenguaje de programación

C#

Parte III


Reflection




OBJETIVO DEL MANUAL 3

OBJETIVO DEL MANUAL 3

Reflection 3

¿Como funciona esto y para que sirve realmente? 3

El enfoque en C# 7

Ejemplo I: 7

¿Como acceder a una clase del assembly? 7

¿Como acceder a un metodo de la clase? 8

Algo más al respecto 12

EJEMPLO II 13

El proyecto biblioteca1 14

El proyecto PruebaBiblioteca 16

¿ Como enlazar una biblioteca al proyecto ? 18

El proyecto UsoDeReflection 21

¿Como acceder a los modulos de un assembly? 22

Accediendo a los metodos 24



OBJETIVO DEL MANUAL

Reflection


Para la ejecucion de una aplicación .NET debemos tener presente el concepto de CONTENEDORES. Este es un concepto que se ha popularizado los ultimos años a traves de Java y difiere de los habituales run-times en que no es un agregado que lleva el ejecutable o un conjunto de bibliotecas que resuelven aspectos especificos (como graficos), SINO QUE ES UNA MAQUINA VIRTUAL dentro de la cual corre la aplicación.-

Es decir esta cascara es la que separa a la aplicación del Sistema operativo.-

Sus ventajas en el mundo Java son obvias en tanto estos contenedores aislan al programador de especificidades de tal o cual sistema operativo.-

No nos resultan tan obvias en el mundo Microsoft donde existe UN SOLO sistema operativo.-

Sin embargo hay otras decisiones que seguramente habrán pesado en la decisión de Microsoft de hacer un esquema de contenedores.-

El primero es que este esquema permite que exista un nivel de compilación que no es la
generación del ejecutable mismo, sino de un lenguaje intermedio (MSIL para Microsoft, Bytecodes para Java y… P-CODE para quienes tuvimos oportunidad de trabajar profesionalmente con el maravilloso Pascal ¡!).-

El segundo es que varios lenguajes (al menos Visual Basic, C# y C++ en modo manejado) pueden trabajar sobre esta maquina virtual y compartir aquellos servicios, bibliotecas y assemblies que la maquina virtual necesita. Esto es un enorme avance desde el punto de vista de cómo cada lenguaje expone funcionalidades comunes a los programadores.-

Tambien aca podemos decir que esto existía, para determinados servicios, en los sistemas operativos propietarios de Digital e IBM.-

Sobre todo en los del primero, su sistema operativo VMS (hoy propiedad de Hewlett Packard) presentaba para la familia de lenguajes (Fortran, C, Cobol y Basic) idénticos nombres de rutinas para todo el sistema de I/O y de archivos ISAM.-

Solo que en estos casos un runtime que se adjuntaba al programa compilado era el encargado de proveer esta funcionalidad comun. Esto por supuesto duplicaba el codigo en cierto punto, aunque los servicios que utilizaba el runtime estaban en el kernel del SO por lo que no era tan problemático.-

¿Como funciona esto y para que sirve realmente?

Vamos a hacer un par de aplicaciones que nos muestren como funciona esto.

Antes de avanzar nos gustaría que repasemos como, en un lenguaje como el C, resolvemos un problema común:

Queremos invocar un método, que esta en una biblioteca CUYO ENLACE no se produce cuando compilamos, sino en tiempo de ejecución.-

Esto da a nuestros programas otra versatilidad. Es decir nuestro programa es capaz de:

Cargar una biblioteca a traves de su nombre LITERAL. Es decir un string en tiempo de ejecucion (por ejemplo leido de un archivo de configuración) dira cual es la biblioteca a a cargar)

Colocar un apuntador a una funcion de dicha biblioteca (definible tambien como un string)

Hay alguna habilidad que debe tener el SO en el que trabajamos y es la de aceptar bibliotecas compartidas. En Windows estas tienen la extension .DLL y en UNIX la extension .SO

Es decir la biblioteca que invocaremos desde nuestra aplicación sera del tipo compartida (Shared) y no del tipo estatica (STATIC).-

El ejemplo lo haremos para UNIX solo para ver alguna pequeña diferencia de cómo lo hariamos en Windows.-

El programa que haremos sabrá cargar una biblioteca por nombre. En nuestro caso los nombres son lib1.so y lib2.so. Estos nombres son totalmente arbitrarios y podrían ser cualquier otro valido para el Sistema operativo.-

El programa cargara alguna de estas 2 bibliotecas y llamara la función saludo().

Esta función existe en ambas bibliotecas y el programa es capaz de trabajar con esta redefinicion (override) ya que dependera de la biblioteca que cargue lo que determinara cual es la tarea que termine realizando.-

Este es el codigo de una biblioteca escrito en ANSI C:


#include <stdio>;

#include <dlfcn.h>; /* bibliotecas de manejo dinamico de funciones:

dlopen(), dlclose(), dlsym() ... */


/* Explicamos como usar el programa. */

void usage(int argc, char* argv[])

{

fprintf(stderr, "Usar asi: %s [12] a los efectos de llamar a la lib1 o a la lib2 \n", argv[0]);

exit(1);

}


int main(int argc, char* argv[])

{

int lib_num; /* Que biblioteca usar ? */

char biblioteca[100]; /* nombre del archivo de la biblioteca */

void* lib_handle; /* manejador para la biblioteca compartida cargada*/

void (*fn_de_la_biblioteca)(); /* puntero a UNA FUNCION de la biblioteca*/

const char* error; /* por si hay errores... */


/* necesito un argumento */

if (argc != 2)

usage(argc, argv); /* Chau!! */


/* Esta es la biblioteca a cargar. */

lib_num = atoi(argv[1]); /* */


if (lib_num < 1 lib_num > 2)

usage(argc, argv); /* Chau !! */



/* ahora armo el nombre de la biblioteca concatenando la palabra "lib" con el numero 1 o 2

para obtener lib1.so o lib2.so */

sprintf(biblioteca, "lib%d.so", lib_num);



/* carga la biblioteca que desea (lib1.so o lib2.so) */

lib_handle = dlopen(biblioteca, RTLD_LAZY);

if (!lib_handle) {

fprintf(stderr, "Error: %s\n", dlerror());

exit(1);

}


/* cargar la funcion saludo QUE ESTA EN CUALQUIERA DE LAS DOS BIBLIOTECAS !*/

fn_de_la_biblioteca = dlsym(lib_handle, "saludo");


error = dlerror();

if (error) {

fprintf(stderr, "Error: %s\n", error);

exit(1);

}


/* Llamar a la funcion. */

(*fn_de_la_biblioteca)();


/* liberar el recurso */

dlclose(lib_handle);


return 0;

}


Este modulo es LIB1.SO:


#include <stdio>;


/* Esta es la funcion de la biblioteca*/

void saludo()

{

printf("Que puedo decir que no sea HOLA MUNDO?\n");

}


/* cleanup de la biblioteca */

void _fini()

{

printf("Limpiando la biblioteca 'lib2.so'\n");

}


Este modulo es LIB2.SO:

#include <stdio.h>;


/* funcion de inicializacion: OBLIGATORIO llamarla '_init'. */

void _init()

{

printf("Inicializando biblioteca 'lib1.so'\n");

}


/* Esta es lo funcion que nos importa */

void saludo()

{

printf("HALOAA !!\n");

}


Como vemos este problema responde a un patrón común: La resolución en tiempo de ejecución de una funcionalidad determinada.-

Los lenguajes modernos como C# y Java resuelven esto con el paradigma denominado programación reflexiva.

Como en muchos otros conceptos de estos lenguajes modernos, las ideas se generaron en Smalltalk y C++.-


El enfoque en C#

El cargador del lenguaje común administra lo que se denominan DOMINIO de APLICACIONES.

El lenguaje común es el CONTENEDOR que aísla al programa del SO. Cualquier aplicación desarrollada en .NET no se comunicara directamente con APIs de WIN32, tal como lo hacíamos anteriormente sino a través de funciones, servicios, etc. del contenedor.-

EL DOMINIO de la APLICACIÓN es el contorno o los limites en los que trabajan aquellos objetos que pertenecen al mismo alcance de aplicación.-

La administración de este cargador incluye la carga de cada assembly en el dominio de aplicación adecuado y controla la disposición y arreglo en memoria del tipo de jerarquía en cada assembly.-

Veamos unos ejemplos.-

Ejemplo I:

En este caso haremos un proyecto que contendrá las clases que queremos acceder a través de Reflection.-

Es por eso que utilizamos el método estático:

Assembly.GetExecutingAssembly();

para poder tener un objeto de la clase Assembly.

Este es el modo de hacerlo cuando lo que necesitamos es un objeto de una clase que está dentro del código actualmente en ejecución.-

Obtenemos un objeto de la clase Assembly de este modo: Assembly assem = new Assembly())

Este el objeto creado nos permitirá, por ejemplo, instanciar objetos de las clases contenidas en el assembly.-


Para eso el primer paso es:

¿Como acceder a una clase del assembly?


El objeto de la clase Assembly tiene un método llamado CreateInstance(). Este método admite como parámetro el nombre de una clase así como los parámetros que necesitase su constructor.


Este método es uno de los métodos capitales de la programación reflexiva ya que nos permite a través de un texto que representa el nombre de la clase, obtener un objeto que es una instancia de la misma.-


El código siguiente muestra la utilización del método CreateInstance():


// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene

// El constructor de Ejemplo2 necesita un parametro que es un entero

//


Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding,

null, new Object[] { 2 }, null, null);


Observe que se accede a una clase y se instancia un objeto de la misma a través de un parámetro que es un texto ("Ejemplo2").-

Observe también que uno de los parámetros de CreateInstance() es un array de Object al que se le envían los parámetros QUE EL CONSTRUCTOR de la clase necesita.

Esto es: No es suficiente con conocer el nombre de clase en el assembly sino que es preciso conocer algo más de ella.

Esto es similar a lo que nos sucede en el programa C que vimos al inicio en el que debemos conocer algo del PROTOTIPO de la función para hacer una llamada exitosa.-

¿Cómo acceder a un método de la clase?

Ahora bien, tenemos un objeto O que es una instancia de una clase del assembly.

¿Cómo hacemos ahora para acceder a un método de dicha clase?

El handler al assembly (assem) tiene un método llamado GetType que obtiene información sobre el tipo exacto que se produce en tiempo de ejecución para el objeto corriente.-

Es decir GetType retorna un tipo (Type). La clase Type es la base de reflection para acceder a la metada del objeto.

Type es una interfaz que tiene distintas implementaciones. La que aquí nos interesa, tiene un método llamado GetMethod() que, tomando como parámetro el nombre del método, nos trae información del mismo. Asi:



MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");



Obtenida la información sobre el método, se lo puede invocar a través del método Invoke de MethodInfo .

Igual que en el caso anterior, si bien es posible obtener acceso al método a través de un literal (en este caso el nombre del método es : MetodoDeEjemplo ), el uso posterior del método requiere conocer cuáles son los parámetros adecuados.


Object ret = m.Invoke(o, new Object[] { 42 });


En este caso el entero 42 se corresponde al tipo de datos que corresponde al Método, MetodoDeEjemplo

EL retorno que produce Invoke es un Object. EL uso posterior del mismo será parte de la lógica de negocios del programa.-

Arme un proyecto de consola simple y coloque el código indicado en un modulo cs (C#)


Y pruebe este código:


using System;

using System.Collections.Generic;

using System.Text;

using System.Reflection;

using System.Security.Permissions;

using System.Configuration;




class Ejemplo2

{

private int factor;

public Ejemplo2(int f)

{

factor = f;

}


public int MetodoDeEjemplo(int x)

{

Console.WriteLine("\nEjemplo2.MetodoDeEjemplo({0}) ejecutado.", x);

return x * factor;

}


public string OtroMetodo(string a)

{


StringBuilder st = new StringBuilder("A este dato le concateno: ");

Console.WriteLine("\nEjemplo2.OtroMetodo({0}) ejecutado.", a);

st.Append(a);

return st.ToString();

}



}


public class ejecuta

{


public static void Main()

{


Assembly assem = Assembly.GetExecutingAssembly();


Console.WriteLine("Nombre completo del Assembly:");

Console.WriteLine(assem.FullName);


//

AssemblyName assemName = assem.GetName();

Console.WriteLine("\nName: {0}", assemName.Name);

Console.WriteLine("Version: {0}.{1}",

assemName.Version.Major, assemName.Version.Minor);


Console.WriteLine("\nAssembly CodeBase:");

Console.WriteLine(assem.CodeBase);


// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene

//El constructor de Ejemplo2 necesita un parametro que es un entero

//

Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding,

null, new Object[] { 2 }, null, null);


// Ahora apuntamos a un metodo del objeto:


MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");

Object ret = m.Invoke(o, new Object[] { 42 });

Console.WriteLine("MetodoDeEjemplo retorno: {0}.", ret);


MethodInfo m2 = assem.GetType("Ejemplo2").GetMethod("OtroMetodo");

ret = m2.Invoke(o, new Object[] { "Este Texto" });

Console.WriteLine("OtroMetodo retorno: {0}.", ret);


Console.WriteLine("\nPunto de entrada del Assembly:");

Console.WriteLine(assem.EntryPoint);

}

}


La ejecución del código nos muestra lo siguiente:



Algo más al respecto


Los assemblies contienen módulos, los módulos contienen tipos y los tipos contienen funciones miembro o métodos.

-

Lo que Reflection provee son objetos que encapsulan a estas unidades (assemblies, módulos y tipos) de modo de crear dinámicamente una instancia de un tipo, ligar ese tipo a un objeto existente u obtener el tipo de un objeto existente.-

De este modo podemos invocar los métodos del tipo y/o acceder a sus campos y propiedades. El uso típico de reflection debería seguir estas reglas:

  • Usar la clase Assembly para definir y cargar assemblies, cargar los módulos que el assembly contiene y que se encuentran en el Manifiesto, ubicar el tipo del assembly en si y crear una instancia del mismo.-

   Esto lo podemos hacer con:


 Assembly a = Assembly.LoadFrom(args[0]);

O


Assembly a = Assembly.GetExecutingAssembly();


EL primer modo es genérico y solo requiere que se indique el assembly. (El paso completo del mismo será obligatorio si no se encuentra en la GAC). El segundo método está restringido a el trabajo con clases que se encuentran en el assembly actual.-

  • Usar el método CreateInstance() para instanciar un objeto de un tipo dado:

Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding, null, new Object[] { 2 }, null, null);


  • Alternativamente observemos que esto puede hacerse así:

MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");

Object ret = m.Invoke(o, new Object[] { 42 });


El método GetType retorna un objeto de la clase MethodInfo.

Observemos que igual que en el caso anterior, el nombre de la clase y el de su metodo, SON LITERALES lo que indica que pdrian ser cargados en tiempo de ejecucion en variables string y la llamada a Assembly.GetType o a Assembly.CreateInstance se podria hacer con datos dinamicos.

Desde ya que esta es la funcion ultima de Reflection.-

Observe de todos modos que debe conocer "algo" del tipo o del metodo de modo de invocar correctamente a los constructores o pasar parametros adecuados a los metodos.-


EJEMPLO II


En este ejemplo haremos una variante que nos llevara a un caso más parecido al que planteamos al principio de esta y que mostramos con un ejemplo en lenguaje C.-

Construiremos 3 proyectos, ambos en la misma solucion solo a los efectos de la simplicidad.

Uno, al que llamaremos biblioteca1, correspondera al armado de una biblioteca donde estan las clases de las que crearemos objetos a los que a accederemos.-

El segundo, al que llamaremos pruebabiblioteca, sera una aplicacin intermedia de ayuda. Esto no es necesario para el proyecto final que es una aplicación que usando reflection hara uso de los metodos de la biblioteca. Pero es una biena idea probar la biblioteca SEPARADAMENTE antes de darle un uso generico. Es por eso que colocamos este proyecto dentro del ejemplo.-

El tercero, que es el que nos importa, sera la aplicación que a traves de Reflection accede a estos objetos y sus metodos en modo dinamico.-

El proyecto biblioteca1

El primer proyecto sera del tipo Class Library y en el colocaremos una clase con dos metodos.-

Luego esta sera nuestra biblioteca armada como Assembly.-

Vamos con el primer proyecto que consiste en armar la biblioteca de la clase que expondra funcionalidad a ser utilizada por el segundo proyecto:


Este es un proyecto Class Library de modo de armar una biblioteca con una clase que exporte sus metodos.-

El codigo que escribiremos para crear tal clase es el que se muestra a continuacion:


using System;

using System.Collections.Generic;

using System.Text;


namespace biblioteca1

{

public class UnaClase

{

public static Boolean SinNumeros(string dato)

{

char []numeros = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

if (dato.LastIndexOfAny(numeros) == -1)

return (true);

else

return (false);

}


public static Boolean SinÑuflos(string dato)

{

char []ñuflos = new char[] { '@', '^', '`', '¨', '~' };

if (dato.LastIndexOfAny(ñuflos) == -1)

return (true);

else

return (false);

}


}

}




Ahora vamos a armar la biblioteca (biblioteca1) como una dll que expondrá la clase UnaClase y sus métodos públicos.-


Esto generara el archivo biblioteca1.dll .-

Revise en la pantalla de salida donde queda generada:




El proyecto PruebaBiblioteca

Ahora vamos a construir el segundo proyecto:



Vamos a hacer una aplicación de consola que utilice la biblioteca creada en el proyecto anterior.

Lo hará del modo habitual, es decir sin utilización de reflection.

EL código C# de este proyecto esta en un solo modulo al que llamaremos Program y es el que se ve a continuación:

using System;

using System.Collections.Generic;

using System.Text;

using biblioteca1;


namespace PruebaBiblioteca

{

class Program

{

static void Main(string[] args)

{

string dato = "ABCDE";

if (biblioteca1.UnaClase.SinNumeros(dato) == true)

Console.WriteLine("{0}: No tiene numeros");

else

Console.WriteLine("{0}: Tiene numeros");


dato = "AB1C2DE";

if (biblioteca1.UnaClase.SinNumeros(dato) == true)

Console.WriteLine("{0}: No tiene numeros");

else

Console.WriteLine("{0}: Tiene numeros");


dato = "A~CDE";


if (biblioteca1.UnaClase.SinÑuflos(dato) == true)

Console.WriteLine("{0}: No tiene Ñuflos");

else

Console.WriteLine("{0}: Tiene Ñuflos");


}

}

}


¿ Como enlazar una biblioteca al proyecto ?

Para enlazar la biblioteca con el proyecto PruebaBiblioteca1 colóquese en la ventana "Solution Explorer" , coloque el mouse sobre el proyecto y pulsando el botón derecho elija Add Reference



Si la bibloteca esta bien construida la salida de la ejecucion de este programa sera algo asi:


El proyecto UsoDeReflection

Armemos ahora el tercer proyecto.-

También será un proyecto de consola.-

Siendo que en el código no haremos una mención explicita a los métodos de la biblioteca, sino que haremos un binding tardío, cuando el programa se ejecute NO NECESITAMOS referenciar la biblioteca1.

Si necesitaremos desde ya el paso completo de donde se encuentra.-

En mi caso la colocare en el disco C: dentro del directorio M3.-



Es por eso que en el codigo hemos colocado:


static void Main(string[] args)

{

Assembly a = Assembly.LoadFrom(@"c:\m3\biblioteca1.dll");

…..

}//-

Desde ya que esta mencion "hard-code" puede ser reemplazada por la lectura de un parametro en un archivo de propiedades o en un archivo de configuracion.-

¿Como acceder a los modulos de un assembly?

El primer metodo que invocamos (llamado VerModulosYTipos) hace una recorrida sobre los modulos del assembly.-

En este caso solo encuentra un solo modulo ya que la biblioteca1 fue armada con un unico archivo cs dentro de un UNICO namespace:



Sobre esto aclaremos que lo que define al modulo no es la cantidad de archivo C Sharp (archivos .cs) ya que en todo caso el archivo físico es una unidad de agrupamiento de código compilable. Pero en el momento de enlazar estos archivos compilados, existen unidades superiores al archivo físico, como:

  • La clase en la que participa ese archivo (recordar que varios archivos se pueden utilizar para componer una sola clase)


  • El namespace al que pertenece el código que está en el archivo.


Con esto presente podemos entender que:


  1. La proyecto con el que armamos la DLL es el que le da el nombre a la DLL.-
  2. Podemos también armar una DLL dentro de un proyecto que se encuentra a su vez dentro de una solución.-
  3. Este proyecto tendrá, en el momento del armado, un NAMESPACE por defecto. Esto significa que dentro del proyecto podemos tener varios NAMESPACES. Esto NAMESPACES permitirán agrupar clases dentro de sí. Esa es la función básica del NAMESPACE: Un agrupador de clases que a criterio del diseñador del sistema deben estar bajo un espacio común.-
  4. El NAMESPACE mas el nombre de la CLASE es lo que denominados nombre calificado completo de la clase.-
  5. El NAMESPACE mas el nombre de la CLASE mas el nombre del método y sus parámetros identifican de modo UNIVOCO dentro de una solución la llamada a un método de una clase.-
  6. Un proceso ejecutándose es un espacio en el que existen recursos de procesador (tiempo y memoria) y en el que todas las partes participantes (métodos de clases y atributos en última instancia) deben estar unívocamente identificados.-


Dentro de una DLL cada MODULO es cada uno de los PROYECTOS que figura dentro de la SOLUCION. (Vea el Ejemplo III para mas detalles sobre estos puntos) .-


El trabajo de VerModulosYTipos es tomar los modulos del assembly en un array de Module[]


Module[] m = a.GetModules();


y de cada modulo (en este caso uno solo) revisar los tipos que los componen.-





for (int j = 0; j < m.Length; j++)

{

……


Type[] tipos = m[j].GetTypes();


for (int i = 0; i < tipos.Length; i++)

{

Type esteTipo = tipos[i];

……..

}

}


Nuestra intencion es utilizar aquellos tipos que nos permitan instanciar directamente. Es decir clases publicas.

Por eso es importante saber mirar esas propiedades o atributos. Se llaman IsClass e IsPublic y lo usamos en:


Console.Write ("Nombre del tipo {0}. Es una clase ? {1}",nombre,esteTipo.IsClass);


y


Console.WriteLine("Es publico ? {0}. ", esteTipo.IsPublic);


Esta es la salida por pantalla de la parte correspondiente a la información sobre los tipos que existen en el modulo





Accediendo a los metodos

El metodo VerLosMetodosDeLosTipos nos permite ver una tecnica para acceder a los metodos de una clase pública.-

Primero analizamos los atributos IsClass e IsPublic ya mencionados antes de llamar al metodo GetMethods de Type.-


if( esteTipo.IsClass && esteTipo.IsPublic)


Podemos tambien averiguar si el metodo es público o es un detalle de implementacion que no debe exponerse al mundo:


mi[k].IsPublic




Esta es la salida por pantalla de la parte correspondiente a la información sobre los metodos que tiene cada clase publica dentro del modulo



Pero para la invocación de un método precisamos saber algo más del mismo.-

Por ejemplo cuales son sus parámetros y quizás cuales son los tipos de dichos parámetros.-