「DataTable.Compute」の終焉:堅牢性と極限のパフォーマンスを両立する自作数式エンジンの設計思想
業務アプリケーションの設計において、多くのエンジニアが「手軽さ」という甘い罠に足を取られる。その最たるものが、`DataTable.Compute` メソッドの乱用だ。
「数式を文字列で渡せば計算してくれる」――その利便性の裏側には、ブラックボックス化された解析ロジック、拡張性の欠如、そして何より、大規模データ処理における致命的なパフォーマンスの低下と、予期せぬ入力による脆弱性が潜んでいる。
もし君が、社内の基幹システムを支えるシニアエンジニア、あるいはミッションクリティカルな計算ロジックを管理するアーキテクトであるならば、この「手軽な道具」を捨て、自らの手で「制御可能な計算エンジン」を構築すべき時が来ている。
本稿では、外部ライブラリに依存せず、VB.NETを用いて、セキュリティ、精度、そして実行速度のすべてにおいて `DataTable.Compute` を凌駕する独自の数式パーサーを設計・実装するための極限の知見を伝授する。
1. なぜ `DataTable.Compute` ではいけないのか
設計の正当性を証明するには、まず既存の手法の限界を明確にする必要がある。
1.1 拡張性の欠如とドメイン知識の分離不能
`DataTable.Compute` は、あくまで DataColumn のフィルタリングや単純な算術演算を目的としている。例えば、「消費税率を考慮した端数処理」や「特定の業務ルールに基づいた条件分岐」といった、ドメイン固有の関数(Domain-Specific Functions)を組み込むことは不可能に近い。
1.2 実行時オーバーヘッド(GC圧の増大)
`DataTable.Compute` は、呼び出しのたびに内部で構文解析を行い、動的な評価を行う。大量のレコードに対してループ内でこれを実行すれば、大量のオブジェクトがヒープ領域に割り当てられ、GC(Garbage Collector)の頻発を招く。これは、Windows Forms アプリケーションの UI スレッドをブロックし、ユーザー体験を著しく損なう原因となる。
1.3 セキュリティリスク
入力文字列がユーザーインターフェース(UI)から直接渡される場合、意図しない構文による計算結果の改ざんや、システムリソースを過度に消費させる「計算爆弾」への対策が極めて困難である。
2. アーキテクチャ設計:レキシカル解析から評価まで
堅牢なパーサーを構築するためには、以下の3つのフェーズを厳格に分離したパイプラインを設計しなければならない。
1. Lexer (Tokenizer): 文字列を意味のある最小単位(トークン)に分解する。
2. Parser: トークンの並びから、演算子の優先順位に基づいた構造(抽象構文木:AST、または逆ポーランド記法)を構築する。
3. Evaluator: 構築された構造を走査し、実際の数値を計算する。
ここで重要なのは、「数値の精度」だ。金融計算や業務計算を扱う場合、`Double` 型の使用は禁物である。浮動小数点数特有の丸め誤差は、システム全体の信頼性を崩壊させる。必ず `Decimal` 型を基軸とした設計を行うこと。
3. 実装への挑戦:高効率な再帰下降パーサー
ここでは、外部依存を一切排除し、メモリ割り当てを最小限に抑えた実装例を示す。パフォーマンスを極限まで高めるため、`Class` ではなく `Structure` を活用し、ヒープへの負荷を軽減する。
''' <summary>
''' 業務計算用 高性能数式エンジン
''' </summary>
Public Module SecureMathEngine
''' <summary>
''' トークンの種類を定義
''' </summary>
Public Enum TokenType
Number
Operator
LeftParenthesis
RightParenthesis
End
End Enum
''' <summary>
''' トークン構造体。ClassではなくStructureを使用し、GC圧を抑制する
''' </summary>
Public Structure Token
Public Type As TokenType
Public Value As Decimal
Public Op As Char
End Structure
''' <summary>
''' 構文解析および評価を行うメインクラス
''' </summary>
Public Class ExpressionParser
Private _tokens As List(Of Token)
Private _position As Integer
''' <summary>
''' 数式文字列を解析し、結果を返す
''' </summary>
Public Function Evaluate(expression As String) As Decimal
Try
_tokens = Tokenize(expression)
_position = 0
Return ParseExpression()
Catch ex As Exception
Throw New InvalidOperationException($"数式解析エラー: {ex.Message}")
End Try
End Function
''' <summary>
''' Lexer: 文字列をトークンに分解する
''' </summary>
Private Function Tokenize(input As String) As List(Of Token)
Dim tokens As New List(Of Token)()
Dim i As Integer = 0
Dim len As Integer = input.Length
While i < len
Dim c As Char = input(i)
If Char.IsWhiteSpace(c) Then
i += 1
ElseIf Char.IsDigit(c) OrElse c = "." Then
' 数値の抽出
Dim start As Integer = i
While i < len AndAlso (Char.IsDigit(input(i)) OrElse input(i) = ".")
i += 1
End While
Dim val As Decimal = Decimal.Parse(input.Substring(start, i - start))
tokens.Add(New Token With {.Type = TokenType.Number, .Value = val})
ElseIf c = '+' OrElse c = '-' OrElse c = '*' OrElse c = '/' Then
' 演算子の抽出
tokens.Add(New Token With {.Type = TokenType.Operator, .Op = c})
i += 1
ElseIf c = '(' Then
tokens.Add(New Token With {.Type = TokenType.LeftParenthesis})
i += 1
ElseIf c = ')' Then
tokens.Add(New Token With {.Type = TokenType.RightParenthesis})
i += 1
Else
Throw New Exception($"未知の文字: {c}")
End If
End While
tokens.Add(New Token With {.Type = TokenType.End})
Return tokens
End Function
''' <summary>
''' Parser: 再帰下降法による式解析 (優先順位: +, -)
''' </summary>
Private Function ParseExpression() As Decimal
Dim left As Decimal = ParseTerm()
While _position < _tokens.Count AndAlso
(_tokens(_position).Type = TokenType.Operator AndAlso
(_tokens(_position).Op = "+"c OrElse _tokens(_position).Op = "-"c))
Dim op As Char = _tokens(_position).Op
_position += 1
Dim right As Decimal = ParseTerm()
If op = "+"c Then left += right Else left -= right
End While
Return left
End Function
''' <summary>
''' Parser: 再帰下降法による式解析 (優先順位: *, /)
''' </summary>
Private Function ParseTerm() As Decimal
Dim left As Decimal = ParseFactor()
While _position < _tokens.Count AndAlso
(_tokens(_position).Type = TokenType.Operator AndAnd
(_tokens(_position).Op = "*"c OrElse _tokens(_position).Op = "/"c))
Dim op As Char = _tokens(_position).Op
_position += 1
Dim right As Decimal = ParseFactor()
If op = "*"c Then
left *= right
Else
If right = 0 Then Throw New DivideByZeroException("ゼロ除算が発生しました。")
left /= right
End If
End While
Return left
End Function
''' <summary>
''' Parser: 最優先単位 (数値または括弧内の式)
''' </summary>
Private Function ParseFactor() As Decimal
Dim token = _tokens(_position)
If token.Type = TokenType.Number Then
_position += 1
Return token.Value
ElseIf token.Type = TokenType.LeftParenthesis Then
_position += 1 ' '(' をスキップ
Dim result = ParseExpression()
If _tokens(_position).Type <> TokenType.RightParenthesis Then
Throw New Exception("括弧が閉じられていません。")
End If
_position += 1 ' ')' をスキップ
Return result
Else
Throw New Exception("不正な構文です。")
End If
End Function
End Class
End Module
4. 極限の知見:パフォーマンスと保守性のための最適化指針
実装コードを書くだけでは、伝説のアーキテクトとは呼べない。運用フェーズを見据えた「重み」を知る必要がある。
4.1 メモリ管理:Allocation の最小化
上記のコードでは、`Token` を `Structure` として定義している。これは、`List(Of Token)` が確保するメモリが、オブジェクトの参照(ポインタ)ではなく、実データとして連続したメモリブロックに配置されることを意味する。これにより、CPU キャッシュのヒット率が向上し、GC の走査対象となるオブジェクト数を劇的に減らすことができる。
4.2 Windows API との連携:システム監視
大規模な計算処理を行う場合、そのプロセスがシステムリソースを食いつぶしていないかを監視する必要がある。`GetProcessMemoryInfo` などの Windows API を使用して、パーサー実行前後の Working Set の変化をログに記録する仕組みを組み込むべきだ。これは、レガシーな環境でのメモリリーク検知において、極めて強力な武器となる。
4.3 レガシー環境への配慮
VB.NET を使用しているということは、.NET Framework 環境での動作が求められるケースが多い。最新の `Span(Of T)` を使ったメモリ効率化が使えない環境であっても、上記のような「再帰下降法」と「構造体によるデータ保持」の組み合わせは、常に一定以上のパフォーマンスを保証する。
結言
`DataTable.Compute` は、プロトタイプを作るには良い。しかし、プロダクション・レベルのシステムにおいては、制御不能な変数になり得る。
自前でパーサーを構築するという行為は、単なる技術的な自己満足ではない。それは、「計算のプロセスを完全に掌握し、予測可能性と信頼性をシステムに付与する」という、エンジニアとしての責務である。
この設計思想を武器に、堅牢で、高速で、そして美しいコードを書き続けてほしい。
