Firmas invitadas
 

Aplicando Delegados con .NET

Publicado el 30/Oct/2006
Actualizado el 30/Oct/2006
Autor: Harvey Triana

 

La aplicación de delegados simplifica problemas generales y produce soluciones más eficaces.


 

Dedicatoria

 

Hay dos cosas inmutables: Las cadenas de texto en .NET y el empeño de “el Guille” en servir al mundo de la programación. Por está fecha celebramos el décimo aniversario de “el Guille” y es un honor para a mi contribuir a su portentoso sitio Web con un articulo.  Mis más cordiales felicitaciones Guille.

 

Introducción

 

Los delgados es una característica de los lenguajes de programación ciertamente avanzada que usan los programadores que solucionan problemas de propósito general. Los programadores de producción tienen una visión mas bien lejana del poder de los delegados ya que por lo general se ocupan de problemas específicos. Los programadores de C++ conocen bien la técnica, la han usado desde hace años. Por ejemplo el sistema operativo Windows basa fuertemente su código en punteros a funciones, es decir en delegados. Los lenguajes actuales de .NET disponen esta característica de manera avanzada y segura. Así pues, los delegados se pueden usar en problemas de aplicaciones específicas.

 

 

Definición de Delegados

 

En lenguajes .NET  un delegado es un tipo que hace referencia a un método. Se define y trata como un tipo de variables especial. En .NET los delegados poseen seguridad de tipos. Esto es particularmente elegante porque pone a alto nivel su uso. Con los delegados de pueden definir métodos de devolución de llamada. Por ejemplo Ajax .NET o ‘Atlas’ basan virtuosamente su código en delegados.  En fin, en la MSDN puede encontrar la guía pertinente para usar delegados, use esta dirección: http://msdn2.microsoft.com/es-es/library/ms173171.aspx

 

 

Un ejemplo Concreto

 

Las aplicaciones que solucionan problemas de matemática aplicada son intemporales. He retomado un articulo que escribir a principios de mil novecientos algo, acerca de la solución de integración numérica, escrito para VB6, lo reescribí en .NET, y lo enfoque a usar delegados. Se percibe su robusta transformación. Una de mis ideas finales debe ser poder una función integrador con menos líneas de código y general. El corolario sería así::

 

“Déme la integral de ésta función entre éste intervalo, con frecuencia tal, usando el método deseado…”

 

Quizás el código final luzca así::

 

C#: Ejemplo de código final usando delegados

float r = Integrator.Intergrator(1F, 2F, 10, FunctionsSample.Reciprocal,

          Integrator.TrapeziumsRule);

 

Lo cual en palabras “explicitas” podría decir:

 

Déme la integral de la función ‘Reciproco’, entre 1.0 y 2.0, con una frecuencia de 10 intervalos, usando la Regla de los Trapecios.

 

Aquí Integrator es una función capaz de resolver una integración numérica de una función con nombre, en este caso Reciprocal (reciproco es la función matemática que evalúa 1 / x), pasada como parámetro,  a través de un método con nombre, TrapeziumsRule  (la regla de los trapecios es una técnica numérica para solucionar integrales), también pasado como parámetro. Esta línea de código, que se lee con intuición, tiene un código ciertamente avanzado, que se va a explicar en este articulo.

 

De la misma forma, se podría aplicar el modelo del ejemplo a muchos casos de problemas de matemática aplicada de manera más general.

 

Existe una característica aun más avanzada con .NET y son los Métodos Anónimos. Lo cual también voy a explicar con ejemplo al final de este articulo.

 

 

Integración Numérica

 

No es el propósito de este articulo describir la integración numérica. En las referencias del articulo dispongo algunos enlaces pertinentes para el asunto. El enfoque del articulo esta destinado a un ejemplo concreto del uso de delegados. Digamos de modo general que una ‘integral’ es una función real que calcula el área bajo una curva. Con frecuencia la integral de una función no es simple. Entonces se recurre a técnicas numéricas de aproximación que resultan aceptables en términos de ingeniería. También aplica con frecuencia el calculo de una integral numérica cuando solo se conoce una matriz de datos de la función, y no se conoce su ecuación.

 

El ejemplo que dispongo en este articulo es un paradigma en donde suponemos que la integral de la función reciproco, f(x) = 1 / x, no es simple y debemos recurrir a técnicas numéricas para su solución. La función integral de f(x) =  1 / x, es conocida como el logaritmo natural de x. Las integrales se evalúan en un intervalo finito, normalmente expresado como integral entre a y b, donde b es mayor que a. Supongamos que a = 1 y b = 2. El ejemplo se concreta así:

 

Ejemplo propuesto

 

Hay varios métodos para determinar el valor de una integral por métodos numéricos. Los más conocidos son la Regla de los Trapecios, y la Regla de Simpson. Encuentra buena información al respecto en las referencias de este articulo. En términos matemáticos los dos métodos se expresan así:

 

Regla de los trapecios:

 

 

Regla de Simpson:

 

 

 

El termino E se refiere al error que implica la técnica numérica. El error tiene una connotación teórica y en la práctica no se aplica. La Regla de Simpson es más precisa que la Regla de los Trapecios, pero se limita a la condición de que n sea impar (el método se basa en aproximación a segmentos de parábola cada 3 punto).

 

 

El código de los dos métodos de solución numérica es como sigue. Creo una aplicación de consola con nombre IntegratorSample. Agrego un clase con nombre Integrator, y dispongo el siguiente código:

 

C#: La clase Integrator

using System;

 

namespace IntegratorSample

{

    class Integrator

    {

        public const float NULLFLOAT = -999.25F;

 

        public static float TrapeziumsRule(float a, float b, float[] f)

        {

            float s = 0F;

            int t = f.Length - 1; // trapeziums count

 

            for (int i = 1; i < t; i++)

            {

                s += f[i];

            }

            return 0.5F * (b - a) * (f[0] + 2F * s + f[t]) / t;

        }

 

        public static float SimpsonRule(float a, float b, float[] f)

        {

            float s1 = 0F, s2 = 0F;

            bool p = false;

            int t = f.Length - 1;

 

            if ((t % 2) == 0)

            {

                for (int i = 1; i < t; i++)

                {

                    if (p) s2 += f[i];

                    else s1 += f[i];

                    p = !p;

                }

                return (b - a) * (f[0] + 4F * s1 + 2F * s2 + f[t]) / 3F / t;

            }

            else return NULLFLOAT; // doesn't apply for even numbers

        }

    }

}

 

Ahora, una utilización clásica de esta clase es como se muestra a continuación. Utilizo la clase Program de la aplicación de consola:

 

Solución estándar usando Integrator

using System;

 

namespace IntegratorSample

{

    class Program

    {

        static void Main(string[] args)

        {

            Sample1();

        }

 

        static void Sample1()

        {

            int n = 11;

            float[] f = new float[n];

            float a = 1F, b = 2F, x = 0F, r;

 

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

            {

                x = a + i * (b - a) / (n - 1F);

                f[i] = 1F / x;

                Console.WriteLine(string.Format("({0}) x = {1} y = {2}",

                                  i.ToString(), x.ToString(), f[i].ToString()));

            }

            r = Integrator.TrapeziumsRule(a, b, f);

            // usando el método SimpsonRule

            r = Integrator. SimpsonRule (a, b, f);

            Console.WriteLine(string.Format("Integral = {0}", r));

        }

    }

}

 

Al ejecutar este programa da la siguiente salida:

 

Salida de IntegratorSample

(0) x = 1 y = 1

(1) x = 1.1 y = 0.9090909

(2) x = 1.2 y = 0.8333333

(3) x = 1.3 y = 0.7692308

(4) x = 1.4 y = 0.7142857

(5) x = 1.5 y = 0.6666667

(6) x = 1.6 y = 0.625

(7) x = 1.7 y = 0.5882353

(8) x = 1.8 y = 0.5555556

(9) x = 1.9 y = 0.5263158

(10) x = 2 y = 0.5

Integral = 0.6937714

 

El valor “exacto” de la integral es ln(2), lo que equivale a: 0.693147180559945... La precisión de la técnica numérica mejora al aumentar el número de trapecios.

 

Aplicando Delegados

 

Si estudia la exposición anterior, se puede concluir que se trata de una solución a un problema especifico. Puntualmente se evalúa la integral de la función f(x) =  1/ x por la Regla de los Trapecios. Si quisiéramos resolver otra ecuación por otro método, se podría escribir un bloque de código similar. En programación clásica de producción, podrías escribir un par de enumeraciones, una para los métodos de evaluación, y otra para las ecuaciones que queremos evaluar, e incrustar directivas de selección de caso dentro del código. Si bien los métodos de evaluación tiene un numero discreto, a lo sumo 5 para solución de integrales por técnicas numéricas, el número de ecuaciones es virtualmente infinito. Si deseamos programar una solución general, debemos recurrir a delegados. En la referencia (1) encuentra la documentación MSDN acerca de los delegados.

 

Para organizar el asunto, diré que hay tres elementos concretos que conforman un delegado.

 

(1)   La declaración del delegado.

(2)   Las funciones o métodos que hacen juego con los tipos de variables declaradas en el delegado

(3)   Un función con un parámetro del tipo del delegado declarado

 

La programación generalizada del problema que expongo se puede tratar en dos partes. Una la solución de delegar el método de cálculo  como parámetro, y segundo, delegar la ecuación que se quiere integrar.

 

Para la primera parte defino el delegado IntergratorRule para invocar el método deseado:

 

El delegado IntegratorRule

public delegate float IntegratorRule(float a, float b, float[] f);

 

Las funciones SimpsonRule y TrapeziumsRule hacen juego en cuanto a parámetros y tipos devueltos con del delegado IntegratorRule.

 

Luego se crea la función Integrator, la cual incorpora un parámetro del tipo IntegratorRule:

 

Funcion Integral

public static float Integral(float a, float b, float[] f, IntegratorRule IR)

{

    return IR(a, b, f);

}

 

Nota. Existe la palabra reservada “Invoke” para usar un delegado. Tal vez se ve más en VB.NET que en C#, la sintaxis es opcional, podría haber escrito: “return IR.Invoke(a, b, f);”

 

De esta manera, disponiendo de la matriz de datos de una función especifica, puedo usar la función Integral pasando como parámetro uno de los métodos de solución (TrapeziumsRule o SimpsonRule) definidos en la clase Integrator, es decir, podría usar la siguiente línea en el ejemplo inicial:  

 

 

Usando la funcion Integrator pasando como parámetro el metodo de calculo

r = Integrator.Integral(a, b, f, Integrator.TrapeziumsRule);

// tal vez prefiera:      

r = Integrator.Integral(a, b, f, Integrator.SimpsonRule);

 

Esto es notablemente más eficiente que definir una función Integrador con un parámetro enumeración con los métodos de cálculo, y pasar el valor de la enumeración como parámetro, el cual se evalúa en una selección de caso dentro de Integrador. Usando delegados evitamos esto y se perciben notoriamente las ventajas de su uso.

 

Ahora, como segunda parte de la solución, voy a escribir el mecanismo por el cual voy a poder pasar la función que quiero integrar como parámetro. Creo el siguiente delegado en la clase Integrador:

 

El delegado MathFunction

public delegate float MathFunction(float x);

 

public static float DoFunction(float x, MathFunction f) {return f(x);}

 

Esto me permitirá llamar por nombre cualquier función de una variable que se defina en alguna clase.

 

Por ejemplo, construyo una clase de nombre FunctionsSample, a la cual puedo agregar muchas funciones especificas. En el ejemplo incluyo dos, Reciprocal y AnyFunction.

 

 

La clase FunctionsSample

using System;

 

namespace IntegratorSample

{

  class FunctionsSample

  {

    public static float Reciprocal(float x) {return 1 / x; }

    public static float AnyFunction(float x) {return 0.2F * x * x - x + 0.5F;}

    // más funciones aqui . . .

  }

}

 

 

Si, por ejemplo, quiero el reciproco de un numero, digamos 1.234, llamaré la función DoFunction de la siguiente manera

 

 

Usando DoFunction

 

r = Integrator.DoFunction(1.234F, FunctionsSample. Reciprocal);

 

 

Para finalizar la solución, voy a sobrecargar la función Integrador, para que se pueda llamar tanto el método de cálculo como la función que se quiere integrar. A esta función sobrecarga le implemento la capacidad de que sea la misma función la que calcule los intervalos para solucionar la integral. Agrego el parámetro Divisions con la cual el cliente queda en libertad de establecer la precisión de la integral devuelta. Entre mayor es Divisions, mayor es la precisión del valor devuelto, y mayor el consumo de recursos. En general el valor de Divisions queda dependiendo de las necesidades del cliente. La segunda función Integral queda de la siguiente manera:

 

Funcion Integrator Genralizada

public static float Integral(

    float a, float b, int Divisions, MathFunction f, IntegratorRule IR)

{

    int n = Divisions;

    float t = 0, d = 0;

    //alguna validación

    if (n < 2) n = 2;

    if ((n % 2) == 0) n++;

    // ancho del trapecio

    d = (b - a) / (float)(n - 1);

 

    float[] v = new float[n];

    t = a;

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

    {

        v[i] = f(t);

        t += d;

    }

    return Integrator.Integral(a, b, v, IR);

}

 

Siguiendo el ejemplo, ya se puede escribir la solución generalizada en una línea:

 

Solución general de calculo de Integrales a funciones con nombre

static void Sample2()

{

    float r = Integrator.Integral(a, b, n,

                         FunctionsSample.Reciprocal,

                         Integrator.TrapeziumsRule);

    Console.WriteLine(string.Format("Integral = {0}", r));

}

 

Se puede leer que se ha llamado la función Integral pasando como parámetros el método de calculo y la función que se quiere calcular. El código es loable.

 

 

Métodos Anónimos

 

Con los métodos anónimos se rompe la necesidad de crear una función con nombre para usar un delegado. En versiones de C# anteriores a la versión 2.0, la única manera de usar un delegado es utilizar métodos con nombre. En C# 2.0 y posteriores los métodos anónimos permiten usar un delegado sin escribir una función con nombre. Me explico siguiendo el ejemplo. La función Reciproco se declara en la clase FunctionSample y se delega en MathFuncion. Usando un método anónimo, no tengo necesidad de esto, declaro la función dentro de una variable del tipo de delegación MathFunction. El siguiente ejemplo muestra como:

 

 

Usando métodos anónimos

static void Sample3()

{

    float r = 0;

 

    //función anónima

    Integrator.MathFunction f = delegate(float x) {return 1F / x;};

       

    r = Integrator.Integral(1F, 2F, 10, f, Integrator.TrapeziumsRule);

    Console.WriteLine(string.Format("Integral = {0}", r));

}

 

En el código anterior, declaré la variable f la cual contiene una función anónima, la cual devuelve el reciproco de un numero. Es claro que es supremamente deseable ya que el numero de funciones matemáticas es virtualmente infinito. Si por ejemplo se me presenta la ecuación f(x) = 0.2F * x * x -  x + 0.5F, con unas pocas lineas de código soluciono el problema inmediato:

 

 

Versatilidad de los métodos anónimos

static void Sample3()

{

    float r;

   

    Integrator.MathFunction f = delegate(float x)

    {

        return 0.2F * x * x -  x + 0.5F;

    };

 

    r = Integrator.Integral(1F, 2F, 10, f, Integrator.TrapeziumsRule);

    Console.WriteLine(string.Format("Integral = {0}", r));

}

 

Conclusiones

 

Los delegados son una característica de los lenguajes .NET que ponen de manifiesto una poderosa herramienta para los desarrolladores. Los delegado aplican loablemente a problemas de ingeniería, simplificando problemas

de manera general e intuitiva.

 

Las soluciones generadas de delegados son más eficaces porque pueden evitar selección de caso dentro de las reglas de negocios.

 

Notas

 

Programé la clase Integrador basada en el tipo float. si bien esto aplica cómodamente a problemas de ingeniería, la precisión del tipo float no es conveniente para problemas científicos. No obstante, no es complicado la transformación de float a double de las clases expuestas en el articulo.

 

El articulo se enfoco a C#, si desea una traducción a VB.NET, no tiene complicación, ni limitaciones. Aparte de cambiar algunas cosas de sintaxis de palabras reservadas, no hay que escribir código más allá del expuesto.

 

Ejemplo Completo

 

Puede descargar el ejemplo compreto de la dirección: http://vexpert.mvps.org/sample/IntegratorSample.zip

 

 

Referencias

 

1)      Delegados con C#: http://msdn2.microsoft.com/es-es/library/ms173171.aspx

2)      Integration: http://www.ping.be/~ping1339/int.htm

3)      Numerical integration: http://www.damtp.cam.ac.uk/lab/people/sd/lectures/nummeth98/integration.htm

4)      Calculus, Volume 1, One-Variable Calculus with an Introduction to Linear Algebra, Tom Apostol

 



Ir al índice principal de el Guille