【テクニカル・上級編】Dictionaryオブジェクトのキーにユーザー定義型を使用する際のハッシュ化戦略 – Excel VBA解析バイブル

スポンサーリンク

Excel VBAを掌握する極限の知見:Dictionaryのキーにユーザー定義型(UDT)を撃ち込むハッシュ化戦略

VBAにおける `Scripting.Dictionary` は、そのO(1)の高速なルックアップ性能により、大規模なデータ処理や重複排除においてなくてはならない存在だ。通常、キーには文字列や数値を使用する。しかし、業務システムの現場で複雑な複合キー(例:「得意先コード + 伝票番号 + 明細行番号」)を扱わなければならないとき、どうしているだろうか。

「すべての要素を区切り文字で連結して一つの文字列キーにする」――そんな原始的な方法で満足していないだろうか。文字列表現へのシリアライズは、パフォーマンステストの段階で必ずボトルネックになり、メモリ領域の断片化を招く。

今回は、VBAの限界を突破し、ユーザー定義型(UDT: User-Defined Type)や複数パラメータの構造体をそのままDictionaryのキーとして機能させるための「ハッシュ化戦略」を、メモリ管理とWindows APIの裏側まで含めて徹底的に解説する。

1. なぜUDTをそのままDictionaryのキーにできないのか?

VBAの `Dictionary` オブジェクトは、内部でキーのハッシュ値を計算してバケットを管理している。しかし、VBAの `Variant` 型に格納されたUDTは、COM(Component Object Model)の境界を越えて `Scripting.Dictionary` に渡されたとき、ハッシュアルゴリズムがそのバイナリ構造を正しく解釈できない。

結果として、ランタイムエラーが発生するか、同じ内容のUDTであってもメモリ上のアドレスや参照が異なるために「別物のキー」として扱われてしまう。

この課題をクリアする唯一にして最強のアプローチが、「UDTのメモリイメージ(またはその一部)を決定論的なハッシュ文字列、あるいは一意なバイナンスキーへ変換する(ハッシュ化)」手法である。

2. 実装アプローチ:高速バイナリハッシュ生成エンジン

実務において、キーとなるUDTのメンバ(文字列、長整数、日付など)を単に結合するのではなく、メモリ上の連続した領域として安全に扱い、高速なハッシュ(または一意のバイト配列)へと変換するアーキテクチャを構築する。

ここでは、Windows APIの `CopyMemory`(`RtlMoveMemory`)を駆使し、UDTの生メモリから効率的に一意のキー文字列を生成する実用コードを提示する。

究極のハッシュ化・実装コード

以下のコードを標準モジュールに配置せよ。

Option Explicit

‘ メモリコピー用Windows API宣言(64bit/32bit完全対応)
If VBA7 Then
Declare PtrSafe Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As LongPtr)
Else
Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)
End If

‘ 業務データを模したユーザー定義型(UDT)
Public Type TTransactionKey
CustomerID As Long ‘ 得意先ID (4 bytes)
SlipNumber As Currency ‘ 伝票番号 (8 bytes – 高精度固定小数点)
BranchCode As Integer ‘ 枝番 (2 bytes)
ProcessDate As Double ‘ 処理日時 (8 bytes – Date型実体)
End Type

”’

”’ UDTのバイナリを安全に読み込み、一意のBase64風またはHEX文字列キーへ変換する
”’

Public Function GetHashFromUDT(ByRef udtKey As TTransactionKey) As String
Dim byteBuffer() As Byte
Dim lngSize As Long

‘ UDTのバイトサイズを正確に取得
lngSize = LenB(udtKey)

‘ バッファ配列のサイズを確保
ReDim byteBuffer(0 To lngSize – 1)

‘ UDTのメモリ領域から安全にバイト配列へコピー
CopyMemory byteBuffer(0), udtKey, lngSize

‘ バイト配列を高速にHEX文字列(一意のハッシュキー)へ変換
GetHashFromUDT = BytesToHexFast(byteBuffer)
End Function

”’

”’ バイト配列を極限まで高速に16進数文字列へ変換する内部関数
”’

Private Function BytesToHexFast(ByRef bytes() As Byte) As String
Const HEX_CHARS As String = “0123456789ABCDEF”
Dim i As Long, uBoundVal As Long
Dim result() As Integer ‘ 文字列構築のオーバヘッドを削減するためInteger配列を使用

uBoundVal = UBound(bytes)
ReDim result(0& To (uBoundVal 2) + 1)

For i = 0 To uBoundVal
result(i 2) = Asc(Mid$(HEX_CHARS, (bytes(i) \ 16) + 1, 1))
result(i 2 + 1) = Asc(Mid$(HEX_CHARS, (bytes(i) And 15) + 1, 1))
Next i

BytesToHexFast = Left$(StrConv(result, vbUnicode), (uBoundVal + 1) 2)
End Function

3. 実践:Dictionaryと組み合わせた高速ルックアップ

上記のハッシュ生成エンジンを使用することで、複雑なUDTをキーにしたDictionary操作が完璧に成立する。

Public Sub ExecuteDictionaryWithUDTKey()
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)

‘ キーとなるUDTのインスタンス化
Dim key1 As TTransactionKey
key1.CustomerID = 1001
key1.SlipNumber = 20231001001#
key1.BranchCode = 1
key1.ProcessDate = Now

‘ ハッシュ化してDictionaryのキーとして登録
Dim dictKey As String
dictKey = GetHashFromUDT(key1)

‘ 値として任意のデータ(オブジェクトや配列など)を格納
dict.Add dictKey, “売上データ_A”

‘ — 検索時のテスト —
Dim keySearch As TTransactionKey
keySearch = key1 ‘ 同じ値を持つ構造体を別変数に代入

Dim searchKey As String
searchKey = GetHashFromUDT(keySearch)

If dict.Exists(searchKey) Then
Debug.Print “ヒット成功! 値: ” & dict(searchKey)
Else
Debug.Print “データが見つかりません”
End If

‘ オブジェクトの明示的解放
Set dict = Nothing
End Sub

4. チーフアーキテクトが語る「メモリ管理とリスクヘッジ」

このアプローチを実務の極限環境(数百万件のトランザクション処理や、24時間稼働するExcel常駐型マクロなど)に投入する際、以下の設計思想を忘れてはならない。

1. UDTのパディング(メモリ配置アライメント)に注意せよ

VBAのUDTは、CPUのメモリアクセス効率を最適化するため、メンバの境界間に「パディング(パディングバイト)」が自動挿入される場合がある。
`LenB(udtKey)` はこのパディングを含んだ実際のメモリサイズを返すため、`CopyMemory` によるハッシュ化においては極めて安全である。ただし、異なるプラットフォーム(32bit版Officeと64bit版Office間)でUDTのバイナリをシリアライズしてファイル保存・ネットワーク転送する用途には使えない(同一セッション内のDictionaryキーとしてのみ機能させること)。

2. オブジェクト参照(Object型メンバ)の禁止

UDTの内部に `Object` 型や可変長文字列(`String`)を含めてはならない。これらを含めると、UDT内部に格納されるのは「ポインタ(参照アドレス)」そのものになり、インスタンスが異なればアドレスも変わるため、内容が同じであってもハッシュ値が一致しなくなる。
DictionaryのキーとしてUDTを使う場合は、「値型メンバ(Long, Integer, Currency, Double, 固定長Stringなど)」のみで構成された構造体に限定すべきである。

3. メモリリークとガベージコレクションの厳守

VBAには明示的なデストラクタがないため、巨大なDictionaryや動的配列を扱う際は、プロシージャの終了時に必ず `Set dict = Nothing` を実行し、COMオブジェクトの参照カウントを即座にゼロに落とすこと。これを怠ると、Excelのプロセス内にメモリ断片化が蓄積し、やがて「リソース不足」の致命傷に至る。

総括

VBAは「おもちゃの言語」ではない。ハードウェアの仕様とWindowsのメモリモデルを正しく理解し、今回紹介したようなハッシュ化戦略を組み合わせることで、C#やC++に匹敵する堅牢かつ高速なインメモリ・データベースエンジンとして機能させることが可能だ。

泥臭い文字列連結によるキー生成は今日で終わりにし、洗練されたバイナリハッシュによる真のモダンVBAアーキテクチャを、あなたのシステムへ実装してほしい。

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