ハッシュテーブルとは
ハッシュテーブルとは配列のようにキーを指定してアクセス出来るコレクションの一種で、 言語によっては辞書(Dictionary)や連想配列などと呼ばれています。
配列とは違い、数値以外のデータをキーとして指定する事が出来ます。
-- 配列要素へのアクセスの例
-- キーは1以上の整数のみ
ary[1]
-- ハッシュテーブル要素へのアクセスの例
-- 数値以外をキーに出来る
-- もちろん数値もキーに出来る
hash["key"]
残念ながらMaxScriptにはハッシュテーブルが無く、標準では上記のようなアクセスは出来ないのですが…
また、配列はデータの検索を行うとき、要素の先頭から順次検索する必要があるので、非常に遅くなるという問題があります(findItem等)。しかし、ハッシュテーブルではデータが”順番”を持たないという特性があり、要素の検索が極めて高速に行えます。
例えば以下のようなデータを追加したとします。
hash[1] = "Val1"
hash[2] = "Val2"
hash[3] = "Val3"
これが配列であれば、当然、要素”Val1”,”Val2”,”Val3”はその順番どおりに格納されているのですが、ハッシュテーブでは必ずしもその順番で格納されているとは限りません。
ハッシュテーブでは、キーと要素のペアがランダムに格納されており、キーを指定してアクセスすると、その都度ペアとなる要素を内部から検索するような仕組みになっています。
ではなぜ検索が高速に行えるのかというと・・・これは少し複雑な仕組みになっているのですが、簡単に説明すると、キーの値をハッシュ化(重複のない一意な数値化)し、その数値をソートして保持しています。
そうすることで、ハッシュ値を使ってバイナリサーチ(二分探査)を行う事ができ、常に高速な検索が行えるというわけです。
バイナリサーチでは要素数が増えても、ほとんど検索時間は増えません。
その為、キーを整数にする場合でも、膨大なデータから要素を頻繁に検索する必要がある時は
非常に有効です。
MaxScriptでハッシュテーブルを利用する
それでは、実際にMaxScriptでハッシュテーブルを利用する方法を紹介します。
例によって.Netを使用します。
-- ハッシュテーブルの初期化
ht = dotNetObject "System.Collections.Hashtable"
-- 要素の追加
ht.add 1 "val1" -- 1 がkey, "val1"が値
ht.add 2 "val2"
ht.add "text1" "val3" -- "text1"がkey, "val3"が値
-- 要素の設定
ht.set_item 1 "val4"
ht.set_item "text2" "val5"
-- 要素の取得
ht.get_item 1 -- "val4"
ht.get_item 2 -- "val2"
ht.get_item "text1" -- "val3"
-- 要素の取得の別の書き方
ht.Item[1] -- "val4"
ht.Item["text2"] -- "val5"
-- 存在しないキーはundefinedを返す
ht.get_item 3 -- undefined
ht.Item["text3"] -- undefined
add, set_itemでは1つ目の引数がキー、2つ目が値になります。
get_itemではキーを指定して値を取得しています。
また、データの取得ではItem[]といった書き方も出来ますが、設定では使えないようです。
set_itemと合わせた時非対称になるので、私はよくget_itemの方を使っています。
オリジナルの.NETの方では存在しないキーにアクセスした時エラーが発生しますが、MaxScriptから使用する時はundefinedを返すようです。
注意点として、addとset_itemは良く似ているように見えますが、既に存在するキーを再度指定した場合、addではエラーが発生するのに対し、set_itemでは値の置き換えが行われます。
全ての要素に順次アクセスする
-- 値の追加
ht = dotNetObject "System.Collections.Hashtable"
ht.set_item 1 "val1"
ht.set_item 2 "val2"
ht.set_item 3 "val3"
-- 順次アクセス
enum = ht.GetEnumerator()
while enum.MoveNext() do
format "%: %\n" enum.Key enum.Value
-- 出力:
-- 3: val3
-- 2: val2
-- 1: val1
for文を使ってアクセス出来ればいいのですが、残念ながら対応していないようです。
代わりに、getEnumerator関数を使って順次アクセスオブジェクトを取得し、MoveNext関数で全ての要素を取得する事が出来ます。
MoveNextは取得できる要素がこれ以上無くなったときfalseを返すので、自動的にwhile文を抜けます。
ここで、出力の順序がインデックス順にはなっていない事が確認出来ます。
その他の機能
-- 要素数プロパティ
ht.Count
-- 指定した要素を削除
ht.Remove key
-- 全ての要素を削除
ht.Clear()
-- 要素が含まれているか確認する
ht.Contains key -- 指定したキーが存在すればtrue
ht.ContainsKey key -- Containsの別名
ht.ContainsValue val -- 指定した値が存在すればtrue
.NETで扱えないオブジェクトを使う
残念ながらMaxScript上の一部のオブジェクトは.NETでは使用できません。
特にノードやマテリアル等のシーンオブジェクトは、頻繁に使う上に代えが効きません。
ではそういったオブジェクトをハッシュテーブル上で使う方法が全く無いのかというと、そういうわけでもありません。
方法として
1. 実データは配列で管理し、配列のインデックスをハッシュに格納する
2. オブジェクトを一意に特定するハンドルを利用する
等があります。
2.の一意に特定するハンドルについては、ノードオブジェクトなら.handleプロパティ、それ以外のオブジェクト(モディファイヤ、コントローラ、マテリアル等)ではAnimHandleを利用できます。
どちらの場合でも、オブジェクトを一意に特定する整数値を取得する事ができます。
以下は1と2の両方を組み合わせて利用した例です。選択したオブジェクトをマテリアル毎にグループ化しています。
ht = dotNetObject "System.Collections.Hashtable"
nodes = #()
-- キーをマテリアルのハンドル、
-- 値をnodesのインデックスとしてハッシュテーブルを構築する
for sel in selection do
(
mtl = sel.material
mtlHandle = if mtl != undefined then GetHandleByAnim mtl else 0P
if ht.Contains mtlHandle then
(
nodeIdx = ht.get_item mtlHandle
append nodes[nodeIdx] sel
)
else
(
append nodes #(sel)
ht.set_item mtlHandle nodes.count
)
)
-- マテリアルスロット1のマテリアルが割り当てられた
-- オブジェクトがあれば、その一覧を取得する
mtlHandle = GetHandleByAnim meditMaterials[1]
if ht.Contains mtlHandle do
(
nodeIdx = ht.get_item mtlHandle
mtlObjs = nodes[nodeIdx]
-- 例として、mtlObjsは #($box001, $box002, ...) のような配列
)
-- マテリアルが割り当てられてないオブジェクトの一覧
if ht.Contains 0P do
(
nodeIdx = ht.get_item 0P
mtlObjs = nodes[nodeIdx]
)
注意点として、AnimHandleはそのプロセス内でしか一意性を保たないようになっている為、3dsMaxを再起動したりした場合はハンドルの値が変わるという事があります。
よって、ハンドルをファイルに保存するような使い方には適していません。
対してノードの.handleプロパティでは、同一のシーンオブジェクトであればセッション外であっても同一の値を保持するようです。
パフォーマンスについて補足
これはハッシュテーブルに限った話ではないのですが、何度も呼び出す関数は予めローカル変数に取り出しておく事で、関数呼び出しのパフォーマンスを向上させる事が出来ます。
ht = dotNetObject "System.Collections.Hashtable"
setItem = ht.set_item
for i = 1 to 100 do
setItem i "value"
enum = ht.GetEnumerator()
moveNext = enum.MoveNext
while moveNext() do
format "%: %\n" enum.Key enum.Value