Inicio > .NET Framework, Apunte, C# > Reflexión: diagnóstico y creación de tipos en tiempo de ejecución

Reflexión: diagnóstico y creación de tipos en tiempo de ejecución


Recuerdo que cuando programaba con C++, en una ocasión tuve que crear un mecanismo para admitir plug-ins y add-ons a un programa. Mientras lo hacía, lo primero que se me ocurrió fue que me gustaría poder obtener todas las clases existentes en una DLL y poder instanciarla a partir de una cadena de texto. De esta forma, mis motor de plug-ins se limitaría a abrir una DLL y leer un archivo de configuración y ya. Pero por supuesto, eso no puede hacerse con C++.

Sin embargo, es una característica que sí está presente en .NET. De hecho es parte vital de la misma, no nada más crear tipos en tiempo de ejecución, sino el poder analizar un objeto determinado: conocer sus métodos, sus propiedades, sus eventos, e incluso invocarlos. A esta capacidad de introspección, en .NET, se le conoce con el nombre de Reflexión (Reflection).

Arquitectura de un ensamblado

Antes que nada debemos conocer cómo se compone un ensamblado. El ensamblado es la unidad más alta que existe para agrupar componentes y clases. Un ensamblado puede ser un archivo ejecutable o una DLL. Los ensamblados contienen módulos (que en C# existe uno sólo y está oculto, pues el lenguaje no soporta esa característica; sin embargo VB.NET sí que la soporta), y espacios de nombres, los cuales contienen clases. Las clases tienen métodos, propiedades, constructores, atributos, eventos, atributos y otros tipos de datos anidados (como otras clases o delegados). Finalmente, los métodos y constructores tienen parámetros.

Pues bien, para cada uno de estos casos existen clases que las describen y se encuentran en el espacio de nombres System.Reflection (salvo Type, que se encuentra en el espacio de nombres System).

image

 

1.- Assembly. Representa un ensamblado. Contiene módulos, espacios de nombres y los tipos de datos.

2.- Module. Representa un módulo. En el caso de C#, sólo existe un módulo que contiene todas las clases.

3.- ConstructorInfo. Representa información de un constructor, con sus parámetros y los modificadores de acceso.

4.- MethodInfo. Representa un método, y tiene información sobre su firma, modificadores de acceso, tipo de retorno, parámetros y modificadores de herencia como si es abstracto (abstract), si es virtual (virtual) o si es oculto (new).

5.- FieldInfo. Representa un atributo en una clase, es decir, una variable declarada en la clase. Informa los modificadores de acceso y si el atributo es estático o no.

6.- EventInfo. Detecta información sobre eventos, como su nombre, tipo de delegado, atributos, etc.

7.- PropertyInfo. Representa una propiedad, y tiene información sobre su tipo, sus modificadores de acceso, sus modificadores de herencia (abstract, virtual, new, etc.) y sobre todo, información sobre sus accesores (getter y setter).

8.- ParameterInfo. Detecta información sobr atributos: su tipo, si el parámetro es de entrada o de salida, si se pasa por valor o referencia, y la posición ordinal dentro de la firma de un método.

Todo comienza con un tipo

Después del vistazo que echamos anteriormente, necesitamos saber cómo obtener objetos de cada una de esas clases. Hay dos formas de hacerlo: mediante la información del ensamblado (clase Assembly), o bien mediante un tipo de dato (clase Type). La forma más fácil es esta última.

Este método que veremos a continuación nos sirve para obtener la información de reflexión de un tipo determinado que conozcamos. Lo primero que necesitamos es una clase:

using System;
using System.ComponentModel;
… 

namespace Calendars
{
  public class Meeting : INotifyPropertyChanged
  {
    private string _title;
    private string _description;
    private DateTime _start;
	private DateTime _end;
	
	public Meeting() { … }
	public Meeting(string title) { … }

    public DateTime Start { … }
    public DateTime End { … }
    public string Title { … }
    public string Description { … }

    public event PropertyChangedEventHandler PropertyChanged;

    protected OnPropertyChanged(PropertyChangedEventArgs args) { … }

    public TimeSpan CalcDuration() { … }
    public TimeSpan CalcRemainder() { … }
  }
}

Creo que esta clase servirá para nuestros ejemplos. Ahora sí, lo que queremos es obtener la descripción reflejada de esta clase. Lo primero que necesitamos es obtener el objeto Type para nuestra clase Meeting. ¿Cómo le hacemos?

Recordemos que en .NET, todas las clases tienen un antecesor común: System.Object. Es decir, todas implementan Object como clase base. Object cuenta con varios métodos: dos métodos estáticos, Equals y ReferenceEquals; dos métodos protegidos, Finalize y MemberwiseClone; y cuatro métodos públicos, Equals, GetType, GetHashCode y ToString. Y como seguramente ya adivinaste, el método que nos interesa es GetType: en efecto, este método nos devuelve un objeto de tipo Type para una instancia en particular. Así, si tenemos una insancia cualquiera, basta invocar GetType, pues todos los objetos lo implementan.

Type t;

int i = 5;
t = i.GetType();
Console.WriteLine(t); // imprime 'System.Int32'

string s = "hola mundo";
t = s.GetType();
Console.WriteLine(t); // imprime 'System.String'

Meeting m = new Meeting();
t = m.GetType();
Console.WriteLine(t); // imprime 'Calendars.Meeting'

Ahora bien, GetType sirve para cuando tenemos una instancia. ¿Y si no tenemos una instancia? Podemos utilizar el operador typeof de C#. Este operador recibe el nombre de un tipo de dato y regresa el objeto Type correspondiente.

Type t;

t = typeof(int);
Console.WriteLine(t); // imprime 'System.Int32'

t = typeof(String);
Console.WriteLine(t); // imprime 'System.String'

t = typeof(Meeting);
Console.WriteLine(t); // imprime 'Calendars.Meeting'

Ya que tenemos el objeto Type la vida es sencilla. Pero no es la única forma de obtener un Type…

Explorando los ensamblados

Los dos métodos anteriores funcionan cuando ya sabemos el tipo de dato. Pero ¿qué pasa si no sabemos de antemano el tipo? Digamos que quiero ver todas las clases existentes en una DLL determinada. Digamos, en la propia DLL. ¿Qué hacer?

En este caso necesitamos obtener un objeto Assembly. ¿Por qué? Porque la clase Assembly tiene, entre otras maravillas, un método llamado GetTypes, el cuál nos regresa un arreglo de objetos Type con todos los tipos existentes, declarados dentro de ese ensamblado. Y eso es lo que queremos, ¿no? Al final nuestra meta es llegar a obtener un objeto Type.

Bien, pues comencemos a explorar los métodos. En primer lugar, Assembly define un método estático llamado GetAssembly, el cual devuelve un objeto Assembly que referencia el ensamblado actual. Pero también hay otras opciones: GetExecutingAssembly devuelve la referencia al ensamblado del ejecutable actual (si es invocado desde una clase que pertenece a un exe, entonces GetAssembly y GetExecutingAssembly devolverán la misma referencia; si no, GetExecutingAssembly devuelve la referencia al archivo ejecutable que a su ves referencia al ensamblado actual); GetEntryAssembly devuelve la referencia al ensamblado donde comenzó la ejecución del proceso actual (salvo en contadas ocasiones, el ensamblado donde se encuentra el método estático Main); y finalmente GetCallingAssembly, que devuelve la referencia al ensamblado del método que invocó el método actual. Bueno, un relajito.

using System;
using System.Reflection;
…

Assembly assembly = Assembly.GetAssembly();
Type[] types = assembly.GetTypes();
foreach (Type type in types)
    Console.WriteLine(t);

El código anterior obtiene el ensamblado actual e itera los tipos que existen en éste.

Información de tipos de datos

Bueno, ya vimos cómo obtener nuestro objeto Type, ahora veamos qué podemos hacer con él. De entrada, el objeto Type nos permite describir un tipo de dato mediante varias propiedades, destacando las siguientes.

  • Assembly. Obtiene el objeto Assembly al que pertenece el tipo de dato.
  • AssemblyQualifiedName. Obtiene el nombre calificado del ensamblado. Por ejemplo, "MiEnsamblado, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxx".
  • BaseType. Representa un objeto Type de la clase padre, o null si la clase padre es Object.
    Type type = typeof(ArgumentNullException);
    while (type != null)
    {
        Console.WriteLine("-> {0}", type);
        type = type.BaseType;
    }
    Console.WriteLine(" + {0}", typeof(object));
    	
    /* El código anterior imprime: 
    
    -> System.ArgumentNullException
    -> System.ArgumentException 
    -> System.SystemException
    -> System.Exception
     + System.Object
    */
  • FullName. Representa el nombre calificado del tipo (con su espacio de nombres). Por ejemplo: System.Reflection.MemberInfo.
  • GenericTypeArguments. Regresa un arreglo de objetos Type que representa el tipo de los parámetros genéricos.
  • IsAbstract. Pues eso, si el tipo es abstracto o no.
  • IsArray. Verdadero si el tipo es en realidad un arreglo.
  • IsClass. Verdadero si el objeto es una clase, falso si es otro elemento como enumeraciones, estructuras, interfaces, etc.
  • IsEnum. Verdadero si el objeto es una enumeración.
  • IsInterface. Verdadero si el objeto es una interfaz.

Después de obtener información sobre un tipo de dato, nos va a interesar obtener información sobre sus miembros. El método GetMembers regresa un arreglo (de tipo MemberInfo) con todos los miembros de la clase. Estos pueden ser propiedades, constructores, métodos, eventos y todos los objetos que describimos en la primera sección. GetMembers tiene una sobrecarga que permite indicar el tipo de elementos a enumerar: instancias, estáticos, públicos, no-públicos, etc.

Type type = typeof(Meeting);
MemberInfo[] mems = type.GetMembers(BindingFlags.Public | BindingFlags.Instance);
foreach (MemberInfo mem in mems)
{
    Console.WriteLine("{0} - {1}", 
        mem.MemberType,
        mem.Name);
}

/* imprime: 
   Constructor - ctor
   Constructor - ctor'1
   Property - Start
   Property - End
   Property - Title
   Property - Description
   Event - PropertyChanged
   Method - OnPropertyChanged
   Method - CalcDuration
   Method - CalcRemainder
   Method - Equals
   Method - GetHashCode
   Method - GetType
   Method - ToString
*/

En el ejemplo anterior no sólo vimos cómo enumerarlos, sino que también vimos que MemberInfo tiene una propiedad, llamada MemberType, de tipo MemberTypes, que permite saber si el miembro es un constructor, propiedad, evento, atributo, etc. Con base en el valor de esta propiedad, es posible hacer una conversión de MemberInfo al tipo propio de cada miembro: ConstructorInfo, MethodInfo, PropertyInfo, FieldInfo, EventInfo, etc. Todas estas clases heredan directamente de MemberInfo, por lo que la conversión es aceptable.

Ahora, en lugar de invocar a Type.GetMembers, puedes también invocar métodos directos:

  • GetConstructor te permite obtener un objeto ConstructorInfo: el constructor cuya firma coincida con el arreglo que se le pasa como parámetro; Get Constructors te permite obtener todos los constructores (i.e. un arreglo de ConstructorInfo).
  • GetEvent permite obtener un objeto EventInfo pasándole como parámetro el nombre del mismo; GetEvents regresa un arreglo de EventInfo con todos los eventos del tipo.
  • GetField regresa el FieldInfo, esto es, la información de un atributo, dependiendo del nombre que se pase como parámetro; o bien GetFields regresa el arreglo con todos los atributos del tipo.
  • GetMethod regresa el descriptor de un método, MethodInfo, a partir del nombre del método y del tipo de parámetros que se pasen; o bien GetMethods regresa todos los métodos del tipo.
  • GetProperty regresa un PropertyInfo con la información de la propiedad cuyo nombre se pasa como parámetro, o bien GetProperties regresa todas las propiedades.

Y así sucesivamente. Existen algunos otros miembros de Type que nos ayudan a conocer más sobre la clase. Por ejemplo, GetInterfaces te da una lista con información sobre las interfaces que el tipo implementa, o GetGenericArguments, que devuelve el tipo de datos de los parámetros para tipos genéricos.

Instanciando e invocando

Ahora viene la parte interesante. A partir de objetos Type, de objetos MemberInfo y derivados, etc., podemos crear tipos e invocar métodos, propiedades y otros objetos. Veamos cómo se hace.

Type type = typeof(Meeting);
ConstructorInfo ctor = type.GetConstructor(new Type[] { });
Object meeting = ctor.Invoke(new Object[] { });

Como puedes ver, instanciamos un tipo Meeting al invocar a su constructor. Para ello obtenemos el constructor que no tiene parámetros (el array Type está vacío), y luego usamos el método Invoke para invocarlo, también sin ningún parámetro (el array Object está vacío). Veamos cómo invocar el constructor que tiene un parámetro.

Type type = typeof(Meeting);
Type[] paramTypes = new Type[] { typeof(string) };
ConstructorInfo ctor = type.GetConstructor(paramTypes);
Object paramObjs = new Object[] { "Junta de revisión" };

Object meeting = ctor.Invoke(paramObjs);

En ambos casos, la variable meeting cuenta ya con una instancia válida de la clase Meeting. Ahora lo que vamos a hacer es invocar alguna de sus propiedades, para establecer los valores de Start y End. Asumamos que nuestra variable obj ya tiene una instancia válida de Meeting, según vimos en el ejemplo anterior.

Type type = typeof(Meeting);
Type[] paramTypes = new Type[] { typeof(string) };
ConstructorInfo ctor = type.GetConstructor(paramTypes);
Object paramObjs = new Object[] { "Junta de revisión" };
Object meeting = ctor.Invoke(paramObjs);

PropertyInfo propStart = type.GetProperty("Start", BindingFlags.Instance);
MethodInfo propStartSet = propStart.GetSetMethod();
propStartSet.Invoke(meeting, new DateTime(2012, 11, 20));

PropertyInfo propEnd = type.GetProperty("End", BindingFlags.Instance);
propEnd.SetValue(meeting, new DateTime(2012, 11, 21);

¡Jolines! Lo interesante comienza en el segundo párrafo, pues el anterior ya lo conocemos.

En efecto, vemos que en éste, obtenemos el PropertyInfo relacionado con la propiedad Start. Sabemos, sin embargo, que cada propiedad tiene dos métodos: un getter y un setter. De ahí que mandemos llamar GetSetMethod, para obtener el MethodInfo del setter (para obtener el MethodInfo del getter, usamos GetGetMethod). Por último, llamamos al método Invoke del MethodInfo para invocar el setter. El primer parámetro es el objeto sobre el cuál ejecutaremos el método (en este caso, meeting), y el segundo objeto es un array con los parámetros. Los setters sólo tienen un parámetro, que es lo que pasamos en el array.

Luego pasamos a modificar la propiedad End. Hacemos algo parecido, pero en lugar de obtener el MethodInfo del setter, llamamos al método SetValue, que hace todo por nosotros. Quise mostrar ambas formas porque el SetValue está disponible sólo para .NET 4.5 en adelante. Si estás con .NET 4.0 o anterior, tendrás que hacerlo de la primera forma.

Tras lo que hemos visto, invocar a los métodos es easy peasy: obtenemos el MethodInfo y luego llamamos al método Invoke.

MethodInfo method = type.GetMethod("CalcDuration", BindingFlags.Instance);
DateTime duration = (DateTime)method.Invoke(meeting, new Object[] { });

Type[] signature = new Type[] { typeof(DateTime) };
method = type.GetMethod("CalcRemainder", signature);
DateTime remainder = (DateTime)method.Invoke(meeting, 
                         new Object[] { DateTime.Now });

El primer ejemplo es muy directo: obtenemos el MethodInfo y luego llamamos a Invoke. El segundo párrafo tiene una particularidad. Cuando queremos obtener un método que tiene parámetros, tenemos que pasar el tipo de parámetros como un array de tipos. Por ejemplo, si queremos obtener el MethodInfo del siguiente método:

void foo(int a, string b, Meeting c, DateTime d);

Tendríamos que invocar al GetMethod de estas forma:

Type[] signature = new Type[] {
    typeof(int),
    typeof(string),
    typeof(Meeting),
    typeof(DateTime)
};

method = type.GetMethod("foo", signature);

El array de Type debe tener los tipos de cada uno de los parámetros. De esta forma puede identificarse al método correcto cuando éste se encuentra sobrecargado.

Otro ejemplo interesante consiste en obtener los atributos (campos) mediante GetField, que nos regresa un FieldInfo. Nota que puedes acceder a atributos privados y protegidos, siempre que tengas permiso. Si no lo tienes, te verás con un SecurityException entre manos.

FieldInfo field = type.GetField("GenericTitle", 
     BindingFlags.Public | BindingFlags.Static);
String value = (String)field.GetValue(meeting); 
     // meeting es ignorado porque es un atributo estático
if (!field.IsInitOnly)
    field.SetValue(meeting, "Hola mundo!"); 
     // meeting es ignorado porque es un atributo estático

Un poquito más interesante. GetValue y SetValue obtienen y establecen el valor del atributo. La propiedad IsInitOnly nos indica si el atributo es de sólo lectura o si puede ser cambiado posteriormente.

Sobre los eventos: podemos subscribir manejadores o eliminarlos mediante EventInfo. Lo que no podemos hacer es invocar al evento, eso sólo lo hace el propio objeto.

EventInfo evnt = type.GetEvent("PropertyChanged");
Delegate del = (s, a) => { Console.WriteLine("Propiedad cambiada!"); };
evnt.AddEventHandler(meeting, del);

Aquí el meollo es obtener el EventInfo y luego llamar a AddEventHandler.

 

Seguridad

Hasta ahora hemos evitado hablar sobre la seguridad asociada a Reflection. Sin duda debe haber algo, pues ¿cómo cualquiera va a poder ejecutar el MethodInfo.Invoke de algún método público o privado? ¡Eso rompería el encapsulamiento!

En primera instancia, cabe decir que el uso de Reflection para obtener información sobre tipos y miembros NO ESTÁ RESTRINGIDA. Todo código puede usar Reflection para enumerar tipos y sus miembros, y enumerar ensamblados y módulos.

Sin embargo, acceder a los miembros: establecer valores, leerlos, invocar métodos, subscribir eventos, etc., es otra historia. En general, sólo código que esté marcado como confiable puede usar Reflection para acceder a miembros con seguridad crítica.

Por último, para dar permisos especiales de un ensamblado a otros que quieran inspeccionarlo vía Reflection, deberán darle un ReflectionPermission con la bandera ReflectionPermissionFlagMemberAccess

¿Qué sigue?

En esta entrada hemos visto cómo utilizar la reflexión para inspeccionar ensamblados y buscar clases y tipos de datos. Hemos visto cómo obtener descriptores de métodos, propiedades, eventos, constructores, etc. Hemos visto cómo instanciar un tipo y cómo invocar métodos y establecer valores en atributos y propiedades.

No hemos visto, sin embargo, un concepto importante, que es el de los atributos de meta-información. Ahm… en .NET, a los atributos se les conoce también como "campos" (fields), pues existe aparte el concepto de atributos, que derivan de la clase System.Attribute. Estos atributos aportan información adicional sobre clases, métodos, propiedades, etc., y pueden accederse vía Reflection. Este tema es tan abundante que será motivo de una entrada propia.

Aparte de lo anterior, otro paso que sigue es el de generación de código. En efecto, ¡podemos generar código ejecutable a partir de Reflection! También podemos apoyarnos en CodeDom para crear nuestros propios lenguajes y compiladores. CodeDom también será sujeto de su propia entrada.

C.U.L8ER.

Categorías:.NET Framework, Apunte, C# Etiquetas: , ,
  1. Erik
    febrero 28, 2014 a las 10:21 am

    buenos días quería pedirte ayuda con interop. lo que pasa es lo siguiente.

    Me pidieron que en una tabla pueda agregar en un campo el nombre y el tipo de un atributo. ej.

    NombreCliente – string
    EdadCliente – int
    DocumentoIndentidad -string

    y en un formulario en tiempo de ejecución se me pueda crear esos registros de la tabla como atributos en el formulario o en una clase pero que sea en tiempo de ejecución.
    Esto con la finalidad de que la clase clientes sea dinamica y si le agrego registros en la tabla estos se puedan crear como atributos en mi formulario.

    saludos

    a la espera de tu respuesta

  2. Jhon
    enero 28, 2015 a las 5:31 pm

    excelente explicación amigo, muy interesante estos temas.

  1. noviembre 27, 2012 a las 7:16 pm

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s