Valutare una espressione algebrica passo dopo passo
a cura di Diego Cattaruzza (requisiti: conoscenza di base di .Net)Premessa
Alcuni giorni fa, un membro della nostra Community ha proposto una Tip (attraverso il link posto in area Tips), che era, più che un pezzo di codice per svolgere una particolare e ristretta funzione, una procedura piuttosto complessa per eseguire un compito particolare: la valutazione di una espressione attraverso una progressiva risoluzione.L'autore, che desidera restare anonimo e quindi lo chiamerò solo Anonimo, si proponeva di fornire al nipote un programma che illustrasse la risoluzione di una espressione, passaggio dopo passaggio.
Non era una vera e propria Tip, quindi, però il codice era davvero interessante e ho chiesto ad Anonimo di farci sopra un articolo di spiegazione, o di permettere a me di farlo.
Gli ho chiesto anche di modificare il codice rendendolo più moderno, ma non ha avuto tempo di dedicarcisi e quindi l'ho fatto io.In questo articolo, mi propongo di illustrare il passaggio dal codice di Anonimo alla mia versione.
Analisi del codice e prime modifiche
La prima cosa che salta agli occhi, esaminando il codice, è la serie di etichette cui puntano vari GoTo.
E' brutta programmazione. Da evitare. Come? Vedremo nel seguito, perché per farlo bisogna comprendere bene la logica dell'algoritmo.
Un GoTo porta il controllo di flusso in un punto diverso del codice; se questo punto è successivo alla posizione del GoTo, il GoTo è di solito sostituibile da una If; se invece il punto è precedente la posizione del GoTo, il GoTo è si solito sostituibile da un ciclo. Inoltre, una procedura di questo tipo è tipicamente richiamabile da se stessa (cioè si presta alla cosiddetta chiamata ricorsiva)La seconda cosa evidente è l'abuso poco funzionale della manipolazione della stringa di log. E' una procedura pressocché inevitabile in VB6, ma VB.Net fornisce (proprio per questo scopo) l'oggetto StringBuilder. Naturalmente, Anonimo stava programmando in VB2005 da neanche un mese, e non era al corrente di questa possibilità.
Ho quindi aggiunto una variabile di questo tipo a livello di modulo (LogStringBuilder) ed ho sostituito la variabile pubblica GetValueLogResolveExp con una proprietà a sola lettura ExpressionResolvingLog che restituisce il contenuto dello StringBuilder.
Naturalmente, questo oggetto viene istanziato dal costruttore della classe. En passant, ho anche cambiato il nome della classe.Imports System.Text Public Class StringEvaluator Implements IDisposable Private LogStringBuilder As StringBuilder Public Sub New() LogStringBuilder = New StringBuilder End Sub Public ReadOnly Property ExpressionResolvingLog() As String Get Return LogStringBuilder.ToString End Get End PropertyHo quindi proceduto a sostituire tutte le concatenazioni a GetValueLogResolveExp con l'uso dei metodi Append o AppendLine (a seconda della necessità di andare a capo oppure no) dello StringBuilder. Qui presento solo il primo esempio:
LogStringBuilder.AppendLine("Risoluzione espressione: " & expressionToEval) LogStringBuilder.AppendLine( _ "________________________________________________________________") LogStringBuilder.AppendLine() LogStringBuilder.AppendLine()Ricominciando l'analisi del codice, mi sono imbattuto nella istruzione On Error Goto: l'ho sostituita con il costrutto Try-Catch implementando la cattura di un tipo specifico di Exception, ArgumentException, in base al significato del messaggio indicato.
Nei vari punti in cui ho cercato di prevenire gli errori, ho lanciato una ArgumentException, fornendo il messaggio più adatto.Throw New ArgumentException( _ "Immettere un'espressione! Impossibile eseguire il metodo!") Throw New ArgumentException("Le parentesi non sono correttamente inserite!") Throw New ArgumentException("Ci sono più di un segno =!") Throw New ArgumentException( _ "Risoluzione impossibile: numero negativo sotto radice " & n)Queste eccezioni, poiché vengono sollevate da codice interno a una classe da istanziare, devono essere rigirate al chiamante, quindi la loro gestione diventa:
Catch e As ArgumentException Throw New ArgumentException(e.Message) Catch e As DivideByZeroException Throw New DivideByZeroException("Errore di divisione per zero! Impossibile continuare") Catch e As OverflowException Throw New OverflowException("Errore di numero troppo grande! Impossibile continuare") Catch ex As Exception Throw New Exception(ex.Message, ex.InnerException) End TryA questo punto, ho analizzato il problema delle parentesi. In effetti, c'è una mancanza: se l'utente digita una parentesi tonda prima di una grafa (il che è lecito) si ha un errore. Secondo la logica imparata a scuola, invece, si dovrebbe risolvere (prima del resto) le parti di espressione racchiuse tra le parentesi più interne.
Inoltre, il compito del metodo (rinominato DidacticEval) è risolvere, in ultima analisi, ogni espressione priva di parentesi. Questa considerazione porta a decidere di usare il metodo ricorsivamente. Infatti, risolvere una espressione come questa (v è il carattere adottato per indicare la radice quadrata, poiché far scrivere sqrt è noioso):15+(13+7^7)-(23+2-v49)con
DidacticEval("15+(13+7^7)-(23+2-v49)")significa eseguire
DidacticEval("15+ DidacticEval(13+7^7) - DidacticEval(23+2-v49)")Ma occorre anche verificare che le parentesi siano correttamente inserite (appaiate e non accavallate).
Ho quindi implementato anche una verifica sulla validità delle parentesi, se ce ne sono.Private Function ParentesiValide(ByVal expressionToValidate As String) As Boolean Dim parsA As String = "{[(", parsC As String = "}])", p1 As Integer, p2 As Integer For i As Integer = 0 To 2 Do While expressionToValidate.Contains(parsA.Chars(i)) p1 = -1 : p2 = -1 p1 = expressionToValidate.IndexOf(parsA.Chars(i)) p2 = expressionToValidate.IndexOf(parsC.Chars(i), p1 + 1) If p1 > -1 AndAlso p2 > -1 AndAlso p2 > p1 Then expressionToValidate = expressionToValidate.Remove(p2, 1) expressionToValidate = expressionToValidate.Remove(p1, 1) ElseIf p1 <> p2 Then Return False End If Loop Next Return True End FunctionIn conseguenza di questo breve codice, posso saltare direttamente al codice STEP0, che nelle intenzioni deve essere l'inizio di un ciclo di ripetute sostituzioni di 'segni doppi', ma in realtà Replace sostituisce tutte le ricorrenze (forse si vuol prevenire la presenza di 'segni tripli', ma in questo caso sarebbe meglio segnalare la scorrettezza, piuttosto che correggerla).
Poi mi sono trovato ad analizzare il codice con il quale viene scandita la stringa dell'espressione per impostare tre vettori: uno per gli operandi, uno per gli operatori e uno per i valori.
Capito il procedimento, ho provveduto a implementare il codice secondo una logica che eviti il ricorso alle etichette e ai GoTo, mediante l'uso di cicli condizionati.
In tal modo ho potuto guidare la risoluzione dell'espressione secondo le priorità ben note ai tempi scolastici (prima le parentesi più interne, poi radici quadrate e potenze, poi moltiplicazioni e divisioni e infine addizioni e sottrazioni).La nuova classe StringEvaluator
La prima cosa da fare è verificare la validità dell'espressione da valutare, che non deve essere una stringa vuota o nulla, avere un numero pari di parentesi correttamente appaiate, avere (eventualmente) un solo operatore di uguaglianza.Public Function DidacticEval(ByVal expressionToEval As String) As Decimal Try 'prime verifiche If String.IsNullOrEmpty(expressionToEval) Then Throw New ArgumentException( _ "Immettere un'espressione! Impossibile eseguire il metodo!") End If If Not ParentesiValide(expressionToEval) Then Throw New ArgumentException("Le parentesi non sono correttamente inserite!") End If If expressionToEval.Contains("=") Then If expressionToEval.IndexOf("=", expressionToEval.IndexOf("=") + 1) > 0 Then Throw New ArgumentException("Ci sono più di un segno =!") End If End IfSe l'espressione è una eguaglianza, si richiama ricorsivamente il metodo per il primo e per il secondo membro, si verifica l'uguaglianza e si restituisce l'esito facilmente convertibile in valore booleano.
' prima i membri dell'uguaglianza. ' se entra in questo if, non prosegue, perché esce con un Return o l'altro If expressionToEval.Contains("=") Then Dim primoMembro As Decimal = _ DidacticEval(expressionToEval.Substring(0, expressionToEval.IndexOf("=") - 1)) Dim secondoMembro As Decimal = DidacticEval(expressionToEval.IndexOf("=") + 1) If primoMembro = secondoMembro Then LogStringBuilder.AppendLine("L'uguaglianza è verificata") Return 1D Else LogStringBuilder.AppendLine("L'uguaglianza NON è verificata") Return 0D End If End IfSe l'espressione contiene parentesi, si entra in un ciclo che richiama ricorsivamente il metodo passando via via il contenuto delle parentesi più interne, finché ce ne sono.
' prima le parentesi Do While expressionToEval.Contains("(") OrElse _ expressionToEval.Contains("[") OrElse _ expressionToEval.Contains("{") LogStringBuilder.AppendLine("Ci sono parentesi. Risolvo quelle più interne") ' trovo le parentesi più interne Dim p1 As Integer = expressionToEval.LastIndexOf("(") Dim p2 As Integer = expressionToEval.LastIndexOf("[") Dim p3 As Integer = expressionToEval.LastIndexOf("{") If p1 < p2 Then p1 = p2 If p1 < p3 Then p1 = p3 Dim pi As Integer = p1 p1 = expressionToEval.IndexOf(")", pi) p2 = expressionToEval.IndexOf("]", pi) p3 = expressionToEval.IndexOf("}", pi) If p1 = -1 Then p1 = expressionToEval.Length If p2 = -1 Then p2 = expressionToEval.Length If p3 = -1 Then p3 = expressionToEval.Length If p1 > p2 Then p1 = p2 If p1 > p3 Then p1 = p3 'estraggo la parte tra parentesi Dim subExpression As String = expressionToEval.Substring(pi + 1, p1 - pi - 1) 'la sottopongo ricorsivamente al metodo Dim risultato As Decimal = DidacticEval(subExpression) 'aggiorno l'espressione da valutare expressionToEval = expressionToEval.Remove(pi, p1 - pi + 1) expressionToEval = expressionToEval.Insert(pi, risultato.ToString) Loop ' finché parentesiA questo punto non ci sono più parti di espressione da passare ricorsivamente al metodo, e posso passare a risolvere via via le varie operazioni, individuando operandi e operatori e sostituendo le parti risolte col rispettivo risultato, in un ciclo che si ripete finché ci sono operatori.
Poiché il primo carattere può essere un segno meno a indicare semplicemente un risultato negativo, l'indagine sulla presenza di operatori inizia dal secondo carattere. Questo comporta prevenire l'errore che si verificherebbe se tale secondo carattere fosse assente.'finché l'espressione è indagabile If expressionToEval.Length > 1 Then ' finché ci sono operazioni da fare Do While expressionToEval.Substring(1).Contains("-") _ OrElse expressionToEval.Substring(1).Contains("+") _ OrElse expressionToEval.Substring(1).Contains("*") _ OrElse expressionToEval.Substring(1).Contains("/") _ OrElse expressionToEval.Substring(1).Contains("^") _ OrElse expressionToEval.Contains("v")Dapprima, sostituisco eventuali segni doppi:
If expressionToEval.Contains("+-") Then LogStringBuilder.AppendLine( _ "Esistono coppie di segni contigui + - ! Sostituisco con segno - !") LogStringBuilder.AppendLine("(Spiegazione: + * - = - )") expressionToEval = expressionToEval.Replace("+-", "-") End If If expressionToEval.Contains("--") Then LogStringBuilder.AppendLine( _ "Esistono coppie di segni contigui - - ! Sostituisco con segno + !") LogStringBuilder.AppendLine("(Spiegazione: - * - = + )") expressionToEval = expressionToEval.Replace("--", "+") End IfPoi risolvo tutte le eventuali radici quadrate:
Do While expressionToEval.Contains("v") LogStringBuilder.Append("Ci sono radici quadrate! Numero sotto radice : ") Dim v As Integer = expressionToEval.IndexOf("v") Dim n As String = "" For c As Integer = v + 1 To expressionToEval.Length - 1 If Char.IsDigit(expressionToEval.Chars(c)) _ OrElse expressionToEval.Chars(c) = ","c Then n = n & expressionToEval.Chars(c) Else Exit For End If Next LogStringBuilder.AppendLine(n) If expressionToEval.Chars(v + 1) = "-"c Then Throw New ArgumentException( _ "Risoluzione impossibile: numero negativo sotto radice " & n) End If Dim r As Decimal = Convert.ToDecimal(System.Math.Sqrt(n)) LogStringBuilder.AppendLine("La radice vale: " & r.ToString) expressionToEval = expressionToEval.Remove(v, (v & n).Length) expressionToEval = expressionToEval.Insert(v, r.ToString) LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) LoopE' quindi la volta di tutte le eventuali potenze, la scansione per evincere gli operandi è leggermente più laboriosa:
Do While expressionToEval.Contains("^") LogStringBuilder.Append("Ci sono potenze! Potenza: ") Dim base As String = "", esponente As String = "", operation As String Dim v As Integer = expressionToEval.IndexOf("^") For c As Integer = v - 1 To 0 Step -1 If Char.IsDigit(expressionToEval.Chars(c)) _ OrElse expressionToEval.Chars(c) Like "[,-]" Then base = expressionToEval.Chars(c) & base ElseIf expressionToEval.Chars(c) Like "[()]" Then Else Exit For End If Next For c As Integer = v + 1 To expressionToEval.Length - 1 If Char.IsDigit(expressionToEval.Chars(c)) _ OrElse expressionToEval.Chars(c) = "," Then esponente = esponente & expressionToEval.Chars(c) Else Exit For End If Next operation = base & "^" & esponente Dim p As Integer = expressionToEval.IndexOf(operation) LogStringBuilder.AppendLine(operation) Dim r As Decimal = _ Convert.ToDecimal(Decimal.Parse(base) ^ Decimal.Parse(esponente)) LogStringBuilder.AppendLine("La potenza vale: " & r.ToString) expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, r.ToString) If p > 0 AndAlso Char.IsDigit(expressionToEval.Chars(p - 1)) Then _ expressionToEval = expressionToEval.Insert(p, "+") LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) LoopA questo punto rimangono solo segni aritmetici, quindi scandisco ciò che rimane della stringa da valutare dimensionando tre vettori al numero di caratteri (non vale la pena cercare la precisione assoluta) e riempiendo i primi elementi con i risultati delle scansioni.
Si valuta, per ogni carattere, se è una cifra o un segno e si discrimina se il segno è un meno. Quindi, in capo alla valutazione, si ha una cifra, un operatore, un valore.' scansione della stringa in tre vettori Dim nChars As Integer = expressionToEval.Length Dim cifre(nChars) As String Dim segni(nChars) As String Dim valori(nChars) As Decimal Dim indice As Integer = 0 For c As Integer = 0 To nChars - 1 cifre(c) = "" Static meno As Boolean If meno Then cifre(indice) = "-" meno = False End If If c = 0 Then If Char.IsDigit(expressionToEval.Chars(c)) _ OrElse expressionToEval.Chars(c) Like "[,-]" Then cifre(indice) = expressionToEval.Chars(c) Else segni(indice) = expressionToEval.Chars(c) indice += 1 End If Else If Char.IsDigit(expressionToEval.Chars(c)) _ OrElse expressionToEval.Chars(c) = ","c Then cifre(indice) &= expressionToEval.Chars(c) Else If expressionToEval.Chars(c) = "-" Then If expressionToEval.Chars(c - 1) Like "[+*/]" Then cifre(indice) &= expressionToEval.Chars(c) Else segni(indice) = "+" indice += 1 meno = True End If Else segni(indice) = expressionToEval.Chars(c) indice += 1 End If End If End If If cifre(indice) <> "" AndAlso c > 0 AndAlso Not meno Then _ valori(indice) = Decimal.Parse(cifre(indice)) NextSi può infine procedere alle operazioni, cominciando da divisioni e moltiplicazioni, modificando via via la stringa dell'espressione e calcolando il risultato di ciascuna operazione nel primo elemento del vettore valori:
' esecuzione dei calcoli rimanenti If expressionToEval.Contains("/") OrElse expressionToEval.Contains("*") Then LogStringBuilder.AppendLine( _ "Eseguo i calcoli. Prima le divisioni e le moltiplicazioni") End If Do While Array.IndexOf(segni, "/") > -1 Dim s As Integer = Array.IndexOf(segni, "/") ' posizione del segno Dim operation As String = cifre(s) & "/" & cifre(s + 1) Dim p As Integer = expressionToEval.IndexOf(operation) LogStringBuilder.Append("Eseguo la divisione: ") LogStringBuilder.Append(operation & " => ") valori(s) = System.Math.Round(valori(s) / valori(s + 1), 5) cifre(s) = valori(s).ToString LogStringBuilder.AppendLine(cifre(s)) expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, cifre(s)) cifre(s + 1) = "" segni(s) = "" LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) Loop Do While Array.IndexOf(segni, "*") > -1 Dim s As Integer = Array.IndexOf(segni, "*") ' posizione del segno LogStringBuilder.Append("Eseguo la moltiplicazione: ") Dim operation As String = cifre(s) & "*" & cifre(s + 1) Dim p As Integer = expressionToEval.IndexOf(operation) LogStringBuilder.Append(operation & " => ") valori(s) = System.Math.Round(valori(s) * valori(s + 1), 5) cifre(s) = valori(s).ToString LogStringBuilder.AppendLine(cifre(s)) expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, cifre(s)) cifre(s + 1) = "" segni(s) = "" LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) LoopQuesto frammento di codice contiene un bug, che ha reso necessaria una correzione. Non basta svuotare gli elementi che 'non servono più'. Bisogna rimuoverli. Poiché non è possibile farlo agevolmente con un vettore, riporto gli elementi significativi in altrettante liste. Di conseguenza, il frammento va sostituito con il seguente:
'disposizione dei vettori in liste Dim lstCifre As New List(Of String) Dim lstSegni As New List(Of String) Dim lstValori As New List(Of Decimal) For c As Integer = 0 To nChars - 1 If cifre(c) <> "" Then lstCifre.Add(cifre(c)) lstValori.Add(Decimal.Parse(cifre(c))) End If If segni(c) <> "" Then lstSegni.Add(segni(c)) Next ' esecuzione dei calcoli rimanenti If expressionToEval.Contains("/") OrElse expressionToEval.Contains("*") Then LogStringBuilder.AppendLine( _ "Eseguo i calcoli. Prima le divisioni e le moltiplicazioni") End If Do While lstSegni.Contains("/") ' Array.IndexOf(segni, "/") > -1 Dim s As Integer = lstSegni.IndexOf("/") 'Array.IndexOf(segni, "/") ' posizione del segno Dim operation As String = lstCifre(s) & "/" & lstCifre(s + 1) 'cifre(s) & "/" & cifre(s + 1) Dim p As Integer = expressionToEval.IndexOf(operation) LogStringBuilder.Append("Eseguo la divisione: ") LogStringBuilder.Append(operation & " => ") lstValori(s) = System.Math.Round(lstValori(s) / lstValori(s + 1), 5) lstCifre(s) = lstValori(s).ToString LogStringBuilder.AppendLine(lstCifre(s)) expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, lstCifre(s)) lstCifre.RemoveAt(s + 1) 'cifre(s + 1) = "" lstValori.RemoveAt(s + 1) lstSegni.RemoveAt(s) 'segni(s) = "" LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) Loop Do While lstSegni.Contains("*") ' Array.IndexOf(segni, "/") > -1 Dim s As Integer = lstSegni.IndexOf("*") 'Array.IndexOf(segni, "/") ' posizione del segno Dim operation As String = lstCifre(s) & "*" & lstCifre(s + 1) 'cifre(s) & "/" & cifre(s + 1) Dim p As Integer = expressionToEval.IndexOf(operation) LogStringBuilder.Append("Eseguo la moltiplicazione: ") LogStringBuilder.Append(operation & " => ") lstValori(s) = System.Math.Round(lstValori(s) * lstValori(s + 1), 5) lstCifre(s) = lstValori(s).ToString LogStringBuilder.AppendLine(lstCifre(s)) expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, lstCifre(s)) lstCifre.RemoveAt(s + 1) 'cifre(s + 1) = "" lstValori.RemoveAt(s + 1) lstSegni.RemoveAt(s) 'segni(s) = "" LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) LoopInoltre, l'ultima istruzione del ciclo For prima di questo codice, va corretta così:
If cifre(indice) <> "" AndAlso cifre(indice) <> "-" Then _ valori(indice) = Decimal.Parse(cifre(indice))Alla fine, scandisco gli elementi rimasti, dato che sono sicuramente soltanto somme algebriche:
(Naturalmente, invece dei vettori, come nella vecchia versione, uso le liste)
For e As Integer = 0 To lstSegni.Count - 1 Dim operation As String If lstSegni(e) = "+" Then If lstValori(e + 1) < 0 Then LogStringBuilder.Append("Eseguo la sottrazione : ") operation = lstCifre(0) & lstCifre(e + 1) Else LogStringBuilder.Append("Eseguo l'addizione : ") operation = lstCifre(0) & "+" & lstCifre(e + 1) End If LogStringBuilder.Append(operation & " = ") lstValori(0) += lstValori(e + 1) Dim p As Integer = expressionToEval.IndexOf(operation) lstValori(0) = System.Math.Round(lstValori(0), 5) lstCifre(0) = lstValori(0).ToString expressionToEval = expressionToEval.Remove(p, operation.Length) expressionToEval = expressionToEval.Insert(p, lstCifre(0)) lstCifre(e + 1) = "" lstSegni(e) = "" LogStringBuilder.AppendLine(lstCifre(0)) LogStringBuilder.AppendLine( _ "L'espressione da risolvere diventa: " & expressionToEval) End If NextDopo la conclusione del ciclo, restituisco il risultato:
LogStringBuilder.AppendLine() LogStringBuilder.AppendLine("Risultato finale= " & expressionToEval) LogStringBuilder.AppendLine( _ "________________________________________________________________") LogStringBuilder.AppendLine() Return Decimal.Parse(expressionToEval)Come si usa la classe
Supponendo che l'utente scriva l'espressione in una TextBox di nome txtEspressione e prema un pulsante, il codice eseguito da questo pulsante può essere:Dim oEval As New StringEvaluator Dim result As Decimal = oEval.DidacticEval(txtEspressione.Text) MessageBox.Show(oEval.ExpressionResolvingLog)Valutare una stringa senza occuparsi della didattica
Il codice fin qui visto ha lo scopo, lo ricordo, di illustrare la risoluzione di una espressione passaggio dopo passaggio.
Ma se si volesse risolvere una espressione 'tout court', lasciando al sistema l'onere di scandire e validare la stringa passata?Naturalmente è possibile, solo che le risorse impiegate sono diverse.
In Visual Basic 6 si referenzia al progetto il componente Microsoft Script Control e se ne usa il metodo Eval.
In VBA si potrebbe fare lo stesso, ma Anonimo ha fatto di più: ha implementato un codice che crea ed esegue dell'altro codice, basandosi sull'oggetto Module di Access:Option Compare Database Option Explicit Private Result As Long Public Function Evaluate(ByVal CompareString As String) As Long Dim ModuleObject As Module ModuleObject = Modules!Classe1 ModuleObject.ReplaceLine(ModuleObject.CountOfLines - 1, "Result=" & CompareString()) Call Calc() Evaluate = Result ModuleObject.ReplaceLine(ModuleObject.CountOfLines - 1, "") End Function Private Function Calc() End FunctionQuesto codice acquisisce un riferimento al proprio modulo, sostituisce la penultima riga (il contenuto della funzione Calc) con una assegnazione
variabile di modulo = espressione da calcolarerichiama la funzione e restituisce il risultato, ripristinando quindi la riga vuota.
Il metodo Evaluate
Anonimo ha pensato di seguire lo stesso schema anche in Visual Basic.Net.
Ha dato una bella occhiata alle varie classi offerte dal Framework e ha implementato il codice seguente (in verità ne ha fatto una seconda Tip, che io ho rielaborato come metodo della stessa classe StringEvaluator):Public Function Evaluate(ByVal expressionToEval As String) As Object ' istanza di un fornitore di accesso al generatore di codice e al compilatore VisualBasic Dim codeProvider As New Microsoft.VisualBasic.VBCodeProvider() ' istanza dei parametri per il compilatore Dim params As New System.CodeDom.Compiler.CompilerParameters() ' codice da compilare ed eseguire Dim codeToExec As String = _ "Class Evaluate" & Environment.NewLine & _ "Public function Eval() As Object" & Environment.NewLine & _ "Return " & expressionToEval & Environment.NewLine & _ "End function" & Environment.NewLine & _ "End Class" ' visualizzazione per verifiche in fase di testing ' MessageBox.Show(codeToExec) ' assembly risultante dalla compilazione del codice Dim assembly As System.CodeDom.Compiler.CompilerResults = _ codeProvider.CompileAssemblyFromSource(params, codeToExec) ' istanza della classe Evaluate definita nel codice compilato Dim instance As Object = assembly.CompiledAssembly.CreateInstance("Evaluate") ' acquisisce le informazioni necessarie all'uso del metodo Eval Dim methodCall As System.Reflection.MethodInfo = instance.GetType().GetMethod("Eval") ' esegue il metodo e restituisce il risultato Return methodCall.Invoke(instance, Nothing) End FunctionIl codice è abbondantemente commentato. C'è da precisare che, di default, i parametri di compilazione si riferiscono a una libreria, piuttosto che a un eseguibile (ecco perché non ne viene impostato alcuno).
Il codice della libreria è una classe con un unico metodo che restituisce il calcolo della espressione da valutare.Ovviamente, la stringa da passare al metodo StringEvaluator.Evaluate dovrebbe essere controllata e validata (parentesi, operatori e funzioni riconoscibili dal Framework: quindi niente parentesi quadre o grafe, o caratteri 'v' per le radici quadrate, eccetera).
Conclusioni
In questo articolo è stata trattata la revisione di codice simil-Basic in codice VB2005, nonché l'uso della compilazione al volo per calcolare un'espressione numerica.
Il codice a corredo (corretto) è scaricabile dall'area download.
Per critiche o suggerimenti, scrivetemi.