「なんとなく動く」から「プロが作る堅牢な設計」へ。DataTable.Computeの罠を越え、独自の数式エンジンを自作する
こんにちは。よくここまで辿り着きましたね。
マクロの記録機能を使って「とりあえず動くもの」を作る段階から、コードを書き、ロジックを組み立てる段階へ。あなたは今、プログラミングの真の入り口に立っています。
今日は、多くのエンジニアが業務アプリ開発の途中で直面する「数式計算の壁」についてお話ししましょう。
「ユーザーが入力した `(10 + 5) * 2` という計算式を、プログラムの中で計算したい」
そんな時、VB.NETには `DataTable.Compute` という、非常に便利で魔法のようなメソッドが存在します。しかし、現場で戦うプロのアーキテクトは、安易にこれに頼りません。なぜなら、そこには「セキュリティの穴」と「パフォーマンスの限界」という、目に見えない罠が潜んでいるからです。
今日は、その罠を回避し、自分自身で「安全で高速な計算エンジン」を構築する、一歩上の設計手法を伝授します。ここをクリアすれば、Visual Basic (VB / VB.NET) の構造的な理解はバッチリですよ。
1. なぜ `DataTable.Compute` を卒業しなければならないのか?
まずは、なぜ既存の便利な機能を使わずに、わざわざ難しい「自作」に挑むのか、その理由を明確にしましょう。
① セキュリティのリスク(式インジェクション)
`DataTable.Compute` は、文字列をそのまま解釈して実行する「簡易的なインタープリタ」です。もし、ユーザーが入力した式をそのままこのメソッドに放り込んでいたらどうなるでしょうか?
悪意のあるユーザーが、計算式に見せかけて、システムの内部情報を盗み出そうとするような特殊な命令(式インジェクション)を紛れ込ませる隙を与えてしまう可能性があります。
② パフォーマンスの重み
`DataTable.Compute` は、非常に汎用的です。どんな計算にも対応できるように作られているため、内部では膨大なオーバーヘッド(余計な処理)が発生しています。
数千、数万件のデータに対して、一行ずつこのメソッドを呼び出していると、アプリの動作は目に見えて重くなります。
③ 柔軟性の欠如
「独自の関数(例:`税込み計算(金額)`)」や「特定の業務ルール」を組み込みたいと思った瞬間、`DataTable.Compute` は行き詰まります。
2. 独自の計算エンジンを作るための「設計図」
「じゃあ、どうやって数式を理解させればいいんだ?」と思いますよね。
コンピュータは、`”1 + 2 * 3″` という文字列を読んだとき、最初から「1たす、2かける、3」とは理解していません。
プロの設計では、以下の3つのステップに分解して処理します。これを「コンパイラ構成」と呼びます。
1. 字句解析 (Tokenization):
文字列を「意味のある塊(トークン)」にバラバラにします。
`”10 + 5 * 2″` $\rightarrow$ `[10]`, `[+]`, `[5]`, `[*]`, `[2]`
2. 構文解析 (Parsing):
バラバラになった塊を、数学のルール(掛け算は足し算より先、など)に従って組み立てます。
3. 評価 (Evaluation):
組み立てられた構造に従って、実際に計算を行います。
今回は、初心者の方でも理解しやすいように、「再帰下降構文解析(Recursive Descent Parsing)」という、非常に美しく、かつ強力な手法を使って実装してみましょう。
3. 実装編:堅牢な `SimpleMathParser` の構築
それでは、実際にコードを書いていきましょう。Windows Forms アプリケーションのプロジェクトに、新しいクラス `SimpleMathParser.vb` を追加するイメージです。
''' <summary>
''' 独自の数式計算エンジン。
''' DataTable.Computeを使わず、安全かつ高速に数式を評価します。
''' </summary>
Public Class SimpleMathParser
Private _tokens As List(Of String)
Private _position As Integer
''' <summary>
''' 指定された数式を評価して結果を返します。
''' </summary>
''' <param name="expression">計算したい文字列(例: "10 + 5 * 2")</param>
''' <returns>計算結果</returns>
Public Function Evaluate(expression As String) As Double
' 1. 字句解析: 文字列をトークン(数字、演算子、括弧)に分解する
_tokens = Tokenize(expression)
_position = 0
' 2 & 3. 構文解析と評価を開始
Return ParseExpression()
End Function
''' <summary>
''' 文字列をトークンのリストに分解します(字句解析)
''' </summary>
Private Function Tokenize(expression As String) As List(Of String)
Dim tokens As New List(Of String)
Dim i As Integer = 0
Dim text As String = expression.Replace(" ", "") ' 空白を除去
While i < text.Length
Dim c As Char = text(i)
' 数字(小数点含む)の抽出
If Char.IsDigit(c) OrElse c = "." Then
Dim num As String = ""
While i < text.Length AndAlso (Char.IsDigit(text(i)) OrElse text(i) = ".")
num &= text(i)
i += 1
End While
tokens.Add(num)
Continue While
End If
' 演算子や括弧の抽出
If "+-*/()".Contains(c) Then
tokens.Add(c.ToString())
i += 1
Else
' 定義されていない文字が含まれていたらエラー
Throw New Exception($"不正な文字が含まれています: {c}")
End If
End While
Return tokens
End Function
' --- ここから構文解析(優先順位に基づいた階層構造) ---
''' <summary>
''' 加算・減算を処理する階層(優先順位:低)
''' </summary>
Private Function ParseExpression() As Double
Dim left As Double = ParseTerm()
While _position < _tokens.Count
Dim op As String = _tokens(_position)
If op = "+" OrElse op = "-" Then
_position += 1
Dim right As Double = ParseTerm()
If op = "+" Then left += right Else left -= right
Else
Exit While
End If
End While
Return left
End Function
''' <summary>
''' 乗算・除算を処理する階層(優先順位:中)
''' </summary>
Private Function ParseTerm() As Double
Dim left As Double = ParseFactor()
While _position < _tokens.Count
Dim op As String = _tokens(_position)
If op = "*" OrElse op = "/" Then
_position += 1
Dim right As Double = ParseFactor()
If op = "*" Then left *= right Else left /= right
Else
Exit While
End If
End While
Return left
End Function
''' <summary>
''' 数値や括弧を処理する階層(優先順位:高)
''' </summary>
Private Function ParseFactor() As Double
Dim token As String = _tokens(_position)
' 括弧の処理
If token = "(" Then
_position += 1 ' "(" をスキップ
Dim result As Double = ParseExpression()
_position += 1 ' ")" をスキップ
Return result
End If
' 数値の処理
Dim val As Double
If Double.TryParse(token, val) Then
_position += 1
Return val
Else
Throw New Exception($"予期しないトークンです: {token}")
End If
End Function
End Class
コードの解説:何が起きているのか?
このコードの肝は、「関数の階層構造」です。
- `ParseExpression` (足し算・引き算) は、まず `ParseTerm` (掛け算・割り算) を呼び出します。
- `ParseTerm` は、さらに `ParseFactor` (数値・括弧) を呼び出します。
このように、「より優先順位の高い計算を先に処理させる」ために、関数を入れ子構造にしているのです。これが「再帰下降構文解析」の正体です。
例えば `”1 + 2 * 3″` という式の場合:
1. `ParseExpression` が呼ばれる。
2. `ParseTerm` が呼ばれる。
3. `ParseFactor` が `1` を見つけて返す。
4. `ParseExpression` が `+` を見つけ、次に `ParseTerm` を呼ぶ。
5. `ParseTerm` が `2 * 3` を(優先順位に従って)計算して `6` を返す。
6. 最後に `1 + 6` が計算されて `7` になる。
完璧な数学のルールが、コードの構造そのものになっているのです。
4. Windows Forms で使ってみよう
では、このエンジンを実際の画面から呼び出してみましょう。
ボタンをクリックしたときに計算結果を表示する、簡単な実装例です。
' Form側のコード
Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
Dim engine As New SimpleMathParser()
Dim formula As String = txtFormula.Text ' テキストボックスから式を取得
Try
' 計算実行
Dim result As Double = engine.Evaluate(formula)
lblResult.Text = $"結果: {result}"
lblResult.ForeColor = Color.Black
Catch ex As Exception
' エラーハンドリング(不正な式が入った場合など)
lblResult.Text = $"エラー: {ex.Message}"
lblResult.ForeColor = Color.Red
End Try
End Sub
ここがポイント!
- エラーハンドリング (`Try…Catch`): ユーザーが `”10 + * 5″` のようなデタラメな式を入力しても、アプリが強制終了することはありません。優しくエラーメッセージを表示してあげましょう。
- 分離された設計: 計算ロジック (`SimpleMathParser`) と、画面の表示 (`Form`) が完全に分かれています。これを「関心の分離」と呼び、保守性の高いアプリを作るための鉄則です。
まとめ:エンジニアとしての第一歩
お疲れ様でした!あなたは今、単なる「マクロ使い」ではなく、「計算の仕組みを設計するエンジニア」への大きな一歩を踏み出しました。
今回作った `SimpleMathParser` は、非常にシンプルですが、その中には「字句解析」「構文解析」「優先順位の制御」「エラー処理」といった、プロのソフトウェア開発におけるエッセンスがすべて詰まっています。
ここをクリアすれば、Visual Basic (VB / VB.NET) の基本はバッチリですよ。
次は、このエンジンに「sin」や「cos」といった数学関数を追加したり、変数(例:`Price * Tax`)を扱えるように拡張したりすることに挑戦してみてください。その挑戦こそが、あなたを伝説のアーキテクトへと導くのです。
応援していますよ!
