オブジェクト指向プログラミング
Dmitry Fedoseev | 2 10月, 2015
はじめに
オブジェクト指向プログラミング(OOP)について学ぼうとしたことがある人は、まず 多相性, カプセル化, オーバーロード and 継承といった言葉に出会ったと思います。既製のクラスを見て多相性やカプセル化といったものがどこにあるか探した人もいるかもしれません。これがOOPを学ぶ過程の最後となってしまうことが多いです。
実際は、見かけよりもシンプルなものです。OOPを使うためにこれらの言葉を知っている必要はありません。たとえその名前を知らなくてもOOPの機能であるそれらを使うだけで良いのです。それでもなお、この記事を読む人にはOOPの使い方だけではなく、それらの言葉の意味も理解しては欲しいですが。
関数のライブラリの作成
最もシンプルで最初にOOPで作るのは自分が頻繁に使う関数のライブラリでしょう。もちろん、インクルードファイル(mqh)に関数を保存することも可能です。関数が必要な時は、ファイルをインクルードしこの関数を呼び出すことができます。しかし、プログラミングを長くやっていると大量の関数を扱うことになり、その名前と目的を把握しておくのは難しくなります。
関数を異なるファイルに入れ、目的に応じたカテゴリーに分けることができます。例えば、配列を扱う関数、文字列を扱う関数、オーダーを計算する関数等です。”カテゴリー”は”クラス”と言い換えることもできます。意味は同じですが、オブジェクト指向プログラミングというトピックにより近くなります。
関数はクラスによって分けることができます:配列を扱う関数、文字列を扱う関数、オーダーに関する関数等です。”クラス”という言葉はOOPの根本的な概念なのでより近づきます。関連書籍や 辞書、Wikipedia) などの百科事典で”プログラミングにおけるクラス”が何を意味するのかを検索することができます。
おそらく、第一印象は”多相性”や”カプセル化”などという言葉を聞いた時と同じではないでしょうか。この場では、”クラス”とは関数と変数の集合という意味で使います。クラスを使いライブラリを作成する場合 - 処理されるデータやオブジェクトのタイプ(配列、文字列、オーダー)によりグループ分けされた関数や変数の集合。
プログラム内のプログラム
フォーラムには同じような質問がありました(これからもあるでしょう)- Expert Advisorのスクリプトを呼び出すにはどうしたらいいのか?とサードパーティのツールを使わない場合、このタスクはスクリプトコードをExpert Advisorに入れることで達成されます。実際、これは難しいタスクではありません。スクリプトはEAと同じ変数や関数を使うため、スクリプトのコードを調整しなければいけません。変更は複雑ではありませんが、大量になる可能性があります。
このスクリプトは独立した別々のプログラムとして呼び出すのが良いでしょう!スクリプトをクラスとしてプログラムし、そのクラスを使用することで可能です。そのことで増えるのは数列のコードだけです。この場合、クラスは関数を処理するデータのタイプではなく、その目的によってまとめます。例:保留されたオーダーを削除するクラス、ポジションと取ったりオーダーを出すクラス、グラフィカルオブジェクトを扱うクラス、など。
クラスの重要な特徴はそれが置かれた場所とは区別されるということです。クラスはOSで動くプログラムのようなものです:複数のプログラムが同時に動きますが、それぞれ独立して動いています。クラスは置かれている場所とは区別されるため”プログラム内のプログラム”と呼ぶことができます。
クラスを見てみる
クラスの作成はclassと言う単語で始まり、引き続きクラス名、そしてすべてのコードが{}内に置かれます:
class CName { // Here is the entire code of the class };
注意!閉じ括弧のあとにセミコロンを入れるのを忘れないでください。
表示と非表示(カプセル化)
どんなプログラムであっても、様々な関数を含みます。これらの関数は2種類に分けられます:メインと補助です。メインの関数はプログラムが実際、構成されているものです。これらの関数はユーザーが知る必要のない多くの関数を必要とします。例えば、クライアントターミナルでポジションを取るには、ユーザーは新しい注文ダイアログを開き、数量を入力し、ストップロスや利食いポイントを決定して”買う”や”売る”をクリックします。
しかし、ボタンをクリックしてから実際にポジションが取られるまでに起こっているのは-ターミナルの開発者のみがきちんと理解していることです。ターミナルは多くのアクションを起こしているはずです:ポジションのボリュームやストップロスと利食いの値、ネットワーク接続などの確認など。非常に多くの手順が隠れて、あるいはカプセル化されています。同じようにクラスの中でコードを小さく分けることができます (関数と変数) -その一部はクラスを使う時に使えるようになったり、隠れたままだったりします。
カプセル化のレベルは次のキーワードで定められます: プライベート, プロテクテッド や パブリック。プロテクテッドとプライベートの違いは-後ほど触れます。まずは、プライベートとパブリックについて見てみましょう。シンプルなクラステンプレートは以下のようになります:
class CName { private: // Variables and functions available only inside the class public: // Variables and functions available outside the class };これでもOOPの利点を使うチャンスです。ExperAdvisorのディレクトリに直接コードを書くのではなく、まずクラスを作成しそこにすべてのコードを書きます。次に、 プライベートとパブリックの違いを基本的な例を使い見てみます。
ライブラリ作成の例
上記のクラステンプレートを使い関数のライブラリを作成します。配列を扱うクラスを作成しましょう。配列を使う際の最も一般的なタスク - 配列に存在しない新しい要素を追加することです。
配列に新しい要素を追加する関数をAddToEnd()と呼び、配列に独自の要素を追加する関数をAddToEndIfNotExists()と呼びましょう。関数AddToEndIfNotExists()では、まず追加された要素が配列に既に存在しているかを確認する必要があります。もしなければ関数AddToEnd() functionを使い要素を追加します。配列に要素が既に存在しているかを確認する関数は、補助的なものと考えられているため、プライベートセクションに置き、その他の関数はパブリックセクションに置かれます。その結果、クラスは以下のようになります:
class CLibArray { private: // Check if an element with required value exists in array int Find(int &aArray[],int aValue) { for(int i=0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { return(i); // Element exists, return index of element } } return(-1); // No such element, return -1 } public: // Add to end of array void AddToEnd(int &aArray[],int aValue) { int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+1); aArray[m_size]=aValue; } // Add to end of array if there is no such value in array already void AddToEndIfNotExistss(int &aArray[],int aValue) { if(Find(aArray,aValue)==-1) { AddToEnd(aArray,aValue); } } };
クラスのロード
クラスを使うためには、まずロードされていなければいけません。クラスが別のファイルに置かれている場合は、このファイルをインクルードしなければなりません
#include <OOP_CLibArray_1.mqh>。
そしてこのクラスをロードします。クラスのロードは変数の宣言と似ています:
CLibArray ar;
まずクラスの名前、そしてこのインスタンスを参照するポインターの名前。ロードされたクラスはオブジェクトになります。オブジェクトの関数を使う場合は、ポインター名、.、関数名の順番で書きます。”.”をタイプするとドロップダウンリストが開きます(図 1)。
図 1. 関数のリスト
ドロップダウンリストのおかげで、関数の名前を覚える必要はありません - 関数のリストを見ながらその関数の目的を思い出せばよいのです。これが関数をファイルに格納するよりもクラスを使い、ライブラリを作成した場合の最大の利点です。
関数を探すとき、ドロップダウンリストで関数の最初の数文字のアルファベットを入力すると、ライブラリに含まれるすべての関数のうち対応するものが表れます。クラスを使う時、そのクラスに関係するクラスだけが表れます。Find()関数が入っていないことにお気づきでしょうか-これがプライベートとパブリックセクションの違いです。この関数はプライベートセクションにあるので利用することができません。
異なるデータタイプのユニーバーサルライブラリの作成(オーバーロード)
この時点では、ライブラリの関数はint 型の配列しか扱うことができません。int型の配列に加え、uintや long、 ulong などの配列を扱うライブラリ関数が必要になるかもしれません。他の型の配列を使うためにはそれぞれ関数を書かなければなりません。しかし、これらの関数にそれぞれ名前を付ける必要はありません -渡されたパラメータによって自動的に正しい関数が選択されます(今回はパラメータの型)long型の配列を扱う関数を持つクラスを実行してみましょう:
class CLibArray { private: // Для int. Check if an element with required value exists in array int Find(int &aArray[],int aValue) { for(int i=0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { return(i); // Element exists, return index of element } } return(-1); // No such element, return -1 } // For long. Check if an element with required value exists in array int Find(long &aArray[],long aValue) { for(int i=0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { return(i); // Element exists, return index of element } } return(-1); // No such element, return -1 } public: // For int. Add to end of array void AddToEnd(int &aArray[],int aValue) { int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+1); aArray[m_size]=aValue; } // For long. Add to end of array void AddToEnd(long &aArray[],long aValue) { int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+1); aArray[m_size]=aValue; } // For int. Add to end of array if there is no such value in array already void AddToEndIfNotExistss(int &aArray[],int aValue) { if(Find(aArray,aValue)==-1) { AddToEnd(aArray,aValue); } } // For long. Add to end of array if there is no such value in array already void AddToEndIfNotExistss(long &aArray[],long aValue) { if(Find(aArray,aValue)==-1) { AddToEnd(aArray,aValue); } } };同じ名前ですが、異なる機能を持つ関数を使っています。これらの関数はオーバーロードされていると呼ばれます。1つの名前で異なる機能がある(オーバーロード)からです。
この記事に添付されているOOP_CLibArray_1.mqhファイルで、具体例を見ることができます。
クラス表記の別の方法
上記の例では、関数はクラス内に書かれていました。関数がそれぞれ大きい場合、そういった表記の仕方はあまり便利ではありません。この場合、関数をクラスの外に書くことができます。クラス内には関数名とパラメータ名だけを書いて、関数自体は完全にクラスの外側に書くことができます。また、関数が属するクラスを示さなければいけません: まずクラス名を書き、2つのコロンと関数名を書きます。
class CLibArray { private: int Find(int &aArray[],int aValue); int Find(long &aArray[],long aValue); public: void AddToEnd(int &aArray[],int aValue); void AddToEnd(long &aArray[],long aValue); void AddToEndIfNotExistss(int &aArray[],int aValue); void AddToEndIfNotExistss(long &aArray[],long aValue); }; //--- int CLibArray::Find(int &aArray[],int aValue) { for(int i=0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { return(i); } } return(-1); } //--- int CLibArray::Find(long &aArray[],long aValue) { for(int i=0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { return(i); } } return(-1); } //--- void CLibArray::AddToEnd(int &aArray[],int aValue) { int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+1); aArray[m_size]=aValue; } //--- void CLibArray::AddToEnd(long &aArray[],long aValue) { int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+1); aArray[m_size]=aValue; } //--- void CLibArray::AddToEndIfNotExistss(int &aArray[],int aValue) { if(Find(aArray,aValue)==-1) { AddToEnd(aArray,aValue); } } //--- void CLibArray::AddToEndIfNotExistss(long &aArray[],long aValue) { if(Find(aArray,aValue)==-1) { AddToEnd(aArray,aValue); } }
この表記方法を使えばクラス全体を見ることもでき、それぞれの関数を詳しく見ることもできます。
この記事に添付されているOOP_CLibArray_2.mqhファイルで、具体例を見ることができます。
クラス内で変数を宣言する
先ほどあげた具体例を見ていきましょう。ファイルに直接書くのとクラス内に書くのとでは1つの違いがあります。直接ファイルに書く場合は、宣言する時に変数に値を割り当てることができます:
int Var = 123;
変数をクラス内で宣言する場合はできません。 -値はクラスの関数が実行される時に割り当てられなければいけません。ですので、まずはパラメータをクラスに渡さなければいけません (例、クラスを使う準備をする)。この関数をInit()と名付けましょう。
基本的な例で見てみます。
スクリプトをクラスに変換する具体例
未決のオーダーを削除するスクリプトだと考えてください( 添付のOOP_sDeleteOrders_1.mq5 fileファイルを見て下さい)。
// Include file to use the CTrade class from standard delivery #include <Trade/Trade.mqh> // External parameters // Select symbol. true - delete orders for all symbols, // false - only for symbol of chart, where the script is running input bool AllSymbol=false; // Select types of orders to delete input bool BuyStop = false; input bool SellStop = false; input bool BuyLimit = false; input bool SellLimit = false; input bool BuyStopLimit = false; input bool SellStopLimit = false; // Load the CTrade class CTrade Trade; //--- void OnStart() { // Variable to check function result bool Ret=true; // Loop by all orders in terminal for(int i=0; i<OrdersTotal(); i++) { ulong Ticket=OrderGetTicket(i); // Select order and get its ticket // Successfully selected if(Ticket>0) { long Type=OrderGetInteger(ORDER_TYPE); // Check order type if(Type == ORDER_TYPE_BUY_STOP && !BuyStop) continue; if(Type == ORDER_TYPE_SELL_STOP && !SellStop) continue; if(Type == ORDER_TYPE_BUY_LIMIT && !BuyLimit) continue; if(Type == ORDER_TYPE_SELL_LIMIT && !SellLimit) continue; if(Type == ORDER_TYPE_BUY_STOP_LIMIT && !BuyStopLimit) continue; if(Type == ORDER_TYPE_SELL_STOP_LIMIT && !SellStopLimit) continue; // Check symbol if(!AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue; // Delete if(!Trade.OrderDelete(Ticket)) { Ret=false; // Failed to delete } } // Failed to select order, unknown result, // function ended up with error else { Ret=false; Print("Error selecting order"); } } if(Ret) { Alert("Script ended successfully"); } else { Alert("Script ended up with error, see details. in Journal"); } }
スクリプトには外部パラメータがあり、多様な注文と削除される注文の選択を可能にします。(スクリプトが実行しているチャートの全てのシンボルあるいはシンボル)。()
このスクリプトをCOrderDeleteというクラスに変換します。プライベートセクションで、スクリプトで外部パラメータと宣言されている変数と同じ変数を、それぞれ頭にm_をつけて宣言しましょう(mはmemberのmです。クラスのmemberであるため。)接頭辞は必須ではありませんが、変数の違いを見分けやすくなるのでとても便利です。このように、クラスのスペースで制限された変数を扱っているということがよくわかります。さらに、グローバルスコープで宣言された変数を隠す変数宣言であるというコンパイラー警告が出ることもありません。
クラス定義の関数本体での、グローバルスコープで同じ変数名の使用はエラーではありませんが、プログラムを理解しにくくしします。これがコンパイラー警告が出る理由です。変数に値を割り当てるには、これらの変数に対応するパラメータ(そしてスクリプトの外部パラメータ)を持つ Init()関数を書きます。このクラスを使う場合、まずはInit()関数関数を呼び出し、外部パラメータを渡します。残りのスクリプトコードは変わりません。唯一の違いは- 直接外部パラメータを使うのではなく、クラス内で宣言された変数を使うということです。
その結果、クラスは以下のようになります:
#include <Trade/Trade.mqh> class COrderDelete { private: // Variables for parameters bool m_AllSymbol; bool m_BuyStop; bool m_SellStop; bool m_BuyLimit; bool m_SellLimit; bool m_BuyStopLimit; bool m_SellStopLimit; // Load the CTrade class CTrade m_Trade; public: // Function to set parameters void Init(bool aAllSymbol,bool aBuyStop,bool aSellStop,bool aBuyLimit,bool aSellLimit,bool aBuyStopLimit,bool aSellStopLimit) { // Set parameters m_AllSymbol =aAllSymbol; m_BuyStop =aBuyStop; m_SellStop =aSellStop; m_BuyLimit =aBuyLimit; m_SellLimit =aSellLimit; m_BuyStopLimit =aBuyStopLimit; m_SellStopLimit=aSellStopLimit; } Main function to delete orders bool Delete() { // Variable to check function result bool m_Ret=true; // Loop by all orders in terminal for(int i=0; i<OrdersTotal(); i++) { // Select order and get its ticket ulong m_Ticket=OrderGetTicket(i); // Successfully selected if(m_Ticket>0) { long m_Type=OrderGetInteger(ORDER_TYPE); // Check order type if(m_Type == ORDER_TYPE_BUY_STOP && !m_BuyStop) continue; if(m_Type == ORDER_TYPE_SELL_STOP && !m_SellStop) continue; if(m_Type == ORDER_TYPE_BUY_LIMIT && !m_BuyLimit) continue; if(m_Type == ORDER_TYPE_SELL_LIMIT && !m_SellLimit) continue; if(m_Type == ORDER_TYPE_BUY_STOP_LIMIT && !m_BuyStopLimit) continue; if(m_Type == ORDER_TYPE_SELL_STOP_LIMIT && !m_SellStopLimit) continue; // Check symbol/s61> if(!m_AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue; // Delete if(!m_Trade.OrderDelete(m_Ticket)) { m_Ret=false; // Failed to delete } } // Failed to select order, unknown result, // function ended up with error else { m_Ret=false; Print("Error selecting order"); } } // Return function result return(m_Ret); } };この記事に添付されているOOP_CDeleteOrder_1.mqhファイルで、具体例を見ることができます。このスクリプトを使うクラスは最小化されています。(外部パラメータ、クラスのロード、Init()メソッドとDelete()メソッドの呼び出し):
// External parameters // Select symbol. true - delete orders for all symbols, // false - only for symbol of chart, where the script is running input bool AllSymbol=false; // Select types of orders to delete input bool BuyStop = false; input bool SellStop = false; input bool BuyLimit = false; input bool SellLimit = false; input bool BuyStopLimit = false; input bool SellStopLimit = false; // Include file with class #include <OOP_CDeleteOrder_1.mqh> // Load class COrderDelete od; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { // Pass external parameters to the class od.Init(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit); // Delete orders bool Ret=od.Delete(); // Process result of deleting if(Ret) { Alert("Script ended successfully"); } else { Alert("Script ended up with error, see details in Journal"); } }
この記事に添付されている OOP_sDeleteOrders_2.mq5ファイルで、具体例を見ることができます。ほとんどのスクリプトはDelete()関数の結果を処理し、結果を通知します。
これですべての基本的な関数は別のファイルにクラスとして置かれていますので、他のプログラム(Expert Advisorやスクリプト) で使うことができます。すなわち、Expert Advisorでこのスクリプトを呼び込む。
オートメーション理論を少しだけ(コンストラクタとデストラクタ)
プログラムのオペレーションは3つのフェーズに分けられます。プログラムの起動、プログラムの動作、動作の完成。この分割の重要性は明らかです: プログラムが開始すると自身の準備を始めます (ロードし、扱うパラメータを設定したり)、プログラムが終了する時には、”掃除”をしなければなりません(チャートからのグラフィカルオブジェクトの削除など)。
このステージを分割するためにExpert Advisorとインディケーターには特別な機能があります: OnInit() (スタートアップ時に実行されます)とOnDeinit()(シャットダウン時に実行されます)。 クラスにも似た機能があります:クラスがロードした時とアンロードされた時に自動的に実行される関数を追加することができます。これらの関数はコンストラクタとデストラクタと呼ばれています。クラスにコンストラクタを追加するということは、全く同じ名前のクラスを追加するのと同じことです。デストラクタを追加するには - コンストラクタと同じようにやりますが、関数名は”~”で始めなければいけません。
コンストラクタとデストラクタのデモンストレーション
// Class class CName { public: // Constructor CName() { Alert("Constructor"); } // Destructor ~CName() { Alert("Destructor"); } void Sleep() { Sleep(3000); } }; // Load class CName cname; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { // Pause cname.Sleep(); }
クラスにはSleep()とい3秒間止まる関数があります。スクリプトを実行すると”コンストラクタ”というメッセージが出て3秒間のポーズの後、”デストラクタ”というアラートウインドウが表示されます。これはCName()と~CName()関数が明確に呼び出されなかったにも関わらずです。
この記事に添付されている OOP_sConstDestr_1.mq5ファイルで、具体例を見ることができます。
パラメータのコンストラクタへの受け渡し
例ではスクリプトをコンストラクタへ変換しています。コードをまだ1列減らすことができます。 - ()関数の呼び出しを無くすことができます。クラスがロードされるとパラメータはコンストラクタに渡されます。コンストラクタのクラスへの追加:
COrderDelete(bool aAllSymbol = false, bool aBuyStop = false, bool aSellStop = false, bool aBuyLimit = false, bool aSellLimit = false, bool aBuyStopLimit = false, bool aSellStopLimit=false) { Init(aAllSymbol,aBuyStop,aSellStop,aBuyLimit,aSellLimit,aBuyStopLimit,aSellStopLimit); }
The Init()関数はそのままですが、コンストラクタから呼び出されました。 クラスが以前のまま使えるように、コンストラクタのすべてのパラメータはオプションです:パラメータの無いクラスをロードし、Init() 関数を呼び出す。
コンストラクタを作成後は、クラスにもう1つの使い方があります。クラスがロードされると、Init() 関数を呼び出さずにパラメータを渡すことができるようになります:
COrderDelete od(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);
クラスの再初期化のため、Init()関数はパブリックセクションに置いたままにします。プログラムを使う場合 (Expert Advisor)、ストップオーダーや、リミットオーダーなど1つだけを削除しなれけばならない場合があります。この場合は、Init()関数を異なるパラメータで呼び出し、それぞれをDelete()関数で削除できるようにします。
この記事に添付されているOOP_CDeleteOrder_2.mqhとOOP_sDeleteOrders_3.mq5ファイルで、具体例を見ることができます。
クラスの複数のインスタンスの使用
前のセクションで述べたように、初期化中に設定されたパラメータにより、同じクラスでも異なることを実行できます。クラスがどういった目的で使われるかが分かっている場合は、クラスの再初期化を省略することができます。そのためには、異なるパラメータを持つクラスをいくつかロードする必要があります。
例えば、EAが実行中は、買い逆指値 や買い指値オーダーを削除しなければならない時があります。 - 売り逆指値 や売り指値オーダーの場合もあるでしょう。この場合、クラスのインスタンスを2つロードすることができます。
買い逆指値 や買い指値オーダーの削除:
COrderDelete DeleteBuy(false,true,false,true,false,false,false);
売り逆指値 や売り指値オーダーの削除:
COrderDelete DeleteSell(false,false,true,false,true,false,false);
買いの指値注文を削除する場合はクラスの1つのインスタンスを使います:
DeleteBuy.Delete();
売りの指値注文を削除する場合はクラスのもう1つのインスタンスを使います:
DeleteSell.Delete();
オブジェクトの配列
プログラムの実行中にどれくらいのクラスインスタンスが必要になるか分からない場合もあります。この場合、クラスのインスタンス(オブジェクト)の配列を作成することができます。コンストラクタとデストラクタのあるこの例を見てみましょう。少しだけクラスを変えてみましょう。クラスのインスタンスそれぞれをモニターできるようにパラメータをコンストラクタに渡します:
// Class class CName { private: int m_arg; // Variable for the instance public: // Constructor CName(int aArg) { m_arg=aArg; Alert("Constructor "+IntegerToString(m_arg)); } // Destructor ~CName() { Alert("Destructor "+IntegerToString(m_arg)); } //--- void Sleep() { Sleep(3000); } };このクラスを使いましょう。あるサイズの配列を宣言できます。例えば10個の要素というように。
CName* cname[10];
通常の変数の宣言との違いが1つあります- アスタリスクです ”*”。アスタリスクにより、前回の自動ポインタとは違い動的なポインタが使われていることが分かります。
動的な配列を使うことができます(事前にサイズを決める必要がないもの。動的な配列と動的なポインタを間違えないでください)
CName* cname[];
この場合はスクリプト内でのスケーリングを必要とします(どの関数の中でも、スクリプトの中で - OnStart() 関数内で-実行されます):
ArrayResize(cname,10);
ここで、配列の全ての要素をループし、それぞれのクラスインスタンスをその各々にロードしましょう。これには新しい キーワードを使います:
ArrayResize(cname,10); for(int i=0; i<10; i++) { cname[i]=new CName(i); }ポーズ:
cname[0].Sleep();
スクリプトをチェック。実行し、コンストラクタは10個あるが、デストラクタは1つもないことを確認してください。動的なポインタを使う場合は、プログラムが終了してもクラスが自動的にアンロードされません。それに加え、"Experts" タブでメモリーリークについてのメッセージをみるかもしれません。オブジェクトは手動で削除してください:
for(int i=0; i<10; i++) { delete(cname[i]); }
スクリプトの終わりでは、10個のデストラクタが動いていますがエラーメッセージはありません。
この記事に添付されている OOP_sConstDestr_2.mq5 ファイルで、具体例を見ることができます。
OOPを利用しプログラム論理を変更する(仮想関数、多相性)
多相性- プログラムの論理をコントロールすることを可能にする、OOPででもっとも興味深く重要な機能です。仮想関数を持つ親クラスと複数の子クラスを使用します。1つのクラスは子クラスによっていくつかの形を取ります。
簡単な例を見てみましょう - 2つの値の比較。比較には5つの種類があります。:より多い (>)、より少ない(<)、それ以上 (>=)、それ以下 (<=)、等しい (==)。
仮想関数を持つ親クラスを作ります。仮想関数 - 完全に通常の関数ですが宣言がvirtualで始まります:
class CCheckVariant { public: virtual bool CheckVariant(int Var1,int Var2) { return(false); } };
仮想関数にはコードがありません。多様なデバイスが利用できるコネクタのようなものです。デバイスのタイプにより異なるアクションを行います。
5つの子クラスの作成
//+------------------------------------------------------------------+ //| > | //+------------------------------------------------------------------+ class CVariant1: public CCheckVariant { bool CheckVariant(int Var1,int Var2) { return(Var1>Var2); } }; //+------------------------------------------------------------------+ //| < | //+------------------------------------------------------------------+ class CVariant2: public CCheckVariant { bool CheckVariant(int Var1,int Var2) { return(Var1<Var2); } }; //+------------------------------------------------------------------+ //| >= | //+------------------------------------------------------------------+ class CVariant3: public CCheckVariant { bool CheckVariant(int Var1,int Var2) { return(Var1>=Var2); } }; //+------------------------------------------------------------------+ //| <= | //+------------------------------------------------------------------+ class CVariant4: public CCheckVariant { bool CheckVariant(int Var1,int Var2) { return(Var1<=Var2); } }; //+------------------------------------------------------------------+ //| == | //+------------------------------------------------------------------+ class CVariant5: public CCheckVariant { bool CheckVariant(int Var1,int Var2) { return(Var1==Var2); } };
このクラスを使う前にロードしなければなりません:どの子クラスが使われるか分かっている場合は、そのタイプに合わせたポインタを宣言します。">"より大きいかを確かめたい場合は:
CVariant1 var; // Load class to check the ">" condition
もちろん事前に子のタイプを知ることはできませんので、クラスのポインタは親クラスのタイプで宣言します。しかし、この場合では動的なポインタが使われています。
CCheckVariant* var;
ここでは新しいキーワードを使って子をロードしなければなりません。選択されたバリアントに応じて子をロードします:
// Number of variant int Variant=5; // Depending on variant number one of five children classes will be used switch(Variant) { case 1: var = new CVariant1; break; case 2: var = new CVariant2; break; case 3: var = new CVariant3; break; case 4: var = new CVariant4; break; case 5: var = new CVariant5; break; }
状態を確かめます:
bool rv = var.CheckVariant(1,2);
状態を確かめるコードはすべてのクラスに対し同じですが、2つの値の比較の結果は子のクラスによって決まります。
この記事に添付されているOOP_sVariant_1.mq5ファイルで、具体例を見ることができます。
カプセル化の詳細 (プライベート、保護、パブリック)
この時点ではパブリックセクションはかなり明らかです。- クラスユーザー(ここではユーザーとは、既製のクラスをつかってプログラムを書くプログラマーを意味します。)に見えるように関数と変数を置く場所です。クラスユーザーからは保護とプライベートセクションに違いはありません -これらのセクションの関数や変数はユーザーには利用不可です:
//+------------------------------------------------------------------+ //| Class with the protected keyword | //+------------------------------------------------------------------+ class CName1 { protected: int ProtectedFunc(int aArg) { return(aArg); } public: int PublicFunction(int aArg) { return(ProtectedFunc(aArg)); } }; //+------------------------------------------------------------------+ //| Class with the private keyword | //+------------------------------------------------------------------+ class CName2 { private: int PrivateFunc(int aArg) { return(aArg); } public: int PublicFunction(int aArg) { return(PrivateFunc(aArg)); } }; CName1 c1; // Load class with the protected keyword CName2 c2; // Load class with the private keywordこのサンプルには2つのクラスがあります:CName1 と CName2です。それぞれのクラスには2つの関数があります:1つはパブリック セクションに、もう1つは -保護セクション (CName1クラス用)かプライベートセクション(CName2クラス用)。両方のクラスとも、関数のドロップダウンリストにあるパブリックセクションから1つだけ関数を持っています。(図 2 と図 3)。
図 2. CName1クラスの関数
図 3. CName2クラスの関数
この記事に添付されているOOP_sProtPriv_1.mq5ファイルで、具体例を見ることができます。
プライベートと保護セクションでは親クラスのファンクションの子クラスへの可視性を決定します:
//+------------------------------------------------------------------+ //| Base class | //+------------------------------------------------------------------+ class CBase { protected: string ProtectedFunc() { return("CBase ProtectedFunc"); } private: string PrivateFunc() { return("CBase PrivateFunc"); } public: virtual string PublicFunction() { return(""); } }; //+------------------------------------------------------------------+ //| Child class | //+------------------------------------------------------------------+ class Class: public CBase { public: string PublicFunction() { // With this line everything compiles correctly return(ProtectedFunc()); // If you will uncomment this line and comment the previous one, there will be a compiler error // return(PrivateFunc()); } };
この例ではCBaseと呼ばれる親クラスとClassという子クラスがあります。子クラスの保護とプライベートセクションにあるい関数を呼び出そうとしています。保護セクションから関数が呼び出された場合、すべてコンパイルされ実行されます。もしもプライベートセクションから呼び出された場合、コンパイルエラーが表示されます(プライベートにあるファンクションが呼び出せません)。これは、プライベートセクションにある関数は子クラスにとって見えないものだということです。
保護セクションの関数はクラスユーザーに対し保護されていて、プライベートセクションの関数は子クラスに対して保護されています。親クラスの関数(違うセクションに置かれている)の子クラスに対する可視性は図4に示されています。
図 4. 親クラスの関数の子クラスへの可視性青い矢 - 利用可能な関数、 グレー - 使用不可。
この記事に添付されているOOP_sProtPriv_2.mq5 ファイルで、具体例を見ることができます。
デフォルトの仮想関数と継承
親クラスのすべての仮想関数に対応する子クラスの関数があるわけではありません。子クラスが同じ名前の関数を持っている場合 - この関数を使用します。もし無ければ -親クラスの仮想関数のコードを実行します。その例を見てみましょう。
//+------------------------------------------------------------------+ //| Base Class | //+------------------------------------------------------------------+ class CBase { public: virtual string Function() { string str=""; str="Function "; str=str+"of base "; str=str+"class"; return(str); } }; //+------------------------------------------------------------------+ //| Child class 1 | //+------------------------------------------------------------------+ class Class1: public CBase { public: string Function() { string str=""; str="Function "; str=str+"of child "; return(str); } }; //+------------------------------------------------------------------+ //| Child class 2 | //+------------------------------------------------------------------+ class Class2: public CBase { }; Class1 c1; // Load class 1 Class2 c2; // Load class 2 //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { Alert("1: "+c1.Function()); // Running function from Class1 Alert("2: "+c2.Function()); // Running function from CBase }
Class2クラスには関数がありませんが、Function()関数を呼び出すことができます。CBaseクラスの関数を実行します。Class1クラスは自身の関数を実行します:
void OnStart() { Alert("1: " + c1.Function()); // Running function from Class1 Alert("2: " + c2.Function()); // Running function from CBase }
クラスユーザーからすると、子クラスを使用時はパブリックセクションの親クラスのすべての関数は利用可能です。これを継承と呼びます。親クラスの関数が仮想と宣言されている場合、もしも子クラスがこの名前(図5.)の関数を持っていれば、その関数で置き換えられます。
図 5. 関数にクラスユーザーでアクセス
子クラスが親クラスの仮想関数に対応する関数を全く持っていない場合、子クラスは”エクストラな”関数を持てます(親クラス内に同じ名前の仮想関数を持たない関数)。クラスを子クラスの型を指すポインタでロードした場合、これらの関数が利用可能です。クラスを親クラスの型を指すポインタでロードした場合、これらの関数は利用可能ではありません(図 6)。
図 6. 追加の”関数の可視性(赤い矢印)はクラスをロードしたポインタの型によって決まります。
この記事に添付されているOOP_sDefaultVirtual_1.mq5ファイルで、具体例を見ることができます。
クラスのロードについての追加
仮想関数や対応する親クラスや子クラスを使用する時、子クラスを使うべきだと分かっている場合は、子クラスに対応するポインタを使うことができます。
Class1 c1; // Load class 1 Class2 c2; // Load class 2
どの子クラスを使うか分からない場合、親クラスの型に動的ポインタを使い、新しいキーワードを使ってクラスをロードしてください:
CBase *c; // Dynamic pointer void OnStart() { c=new Class1; // Load class ...
自動ポインタを親クラスに使用する場合
CBase c; // Automatic pointer
親クラスがそのまま使われます。仮想関数を呼び出した場合、その関数内にあるコードを実行します。仮想関数が通常の関数に変換されます。
関数内のオブジェクトの処理
タイトルがセクションの内容を十分表しています。オブジェクトを指すポインタが関数に渡され、関数内でオブジェクト関数を呼び出せるようになります。関数のパラメータは親クラスの型と一緒に宣言できます。これにより関数をユニバーサルにすることができます。クラスを指すポインタは参照によってのみ関数に渡すことができます(&マークを使って示します):
//+------------------------------------------------------------------+ //| Base Class | //+------------------------------------------------------------------+ class CBase { public: virtual string Function() { return(""); } }; //+------------------------------------------------------------------+ //| Child class 1 | //+------------------------------------------------------------------+ class Class1: public CBase { public: string Function() { return("Class 1"); } }; //+------------------------------------------------------------------+ //| Child class 2 | //+------------------------------------------------------------------+ class Class2: public CBase { public: string Function() { return("Class 2"); } }; Class1 c1; // Load class 1 Class2 c2; // Load class 2 //+------------------------------------------------------------------+ //| Function to process objects | //+------------------------------------------------------------------+ void Function(CBase &c) { Alert(c.Function()); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { // Process objects using one function. Function(c1); Function(c2); }
この記事に添付されているOOP_sFunc_1.mq5ファイルで、具体例を見ることができます。
関数とメソッド、変数とプロパティ
ここまでの記事では”関数”という言葉を使ってきました。しかしOOPでは”関数”という言葉の代わりに”メソッド”という言葉を使います。クラスを書くプログラマの視点から見ると、すべての関数は関数のままです。既製のクラスを使うプログラマの視点から見ると、パブリックセクション (.をドロップダウンリストで入力すると使えるようになります)にあるクラスインタフェースの関数はメソッドと呼ばれます。
メソッドに加え、クラスインターフェースはクラスのプロパティを含むことができます。パブリックセクションには関数だけではなく、変数(配列を含む)も含むことができます。
class CMethodsAndProperties { public: int Property1; // Property 1 int Property2; // Property 2 void Function1() { //... return; } void Function2() { //... return; } };
これらの変数はクラスのプロパティと呼ばれ、ドロップダウンリストでも利用可能です (図 7)。
図 7. クラスのメソッドとプロパティが1つのリストにあります
プロパティは変数と同じように使うことができます:
void OnStart() { c.Property1 = 1; // Set property 1 c.Property2 = 2; // Set property 2 // Read properties Alert("Property1 = " + IntegerToString(c.Property1) + ", Property2 = " + IntegerToString(c.Property2)); }
この記事に添付されているOOP_sMethodsAndProperties.mq5ファイルで、具体例を見ることができます。
データ構造
データ構造はクラスと似ていますが、少しだけ簡単です。クラスはデータ構造のようなものということはできますが、より複雑です。データ構造が含むことができるのは変数だけだという違いがあります。パブリックやプライベート、保護セクションに分ける必要はありません。構造のすべての中身はパブリックセクションに置かれます。データ構造はstructから始まり、構造名、括弧内に変数を宣言します。
struct Str1 { int IntVar; int IntArr[]; double DblVar[]; double DblArr[]; };
構造を使うには変数として宣言されていなければいけませんが、変数型ではなく構造名を使います。
Str1 s1;
構造の配列を宣言することもできます:
Str1 sar1[];
構造は変数や配列だけでなく、他の構造を含むこともできます:
struct Str2 { int IntVar; int IntArr[]; double DblVar[]; double DblArr[]; Str1 Str; };
この場合、構造2の一部である構造1の変数を呼び出す場合、2つのポイントを使用しなければなりません:
s2.Str.IntVar=1;
この記事に添付されているOOP_Struct.mq5ファイルで、具体例を見ることができます。
クラスは変数だけではなく構造も含むことができます。
まとめ
オブジェクト指向プログラミングと覚えておくべき重要なポイントについておさらいしましょう:
1. クラスはクラスキーワード、続いてクラス名、括弧内にコードというように作成されます。
class CName { private: protected: public: };
2. クラスの関数や変数プライベート, プライベート や パブリックという3つのセクションに置くことができます。プライベートセクションにある関数と変数はクラス内でしか使用できません。保護セクションにある関数と変数はクラス内と子クラスからしか使用できません。パブリックセクションにある関数と変数はすべて利用可能です。
3. クラスの関数はクラス内かクラス外、どちらに置くこともできます。関数をクラスの外に置く場合は、どのクラスに属すのかを、クラス名と2つのコロンを関数名の前につけることで分かるようにします。
void ClassName::FunctionName() { ... }
4. クラスは自動ポインタ、動的ポインタどちらを使ってもロードすることができます。動的ポインタを使う場合、クラスは新しいキーワードを使いロードしなければなりません。この場合、プログラムを終了するときに削除キーワードを使いオブジェクトを削除しなければなりません。
5. 子クラスに親クラスに属していることを伝えるためには、子クラス名の後に親クラス名を加える必要があります。
class Class : public CBase { ... }
6. クラス初期化の途中には変数に値を割り当てることはできません。 関数-を実行中に値を割り当てることはできます- コンストラクタを実行中のことの方が多いです。
7. 仮想関数は仮想キーワードを使い宣言します。子クラスが同じ名前の関数を持つ場合、その関数を-そうでない場合は親クラスの仮想関数を実行します。
8. クラスを指すポインタは関数に渡すことができます。子クラスを指すポインタを関数に渡すために、関数のパラメータを親クラスの型で宣言することができます。
9. パブリックセクションには関数(メソッド)だけではなく、変数(プロパティ)も含むことができます。
10. 構造は配列や他の構造を含むこともできます。
添付ファイルの一覧
- OOP_CLibArray_1.mqh - インクルードされたファイル、MQL5/Includeフォルダに置きます。クラスを使いライブラリを作成する例。保護されたキーワードとプライベートキーワード。オーバーロード。
- OOP_CLibArray_2.mqh - インクルードされたファイル、MQL5/Includeフォルダに置きます。クラスの外にクラス関数を置く例。
- OOP_sDeleteOrders_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。未決のオーダーを削除する簡単なスクリプト。
- OOP_CLibArray_1.mqh - インクルードされたファイル、MQL5/Includeフォルダに置きます。OOP_sDeleteOrders_1スクリプトをクラスに変換する例
- OOP_sDeleteOrders_2.mq5 - インクルードされたファイル、MQL5/Scriptsフォルダに置きます。 クラスを使ってオーダーを削除する例。OOP_CDeleteOrder_1.mqhファイルから(Init()関数を使いパラメータを設定)。
- OOP_sDeleteOrders_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。コンストラクタとデストラクタのデモ。
- OOP_CLibArray_2.mqh - インクルードされたファイル、MQL5/Includeフォルダに置きます。オーダーを削除し、コンストラクタ経由でパラメータを渡すクラス。
- OOP_sDeleteOrders_.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。クラスを使ってオーダーを削除する例。OOP_CDeleteOrder_2.mqhファイルから(コンストラクタを使いパラメータを設定)。
- OOP_sDeleteOrders_2.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。クラスを配列に入れる例
- OOP_sVariant_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。子を持つ親クラスの例仮想関数、多相性
- OOP_sProtPriv_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。クラスを使った時の保護 とプライベートキーワードのアイデンティティの例。
- OOP_sProtPriv_2.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。子クラスの保護 とプライベートキーワードの影響の例。
- OOP_sDefaultVirtual_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。子クラスが親クラスの仮想関数対応する関数を持たない場合の例。
- OOP_sFunc_1.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。関数のオブジェクトの使用例
- OOP_sMethodsAndProperties.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。プロパティの例。
- OOP_Struct.mq5 - スクリプト、 MQL5/Scriptsフォルダに置きます。構造の例。
これらのファイルを試した後には、OOP_CDeleteOrder_2.mqhとOOP_sDeleteOrders_3.mq5以外のファイルは削除することができます。OOP_CDeleteOrder_2.mqhとOOP_sDeleteOrders_3.mq5のファイルは実践的なプログラミングに有用です。