【テクニカル・上級編】VB.NETで作る独自の簡易数式計算エンジン:DataTable.Computeのセキュリティリスクとパフォーマンス限界を突破する安全な構文解析パーサーの自社開発 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

「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` は、プロトタイプを作るには良い。しかし、プロダクション・レベルのシステムにおいては、制御不能な変数になり得る。

自前でパーサーを構築するという行為は、単なる技術的な自己満足ではない。それは、「計算のプロセスを完全に掌握し、予測可能性と信頼性をシステムに付与する」という、エンジニアとしての責務である。

この設計思想を武器に、堅牢で、高速で、そして美しいコードを書き続けてほしい。

タイトルとURLをコピーしました