Como evaluar expresiones matemáticas en VB.Net
Fecha: 16/Jun/2005 (13-06-05)
|
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 SubEl 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 ClassPor 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