ハッシュテーブル(辞書・連想配列)を使う

投稿者: | 2016/12/21

今回はMaxScriptでハッシュテーブル(or 辞書 or 連想配列)を利用する方法について。
ハッシュテーブルを利用する上で、必要になるであろう機能を一通りまとめました。

ハッシュテーブルとは

ハッシュテーブルは配列のように複数の子を保持出来る、いわゆるコレクションの一種で、言語によっては辞書や連想配列と呼ばれています。
配列との最大の違いは、数値以外の様々なデータをキーとして使用する事が出来る点です。

以下はイメージですが、こういった感じで文字列や、その他様々なデータをキーに使用する事が出来ます。

-- 配列要素へのアクセスの例
-- キーは1以上の整数のみ
ary[1]

-- ハッシュテーブル要素へのアクセスのイメージ
-- 数値以外もキーに出来る
hash["key"]

残念ながらMaxScriptにはハッシュテーブルが無く、標準では上記のようなアクセスは出来ないのですが…。

また配列ではデータは連続した領域に保存されているのに対し、ハッシュテーブルではキーと値のペアをランダムな領域に保存するような仕組みになっています。

例えば配列に対し ary[2500] = 1 のような代入を行った場合、配列の1番から2500番までの全ての領域が確保され、2500番のみに数値が代入されます。
対して、ハッシュテーブルの場合は単にキー(2500)と値(1)のペアを記憶するだけなので、無駄な領域は発生しません。

しかも、キーの検索はどんなにデータ量が増えても、常に一定の時間になるという特徴があり、そのおかげで膨大なデータから頻繁に検索するような場面では、配列より遙かに高いパフォーマンスを発揮する事が出来ます。

MaxScriptでハッシュテーブルを利用する

それでは、実際にMaxScriptでハッシュテーブルを利用する方法を紹介します。
標準では利用出来ませんが、例によって.NetにはHashTableクラスが用意されています。

-- ハッシュテーブルの初期化
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

値の取得ではget_itemとItem[]の2種類のアクセス法があるのですが、逆に値の設定ではItem[key] = valといった書き方は出来ません。
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では使用できません。
特にノードやマテリアルといったシーンオブジェクトは、頻繁に使う上に代えが効きません。

そういったオブジェクトを.NETで使用する場合、以下のような代替方法があります。

  • 実データは配列で管理し、配列のインデックスをハッシュに格納する
  • オブジェクトを一意に特定するIDを代わりに格納する

IDについては、ノードオブジェクトなら.handleプロパティ、それ以外のオブジェクト(モディファイヤ、コントローラ、マテリアル等)ではAnimHandleを利用できます。
どちらの場合でも、オブジェクトを一意に特定する整数値を取得する事ができます。

こういったID値については、過去の記事でも紹介しています。

以下は1と2の両方を組み合わせて利用した例です。
少々複雑ですが、選択したオブジェクトをマテリアル毎にグループ化しています。

ht = dotNetObject "System.Collections.Hashtable"
nodes = #()

-- ハッシュテーブルを構築する
-- キー: マテリアルのID
-- 値 : nodesのインデックス
for sel in selection do
(
    mtl = sel.material
    mtlHandle = if mtl != undefined then GetHandleByAnim mtl else 0

        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 0
    mtlObjs = nodes[nodeIdx]
)

パフォーマンスについて補足

これはハッシュテーブルに限った話ではないのですが、何度も呼び出す関数は予めローカル変数に取り出しておく事で、関数呼び出しのパフォーマンスを向上させる事が出来ます。

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

コメントを残す

メールアドレスが公開されることはありません。