2018年10月11日木曜日

MQL4 で可変長配列を使用する方法

2019/4/12 追記
[CArray](https://www.mql5.com/en/docs/standardlibrary/datastructures/carray) 系クラスの中で使用されているメモリの動的確保について書きました。
[MQL4 で配列用メモリを動的確保する方法 | Strategy of C](https://strategyofc.blogspot.com/2019/04/mql4.html)
場合によっては、こちらを直接使った方が良いかもしれません。
@MetaTrader 4 (Version 4.00 Build 1090) MQL4 では C++ の [std::vector](http://www.cplusplus.com/reference/vector/vector/) のような動的配列が標準で使用できますBuild 600 で入った変更を受けて作成された模様。 `Include/Arrays/` 以下に定義されており、#include <Arrays/ArrayObj.mqh> のように include するだけで使用可能です。 今回はその中の [CArrayObj](https://www.mql5.com/en/docs/standardlibrary/datastructures/carrayobj) について簡単な使い方を書こうと思いますが、 [CArrayInt](https://www.mql5.com/en/docs/standardlibrary/datastructures/carrayint)、[CArrayChar](https://www.mql5.com/en/docs/standardlibrary/datastructures/carraychar) 等他の class も基本の思想は同じです。 公式のリファレンスを探したのですが、 MQL5 用のものしか見つけられませんでした。 ただ、確認した限り、MQL4 でも同様に使用できるようですMQL5 から逆移植されたものと思われる。 > 参考 > > [Standard Library / Data Collections - Reference on algorithmic/automated trading language for MetaTrader 5](https://www.mql5.com/en/docs/standardlibrary/datastructures) なお、完全に動作するソースコードは最後に載せておきます。 ### 配列に挿入可能な class `CArrayObj` に追加可能なオブジェクトは [CObject](https://www.mql5.com/en/docs/standardlibrary/cobject) を継承している必要があります。 また、`Sort()` メソッドを使用したい場合、`Compare()` を Override しないと正しく動作しないので注意が必要です。 後ほど書きますが `Search()` 等の一部のメソッドは `Sort()` 済みでないと動作しません。そのため、そういったメソッドを使用するためにも `Compare()` は必要です。 ```mq4 `first-line: 7; class Item : public CObject { // 省略 // この Compare を「正しく」Orverride していないと // Sort() が出来ない。結果 Search() も出来ない。 virtual int Compare(const CObject *node, const int mode=0) const { int diff = this.GetValue() - dynamic_cast(node).GetValue(); return (diff == 0) ? 0 : (diff > 0) ? 1 : -1; } }; ``` ### 追加、挿入 配列の末尾に追加するには `Add()`, 途中に挿入するには `Insert()` を使用します。 ```mq4 `first-line: 37; // 末尾に追加 for (int i = 0; i < 10; i++) { int value = (i % 2 == 0) ? i : i + 10; m_item_array.Add(new Item(value)); } // 途中に挿入 Item* item_100 = new Item(100); m_item_array.Insert(item_100, 5); ``` 上記を実行した後の配列の状態 ```text i:0, value:0 i:1, value:11 i:2, value:2 i:3, value:13 i:4, value:4 i:5, value:100 i:6, value:15 i:7, value:6 i:8, value:17 i:9, value:8 i:10, value:19 ``` > 参考 > > [[MQL4] CArrayObj::Insert() に最大値以上の index を指定すると Add と同じ動作になる](https://strategyofc.blogspot.com/2018/10/mql4-carrayobjinsert-index-add.html) ### 特定位置のオブジェクトを取得する インデックスがわかっている場合は `At()` を使います厳密には `dynamic_cast` を使用するべきだが、こういうケースでは無理に使う必要はないと思う ```mq4 Item* item = m_item_array.At(i); ``` ### あるオブジェクトのインデックスを調べる 特定のオブジェクトが配列に含まれるか、含まれているならどの位置なのか調べるには `Search()` メソッドを使用します。 この `Search()`、ソート済みでない配列に対して呼ばれた場合、`-1` を返すので注意が必要です。 また、`Search()` が調べるのは、オブジェクトのリファレンスではなく、`Compare()` が 0 を返すかどうかです。 そのため、同じ `value` を持つオブジェクトが複数ある場合、想定とは違った動作になる可能性があります。 ```mq4 `first-line: 53; // 特定の Item の index を調べる // Sort 済みの配列でないと Search の返り値が -1 になるので注意 m_item_array.Sort(); int index = m_item_array.Search(item_100); ``` なお、上記の `index` は 10 になります。ソートしたら `value: 100` は最後にくるので。 ```text index_100 i:10 ``` > 参考 > > - [[MQL4] CArrayObj はソート済みじゃないと Search() の結果が -1 になってしまう](https://strategyofc.blogspot.com/2018/10/mql4-carrayobj-search-1.html) > - [[MQL4] CArrayObj::Search() が想定外のインデックスを返してくることがある](https://strategyofc.blogspot.com/2018/10/mql4-carrayobjsearch.html) ### 配列から削除する 配列から削除する方法は大きく分けて2つあります。 1つは単に配列から取り除くだけの `Detach()`。 配列から消えるだけなので、メモリの解放をしてやらないとメモリリークになります。 ```mq4 `first-line: 58; // index を指定して配列から除く m_item_array.Detach(index); // Detach は Item を配列から除くだけなので、自前で delete する必要がある delete item_100; // delete したら NULL 入れておいた方が安全(今回は無意味だが) item_100 = NULL; ``` 上記を実行した後の配列の状態。 ```text i:0, value:0 i:1, value:2 i:2, value:4 i:3, value:6 i:4, value:8 i:5, value:11 i:6, value:13 i:7, value:15 i:8, value:17 i:9, value:19 ``` もう1つは、配列から除くと同時に `delete` も行ってくれる `Delete()`。 こちらの方が便利ですが、別の場所でリファレンスを持ってる場合、重大なエラーになるので注意が必要です。 ```mq4 `first-line: 72; for (int i = m_item_array.Total() - 1; i >= 0; i--) { Item* item = m_item_array.At(i); // Delete は Detach と同時に delete も行ってくれる m_item_array.Delete(i); } ``` 上記を実行すると配列は空になります。 これは、`Clear()` メソッドを実行するのと等価ですが、今回はあえてループで書いてみました。 ### 全ソースコード ```mq4 `title: "Script/ArrayObjTest.mq4"; #property strict #include <Object.mqh> #include <Arrays/ArrayObj.mqh> // 実際は別ファイルで定義した方が良いでしょう class Item : public CObject { private: const int m_value; public: Item(int value) : m_value(value) { } virtual ~Item() { } int GetValue() const { return m_value; } // この Compare を「正しく」Orverride していないと // Sort が出来ない。結果 Search も出来ない。 virtual int Compare(const CObject *node, const int mode=0) const { int diff = this.GetValue() - dynamic_cast(node).GetValue(); return (diff == 0) ? 0 : (diff > 0) ? 1 : -1; } }; CArrayObj m_item_array; void OnStart() { int file_handle = FileOpen("result.txt", FILE_TXT|FILE_WRITE); // 本当はファイルが開けなかった時のエラーハンドリングが必要だが今回は省略 // 末尾に追加 for (int i = 0; i < 10; i++) { int value = (i % 2 == 0) ? i : i + 10; m_item_array.Add(new Item(value)); } // 途中に挿入 Item* item_100 = new Item(100); m_item_array.Insert(item_100, 5); FileWriteString(file_handle, "============\n"); for (int i = 0; i < m_item_array.Total(); i++) { Item* item = m_item_array.At(i); FileWriteString(file_handle, StringFormat("i:%d, value:%d\n", i, item.GetValue())); } // 特定の Item の index を調べる // Sort 済みの配列でないと Search できないので注意 m_item_array.Sort(); int index = m_item_array.Search(item_100); // index を指定して配列から除く m_item_array.Detach(index); // Detach は Item を配列から除くだけなので、自前で delete する必要がある delete item_100; // delete したら NULL 入れておいた方が安全(今回は無意味だが) item_100 = NULL; FileWriteString(file_handle, "============\n"); for (int i = 0; i < m_item_array.Total(); i++) { Item* item = m_item_array.At(i); FileWriteString(file_handle, StringFormat("i:%d, value:%d\n", i, item.GetValue())); } for (int i = m_item_array.Total() - 1; i >= 0; i--) { Item* item = m_item_array.At(i); // Delete は Detach と同時に delete も行ってくれる m_item_array.Delete(i); } // ↑これは m_item_array.Clear(); を実行するのと等価 FileWriteString(file_handle, "============\n"); FileWriteString(file_handle, StringFormat("Total: %d\n", m_item_array.Total())); FileClose(file_handle); } ```