ホーム < ゲームつくろー! < デザインパターン習得編
Iterator
〜リスト内のオブジェクトにアクセスする便利な方法
@ 配列へアクセスする怖さ
C言語で「配列」というと、Item[28]のような書き方をします。しかし、実はC言語に配列というのは無いそうで、単なるポインタ演算を利用しているだけなんだそうです。[]というのは、「配列要素演算子」という名前はついていますが、これは簡便法なんだとか。つまり、
*(Item+28) → Item[28]
ということです。確かに簡便になってます。
C言語の場合、配列要素演算子を用いて配列にアクセスする時に、配列として確保している範囲を乗り越えていとも簡単にアクセスできてしまいます。
int Ary[10];
Ary[20] = 100; // これは可能
当然のことながら、これは一般保護エラーです。このことから、C言語で配列を用いるときには、要素数に物凄く気を使う必要がありました。
Iteratorパターンは、この配列の要素数を管理する部分だけをクラスとして抜き出してしまい、配列の安全で便利なアクセス方法を提供してくれるパターンです。個人的にとても分かりやすいパターンだと思います。
A アイテムをリストで管理
RPGなどのアイテムを表すItemクラスのオブジェクトを格納するリストであるItemListクラスを作ります。ただし、これはダメな例です。
// ダメなリスト
class ItemList
{
protected:
Item* m_pItemAry; // アイテム配列
unsigend int m_CurNum; // 現在の参照番号
unsigned int m_Size; // 格納しているアイテムの数
public:
ItemList(){ m_pItemAry = new Item;}
void Add(Item); // アイテムを追加
void Remove(Item); // アイテムを削除
// アイテムを取得
Item Next(){
if(m_CurNum > m_size)
m_CurNum = 0;
return m_pItemAry[CurNum++];
}
};
このクラスでは格納したアイテムにアクセスするためにNext関数を用います。この関数内では、m_CurNumが配列の要素内ならば登録してあるアイテムを取得します。Next関数が呼ばれるたびに、m_CurNumが1つずつ増え、要素数よりも大きくなるとゼロに戻ります。これで、少なくともアクセス違反になることはありません。
しかし、時にはアイテム配列を逆順に取り出したいこともあるでしょうし、同じ配列に対して複数のアクセスをする必要にかられる事もあります。例えば、2人同時プレーで共通したアイテムボックスを使用する場合、このクラスでは双方の指している場所(カーソル位置)を管理できない事になります。
つまり、入れ物と探す人は別にした方が便利なことが多いのです。そこで、上のクラスは箱専用のクラスとして設計し直します。
// 改良リスト
class ItemList
{
protected:
Item* m_pItemAry; // アイテム配列
unsigned int m_Size; // 格納しているアイテムの数
ItemIterator* m_pIterator; // イテレーター配列
public:
ItemList(){
m_pItemAry = new Item;
m_pIterator = new ItemIterator;
}
void Add(Item); // アイテムを追加
void Remove(Item); // アイテムを削除
void GetSize(){ return m_Size:} // 要素数を取得
Item Get(unsigend int elem){ return m_pItemAry[elem]; // アイテムを取得
// イテレーターを生成
ItemIterator* CreateIterator(){
ItemIterator* tmp = new ItemIterator(this);
AddIterator(tmp);
return tmp;
}
protected:
// イテレーターを登録
void AddIterator(ItemIterator*);
};
class ItemIterator
{
protected:
ItemList* m_pList;
unsigned int m_Elem;
public:
ItemIterator(ItemList* plist){ m_pList = plist:} // コンストラクタでリストを登録
virtual void First(){ m_Elem = 0;} // 要素番号を先頭に戻す
// 要素番号を増加
virtual void Next(){
if(!IsDone())
m_Elem = 0;
else
m_Elem++;
}
// 現在の要素番号が正しい位置にあるかをチェック
bool IsDone(){
if(m_Elem > m_pList->GetSize())
return false;
return true;
}
// アイテムを取得
Item GetItem(){ return m_pList->Get(m_Elem);}
};
長々と書きましたが、ItemListは箱としての役目に徹し、代わりにItemIteratorというクラスのオブジェクトを生成するCreateIterator関数が作られました。ItemIteratorクラスは、要素の番号を専門に管理するクラスで、First関数、Next関数、IsDone関数そしてGetItem関数があります。IsDone関数は要素番号が正しい位置にあるかどうかをチェックします。GetItem関数は登録されたリストからアイテムを取り出して、クライアントに渡す役目をしています。
B イテレーターの利点
箱と探す人(イテレーター)に分けることで、どういう利点があるか?これは言うまでもなく、同じ箱から色々な探し方が出来るようになることにあります。ItemListクラスを派生させて、ItemListExクラスを作ったとします。このクラスでは逆方向の検索も出来るようにしたいと考えています。
class ItemListEx
{
public:
CreateRevIterator(){
Iterator* tmp = new RevItemIterator(this);
AddIterator(tmp);
return tmp;
}
};
RevItemIteratorクラスはItemIteratorクラスを派生したもので、仮想関数をオーバーライドして、逆順の走査を可能にしています。
class RevItemIterator
{
public:
RevItemIterator(ItemList* plist):m_pList(plist){};
virtual void First(){ m_Elem = m_pList->GetSize()-1;} // 最後尾にセット
// 一つ前の要素番号に変更
virtual void Next(){
if(m_Elem == 0)
First();
else
m_Elem--;
}
};
こういう拡張がいくらでも出来るわけです。もっと発展した操作法としては、特定のアイテムだけを次々と指していくイテレーターなども実装できることになります(アイテムを指定するために、ItemListにも変更が必要になりますが)。
Iteratorパターンのクラス図を示します。
アイテムクラスとそのイテレーターで置き換えます。
D C++ならSTLを使おう
ここまで説明はしてきて何なのですが、実はこのIteratorパターンはC++のSTL(標準テンプレートライブラリ)でiteratorとしてそのままズバリ採用されています。よって、通常のリストを使う分には自作するよりはSTLのlistを使った方が安全面と効率面で良いでしょう。ただ、STLがない他の言語では十分に使えますし、この「データソースと検索を分ける」という考え方は、STL以外の部分でも使えます。当てはまりそうなところが出てきたら、使ってみるのも良いかもしれません。