Como evaluar expresiones matemáticas en VB.Net

 

Fecha: 16/Jun/2005 (13-06-05)
Autor: Jose G Alvarez - [email protected]

 


Generalmente denominamos funciones a las expresiones matemáticas o no, posibles de ser evaluadas. Sin embargo para simplificar este articulo, sencillamente las llamaremos expresiones para no confundirlas con el concepto de función de los lenguajes de programación.

En algunas ocasiones nos encontramos con la imposibilidad de evaluar directamente una expresión en VB.net. Y es que desde los inicios del Basic, nunca ha existido una función propia del lenguaje que evalúe una expresión.

Recuerdo con nostalgia cuando, con GW-Basic, aprendí un artilugio que consistía en que nuestro código creaba un archivo secuencial (con extensión .bas) que contenía el código de una función y dentro de esta función escribíamos la expresión que queríamos evaluar. Claro, esto era posible pues GW era un lenguaje interpretado y no existía un compilador que generara un ejecutable.

En VB el truco consistía en usar una referencia a un objeto “Microsoft Script Control” y usar su método Eval. Como en el ejemplo siguiente.

Dim oSC As New ScriptControl
Dim expMath As String
Dim expToEval As String

oSC.Language = "VBScript"

expMath = "X ^ 2 + 2 * X + 1"
expToEval = Replace(expMath, "X", 5)

MsgBox oSC.Eval(expToEval)

 

Esta técnica, aunque resulta muy útil y soluciona en algunos casos nuestros problemas, esta limitada a que solo podemos usar objetos y/o funciones definidas por VBScript dentro del parámetro Expression del método Eval.

Todo esto ha servido para que reflexione sobre cuan viejo soy, aunque estas no son las técnicas de reflexión que se usan en VS.net. De estas vamos a hablar mas adelante.

Si bien en VB.net podemos hacer referencia a un objeto “Microsoft Script Control”, tendríamos las mismas desventajas que con versiones anteriores de VB. Como podemos entonces, evaluar una expresión en VB.net, sin desaprovechar las bondades de .net?

Bien, si recordamos el artilugio que se usaba con GW-Basic, lo desempolvamos y lo implementamos bajo la plataforma .net, podremos, no solo evaluar una expresión, si no que podremos crear y ejecutar cualquier código de forma dinámica. Sin embargo por ahora nos conformaremos con crear una clase que evalúe una expresión matemática.

Crearemos entonces una clase Evaluador. Esta creará un assembly con el código que deseamos ejecutar, en este caso un método o función con la expresión a evaluar. Luego usando la reflexión invocara este método y obtendremos los resultados. Suena sencillo… realmente lo es…

La clase Evaluador contendrá un objeto privado (oEnsamblado) de tipo Assembly (del espacio de nombres System.Reflection) y dos métodos públicos: PrecompilarAssembly y Evaluar. He decidido separar ambos métodos por cuestiones de rendimiento. Si combináramos los dos métodos y fuésemos a dibujar la grafica de una expresión, el rendimiento final seria simplemente inaceptable. La compilación de un assembly requiere una cantidad de proceso considerable.

El objeto oEnsamblado será la representación del assembly que compilaremos. Este assembly, como mencionamos anteriormente, será una clase (EvalClase) que expondrá un único método (Eval) que contendrá la expresión a evaluar y retornará el resultado.

Imports System.Text
Imports System.CodeDom.Compiler
Imports System.Collections.Specialized

Public Class Evaluador
    Private oEnsamblado As System.Reflection.Assembly

    Public Sub PrecompilarAssembly ...


    End Sub


    Public Function Evaluar ...


    End Sub
 
 

El método PrecompilarAssembly como su nombre lo indica, precompila el assembly de la clase que contiene un método con la expresión a evaluar. Vamos a visualizar el código a generar y los posibles escenarios dependientes de la expresiones a evaluar…

Supongamos que deseamos evaluar la expresión matemática: y(x) = x + 3. El código necesario seria algo como el siguiente:

Public Class EvalClase
    Public Shared Function Eval(ByVal X As Double) as Object
        Return X + 3
    End Function 
End Class
 

Por otro lado supongamos que queremos evaluar: z(x,y) = Log(x) + y

Imports System.Math

Public Class EvalClase
    Public Shared Function Eval(ByVal X As Double, ByVal Y As Double) as Object
        Return Log10(X) + Y
    End Function 
End Class


Vemos que existen tres factores cambiantes, la expresión, los parámetros del método Eval y los namespaces. Por tanto nuestro método Precompilarfuncion recibirá, tres parámetros correspondientes a esto elementos.

Veamos entonces el método PrecompilarAssembly

Public Function PrecompilarAssembly(ByVal Funcion As String, _
ByVal ParametrosList As StringCollection, ByVal NameSpaceList As StringCollection) As Boolean

    Dim mStrings As String
    Dim mParametros As String
    'Definimos un objeto de tipo StringBuilder que contendra el código a compilar
    Dim CodigoFuente As New StringBuilder()

    'Agregamos los Imports necesarios a nuestro codigo fuente  
    For Each mStrings In NameSpaceList
        CodigoFuente.Append("Imports " & mStrings & vbCr)
    Next

    'Preparamos un string con los parametros que usará el metodo Eval 
    'de de la clase EvalClase 
    For Each mStrings In ParametrosList
        mParametros &= ", " & mStrings
    Next

    mParametros = Trim(mParametros)
    If mParametros.Length > 0 Then
        mParametros = Trim(Mid(mParametros, 2))
    End If

    'Terminamos de construir la clase a compilar
    CodigoFuente.Append("Public Class EvalClase" & vbCr)
    CodigoFuente.Append("   Public Shared Function Eval(" & _
        mParametros & ") as Object" & vbCr)
    CodigoFuente.Append("       Return " & Funcion & vbCr)
    CodigoFuente.Append("   End Function " & vbCr)
    CodigoFuente.Append("End Class " & vbCr)

    'Creamos una instancia de la clase VBCodeProvider 
    'que usaremos para obtener una referencia a una interfaz ICodeCompiler
 Dim oCProvider As New VBCodeProvider()
    Dim oCompiler As ICodeCompiler = oCProvider.CreateCompiler

    'Usamos la clase CompilerParameters para pasar parámetros al compilador
    'En particular, definimos que el assembly sea compilado en memoria.
    Dim oCParam As New CompilerParameters()
    oCParam.GenerateInMemory = True

    'Creamos un objeto CompilerResult que obtendrá los resultados de la compilación
    Dim oCResult As CompilerResults
    oCResult = oCompiler.CompileAssemblyFromSource(oCParam, CodigoFuente.ToString)

    'Comprobamos que no existan errores de compilación.
    Dim oCError As CompilerError
    If oCResult.Errors.Count > 0 Then
        'Si existen errores los mostramos.
        'Si bien, podriamos implementar un mejor método para visualizar 
        'los errores de compilación, este nos servirá por los momentos.
        For Each oCError In oCResult.Errors
            MsgBox(oCError.ErrorText.ToString)
        Next
        Return False
    Else
        'Como el ensamblado se generó en memoria, debemos obtener 
        'una referencia al ensamblado generado, para esto usamos 
        'la propiedad CompiledAssembly
        oEnsamblado = oCResult.CompiledAssembly
        Return True
    End If
End Sub



El método PrecompilarAssembly devolverá True o False dependiendo si la compilación tubo éxito o no. El assembly compilado en memoria, estará referenciado por el objeto oEnsamblado.

Ahora necesitamos invocar el método Eval de nuestro assembly recientemente generado. Para ello haremos uso de las técnicas de reflexión.

Public Function Evaluar(ByVal ParamArray Parametros() As Object) As Object
    If oEnsamblado Is Nothing Then
        Return Nothing
    Else
        'Instanciamos la clase EvalClase de nuestro assembly 
        'creando un tipo a partir de ella.
        Dim oClass As Type = oEnsamblado.GetType("EvalClase")

        'Usamos GetMethod para accesar al método Eval, e invocamos este con los parametros necesarios.
        Return oClass.GetMethod("Eval").Invoke(Nothing, Parametros)
    End If
End Function


Veamos ahora como usar nuestra clase Evaluador, En el ejemplo se evaluara la expresión z(x,y) = Log(x) + y. con x = 100; y=3. Nota: recuerde que en notación matemática Log(x) simboliza el logaritmo de x en base 10, Para ello debemos usar la función Log10.

' Creamos una nueva instancia de la clase Evaluador
Dim mEval As New Evaluador.Evaluador()

'Creamnos una variable tipo string y le asignamos la expresión que queremos evaluar
Dim mExpresion As String = "Log10(X) + Y"

'Creamos un objeto StringCollection y agregamos los parámetros de entrada que usará el método eval
Dim mParameters As New StringCollection()
mParameters.Add("ByVal X as Double")
mParameters.Add("ByVal Y as Double")

'En este caso, la función Log10() Pertenece al espacio de nombres System.Math. 
'se hace necesario entonces, crear un objeto StringCollection y agregar 
'el namespace System.Math.
Dim mNameSpaces As New StringCollection()
mNameSpaces.Add("System.Math")

'Invocamos el método PrecompilarFunción y verificamos si se genero correctamente el assembly.
If If mEval.PrecompilarFuncion(mExpresion, mParameters, mNameSpaces) Then
    'Si el assembly se generó correctamente, creamos un array con los valores de los parametros a evaluar
    Dim mParam() = {100, 3}
    'invocamos el método Evaluar y mostramos el resultado
    MsgBox(mEval.Evaluar(mParam))
Else
    MsgBox("No se ha generado el Assembly")
End If




En conclusión, hemos utilizado las técnicas de compilar mediante código para crear un assembly en memoria y luego mediante la reflexión invocar sus métodos. Se me ocurren en este momento, no menos de 10 mejoras que se pueden hacer a la clase Evaluador. Sin embargo la he construido con un propósito específico, crear una clase Graficador que implementará la clase Evaluador para graficar funciones matemáticas, y para este fin, está mas que bien.

 

Referencias


CÓMO: Compilar código mediante programación utilizando el compilador de Visual Basic .NET: http://support.microsoft.com/default.aspx?scid=kb;ES;304654

Generar código fuente y compilar un programa a partir de un gráfico CodeDOM
http://msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/cpguide/html/cpconGeneratingSourceCodeCompilingProgramFromCodeDOMGraph.asp

Reflection y sus aplicaciones
http://www.microsoft.com/spanish/msdn/comunidad/mtj.net/voices/art121.asp

 


ir al índice