English Русский 中文 Español Deutsch Português
preview
初心者からプロまでMQL5をマスターする(第3回):複雑なデータ型とインクルードファイル

初心者からプロまでMQL5をマスターする(第3回):複雑なデータ型とインクルードファイル

MetaTrader 5 | 5 2月 2025, 16:32
411 0
Oleh Fedorov
Oleh Fedorov

はじめに

この記事は初心者向け連載の続きです。ここでは、読者が前の2つの記事の内容をすでに理解していると仮定しています。

最初の記事では、プログラミングの経験がない読者を対象に、プログラマーに必要なツールや主要なプログラムの種類、そして「関数」といった基本的な概念を紹介しました。

第2回の記事では、データ操作に関する内容を説明しました。「リテラル」、「変数」、「データ型」、「演算子」などの概念を紹介し、算術演算子、論理演算子、ビット演算子など、データを変更するための主な演算子を解説しました。

この記事では、プログラマーが複雑なデータ型を作成する方法について説明します。

  • 構造体
  • 共用体
  • クラス(初心者向け)
  • 変数名を関数として使用できるデータ型(この方法により、関数を他の関数にパラメータとして渡すことができます)

さらに、#includeプリプロセッサディレクティブを使用して外部のテキストファイルをインクルードし、プログラムのモジュール性と柔軟性を確保する方法についても触れます。データはさまざまな方法で構成できますが、コンパイラは常にプログラムが必要とするメモリ量を把握しておく必要があり、そのためデータを使用する前に必ずその型を指定して定義する必要があります。

単純なデータ型(double、enum、stringなど)は第2回の記事で説明しました。その中で、変数(プログラム実行中に変化するデータ)や定数について詳しく取り上げました。しかし、プログラミングを進める中で、単純なデータを組み合わせてより複雑な型を作成する方が便利な場合がよくあります。この記事の最初の部分では、そういった「構造体」の作成方法について詳述します。

プログラムがよりモジュール化されていればいるほど、開発や保守が容易になります。特にチーム開発ではこの点が重要です。また、個人で開発している場合でも、長大なコードの中でエラーを探すよりも、小さなコードブロックでエラーを特定する方がはるかに効率的です。特に、時間が経過した後にプログラムに機能を追加したり、最初は見落としていた論理的なエラーを修正したりする際に、このアプローチが役立ちます。

適切なデータ構造を提供し、長い条件式やループを避けて便利な関数を分け、論理的に関連するコードを異なるファイルに分けることで、プログラムの変更が格段に容易になります。


構造体

構造体は、単一の変数に便利に格納できる複雑なデータセットを表現します。例えば、日中取引の実行時間に関する情報は、時間、分、秒を含むべきです。

もちろん、各コンポーネントに対して3つの変数を作成し、必要に応じてそれぞれの変数にアクセスすることも可能です。しかし、これらの変数は一つの情報の一部であり、通常は一緒に使われることが多いため、こういったデータに対しては別の型を定義することが便利です。構造体はまた、タイムゾーンやプログラマーが必要とするその他のデータ型を含むこともできます。

最も簡単な場合、構造体は以下のように定義されます。

struct IntradayTime {
  int hours;
  int minutes;
  int seconds;
  string timeCodeString;
};  // note the semicolon after the curly brace

例1:取引時間を説明する構造体の例

このコードは、新しいデータ型IntradayTimeを作成します。この宣言の中括弧内には、結合するすべての変数が定義されています。したがって、IntradayTime型のすべての変数には、時間、分、秒が含まれます。

各変数内の構造体の各部分には、ピリオド「.」を使ってアクセスできます。

IntradayTime dealEnterTime;

dealEnterTime.hours = 8;
dealEnterTime.minutes = 15;
dealEnterTime.timeCodeString = "GMT+2";

例2:構造体型変数の使用

構造体を記述する際、その「内部」変数(多くの場合、フィールドと呼ばれます)には、他の構造体を含む任意の有効なデータ型を使用することができます。次は例です。

// Nested structure
struct TradeParameters
{
   double stopLoss;
   double takeProfit;
   int magicNumber;
};

// Main structure
struct TradeSignal
{
   string          symbol;    // Symbol name
   ENUM_ORDER_TYPE orderType; // Order type (BUY or SELL)
   double          volume;    // Order volume
   TradeParameters params;    // Nested structure as parameter type
};

// Using the structure
void OnStart()
{

// Variable description for the structure
   TradeSignal signal;

// Initializing structure fields
   signal.symbol = Symbol();
   signal.orderType = ORDER_TYPE_BUY;
   signal.volume = 0.1;

   signal.params.stopLoss = 20;
   signal.params.takeProfit = 40;
   signal.params.magicNumber = 12345;

// Using data in an expression
   Print("Symbol: ",  signal.symbol);
   Print("Order type: ",  signal.orderType);
   Print("Volume: ",  signal.volume);
   Print("Stop Loss: ",  signal.params.stopLoss);
   Print("Take Profit: ",  signal.params.takeProfit);
   Print("Magic Number: ",  signal.params.magicNumber);
}

例3:構造体を使用して、別の構造体のフィールドの型を記述する


構造体の初期値として式ではなく定数を使用する場合は、初期化の省略表記を使用できます。ここでは中括弧を使用する必要があります。たとえば、前の例の初期化ブロックは次のように書き直すことができます。

TradeSignal signal = 
  {
    "EURUSD", 
    ORDER_TYPE_BUY, 
    0.1, 
 
     {20.0,  40.0,  12345}
  };

例4:定数を使用して構造体を初期化する


定数の順序は、説明内のフィールドの順序と一致する必要があります。初期フィールドの値をリストすることで、構造体の一部のみを初期化することもできます。この場合、他のすべてのフィールドはゼロに初期化されます。

MQL5はMqlDateTime、MqlTradeRequest、MqlTickなどの定義済み構造体のセットを提供します。原則として、それらの使用法は、このセクションで説明されている以上に複雑ではありません。これらおよび他の多くの構造体のフィールドのリストについては、言語リファレンスで詳しく説明されています。

さらに、任意の構造体(およびその他の複合型)のこのリストは、必要な型の変数を作成し、その名前を入力してキーボードでピリオド(「.」)を押すと、MetaEditorに表示されます。

構造フィールドのリスト

図1:MetaEditorの構造体フィールドのリスト

構造体のすべてのフィールドは、プログラムのすべての関数でデフォルトで使用できます。


MQL5構造体について:外部DLLを扱える方への補足

警告:このセクションは初心者には難しいかもしれません。そのため、初めてこの記事を読む際には、この部分をスキップして共用体セクションへ進み、後で戻ってくるのもよいでしょう。

デフォルトでは、MQL5の構造体のデータはパックされた形式(つまり、データが連続して配置される形式)になっています。そのため、構造体のサイズを特定のバイト数に調整したい場合は、追加の要素を挿入する必要があるかもしれません。

この場合、最初に大きなデータを配置し、その後に小さなデータを配置するのが望ましい方法です。これにより、多くの問題を回避できます。ただし、MQL5の構造体には、特殊なpack演算子を使用してデータのアライメント(整列)を調整する機能も備わっています。

struct pack(sizeof(long)) MyStruct1
     {
      // structure members will be aligned on an 8-byte boundary
     };

// or

struct MyStruct2 pack(sizeof(long))
     {
      // structure members will be aligned on an 8-byte boundary
     };

例5:構造体の整列

packの括弧内では、1、2、4、8、16のいずれかの数値のみを指定できます。

また、特殊コマンドoffsetofを使用すると、構造体内の任意のフィールドが先頭から何バイトの位置にあるかを取得できます。たとえば、例3のTradeParameters構造体において、stopLossフィールドのオフセットを取得するには、次のコードを使用します。

Print (offsetof(TradeParameters, stopLoss)); // Result: 0

例6offsetof演算子の使用

文字列、動的配列、クラスベースのオブジェクト、ポインタを含まない構造体は「単純な構造体」と呼ばれます。単純な構造体の変数や、それらで構成される配列は、外部DLLライブラリからインポートされた関数に自由に渡すことができます。

また、代入演算子を使用して単純な構造体同士をコピーすることも可能ですが、以下の2つのケースに限られます。

  • 変数の型が同じである
  • 変数の型が直接の継承ラインによって関連付けられている

    つまり、plants構造体とtrees構造体を定義した場合、plantsの変数をtreesに基づいて作成された変数にコピーすることができ、その逆も可能です。しかし、例えばbushesという構造体もある場合、bushesからtrees(またはその逆)へは要素ごとにコピーする必要があります。

それ以外のケースでは、たとえ同じフィールドを持つ構造体であっても、要素ごとにコピーしなければなりません。

同じルールが型キャストにも適用されます。つまり、たとえbushとtreeが同じフィールドを持っていたとしても、bushをtreeに直接キャストすることはできません。しかし、plantをbushにキャストすることは可能です。

もしbush型をtreeにキャストしたい場合は、共用体を使用できます。ただし、共用体にはいくつかの制限があります。その詳細はこの記事の関連セクションで説明します。簡単に言えば、数値フィールドであれば比較的容易に変換可能です。

//---
enum ENUM_LEAVES
  {
   rounded,
   oblong,
   pinnate
  };

//---
struct Tree
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
struct Bush
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
union Plant
  {
   Bush bush;
   Tree tree;
  };

//---
void OnStart()
  {
   Tree tree = {1, rounded};
   Bush bush;
   Plant plant;

// bush = tree; // Error!
// bush = (Bush) tree; // Error!
   plant.tree = tree;
   bush = plant.bush; // No problem...

   Print(EnumToString(bush.leaves));
  }
//+------------------------------------------------------------------+

例7:共用体を使用して構造体を変換する

とりあえず、構造体についての説明はここまでにします。構造体のすべての機能を完全に理解するには、この記事以上に多くの詳細やニュアンスを知る必要があります。MQL5の構造体を他のプログラミング言語と比較したり、さらに詳しく学習したりしたい場合は、言語リファレンスを確認してください。

しかし、初心者にとっては、この記事で説明した内容で十分理解できると思います。それでは、次のセクションに進みましょう。


共用体

一部の処理では、1つのメモリセル内のデータを異なる型の変数として解釈する必要がある場合があります。このようなケースは、主に構造体の型変換をおこなう際に発生します。また、暗号化処理などでも同様の要件が生じることがあります。

このようなデータの記述方法は、単純な構造体とほとんど変わりません。

// Creating a type
union AnyNumber {
  long   integerSigned;  // Any valid data types (see further)
  ulong  integerUnsigned;
  double doubleValue;
};

// Using
AnyNumber myVariable;

myVariable.integerSigned = -345;

Print(myVariable.integerUnsigned);
Print(myVariable.doubleValue);

例8:共用体の使用

共用体を使用する際のエラーを回避するために、同じメモリ領域を占めるデータ型を使用することを推奨します。ただし、一部の型変換ではこのルールが不要、または逆に不都合を生じる場合もあるため、状況に応じた判断が必要です。

次のデータ型は共用体のメンバーにできません。

  • 動的配列
  • 文字列
  • オブジェクトと関数へのポインタ
  • クラスオブジェクト
  • コンストラクタまたはデストラクタを持つ構造体オブジェクト
  • 上記1~5の要素を含む構造体オブジェクト

それ以外に特別な制限はありません。

重要:構造体内で文字列フィールドを使用するとコンパイルエラーになります。常にこの点を考慮して設計してください。


オブジェクト指向プログラミングの基本的な理解

オブジェクト指向プログラミングは、多くのプログラミング言語の基礎となるプログラミングパラダイムです。このアプローチでは、プログラム内の処理を独立したブロック(オブジェクト)に分割し、それぞれが特定の「エンティティ」(ファイル、ウィンドウ、価格リストなど)を表します。

各オブジェクトの目的は、データとそれを処理するためのアクションを一か所にまとめることです。適切に設計されたオブジェクト指向のプログラムには、次のようなメリットがあります。

  • コードの再利用が容易になる
  • IDEによる補完が効き、変数・関数名を素早く補完できる
  • エラーが見つけやすくなり、新たなバグの発生を抑えられる
  • 複数人・複数チームでの並行開発が容易になる
  • 時間が経っても、コードの変更・拡張が簡単になる
  • これらの特性により、開発スピードが向上し、プログラムの信頼性が高まり、保守性の良いコードが書けるようになります。

このようなレイアウトは、日常生活の原則に従っているため、一般的に自然です。私たちは常にあらゆる物を分類しています。「これは動物のクラスに属し、あれは植物のクラスに属し、あれは家具です」などです。家具はさらにキャビネット型や布張り型に分けることができるなどです。

これらすべての分類では、オブジェクトの特定の特徴とその説明が使用されます。たとえば、植物には幹と根があり、動物には動く手足があって移動できます。したがって、各クラスにはいくつかの特徴的な属性があります。プログラミングでも同じことが言えます。

もし、線を扱うためのライブラリを作成する場合、各線が何をできるのか、そしてそのために何を持っているのかを明確に理解する必要があります。たとえば、どの線にも始点、終点、太さ、色があります

これらは、その線クラスのプロパティ属性、またはフィールドです。線に対しておこなえるアクションとしては、「描画する」「移動する」「特定のオフセットでコピーする」「特定の角度で回転する」などがあります。

もし線オブジェクトがこれらすべてのアクションを独立して実行できる場合、プログラマーはこのオブジェクトのメソッドについて語ります。

プロパティとメソッドはまとめてクラスのメンバー(要素)と呼ばれます。

したがって、このアプローチを使用して線を作成するには、まずこの線とプログラム内の他のすべての線のクラス(説明)を作成し、次にコンパイラに「これらの変数は行であり、この関数はそれらを使用する」と伝える必要があります。

クラスは、このクラスに属するオブジェクトのプロパティメソッドの説明を含む変数型です。

記述方法の点では、クラスは構造体と非常に似ています。主な違いは、デフォルトでは、クラスのすべてのメンバーはそのクラス内でのみアクセス可能であることです。構造体では、そのすべてのメンバーがプログラムのすべての関数からアクセスできます。以下は、目的のクラスを作成するための一般的なスキームです。

// class (variable type) description
class TestClass { // Create a type

private:          // Describe private variables and functions 
                  //   They will only be accessible to functions within the class 
  
// Description of data (class "properties" or "fields")
  double m_privateField; 

// Description of functions (class "methods")
  bool  Private_Method(){return false;} 

public:           // Description of public variables and functions 
                  //   They will be available to all functions that use objects of this class    

// Description of data (class "properties", "fields", or "members")   
  int m_publicField; 

// Description of functions (class "methods")   
  void Public_Method(void)
    {
     Print("Value of `testElement` is ",  testElement );   
    }
 }; 


例9:クラス構造体の説明

キーワードpublic:private:は、クラスメンバーの可視性ゾーンを定義します。

public:という単語の下のすべては、クラスの「外部」、つまり、このクラスに属していないものも含め、プログラムの他の関数で使用できるようになります。

このセクションより上(およびprivate:という単語より下)にあるものはすべて「隠され」、これらの要素へのアクセスは同じクラスの関数に対してのみ可能になります。

クラスには、任意の数のpublic:セクションprivate:セクションを含めることができます。

ただし、明確な指針が示されているとはいえ、スコープごとに1つのブロック(1つのprivate:と1つのpublic:)のみを使用することが推奨されます。これは、同じアクセスレベルを持つすべてのデータや関数が互いに近くに配置されるためです。ただし、経験豊富な開発者の中には、関数用にprivateとpublicの2つ、変数用にprivateとpublicの2つ、合計4つのセクションを作成することを好む人もいます。この点については、最終的にどのスタイルを選択するかはあなた次第です。

基本的に、クラス内でpublic:として宣言されていないすべてのメンバーはデフォルトでprivateとなります(構造体とは異なる)。したがって、private:というキーワードは省略可能ですが、コードの可読性が低下するため、省略しないほうが望ましいとされています。

また、一般的に、記述されたクラス内には少なくとも1つの関数がpublicである必要があります。そうでなければ、ほとんどの場合、そのクラスは実用的ではなくなります。例外は存在しますが、それは非常に稀なケースです。

データを保護するために、public:セクションには変数ではなく関数のみを配置することが、良いプログラミング習慣とされています。これにより、クラスの変数はそのクラスのメソッドを介してのみ変更できるようになり、プログラムコードの信頼性が向上します。

記述されたクラスを使用するには、プログラム内の適切な場所に、必要な型の変数を作成します。変数の作成方法は通常の方法と同じです。クラスの各変数のメソッドやプロパティには、構造体と同様にピリオド記号を用いてアクセスします。
// Description of the variable of the required type
TestClass myTestClassVariable;

// Using the capabilities of this variable
myTestClassVariable.testElement = 5;
myTestClassVariable.PrintTestElement();

例10:クラスの使用

publicプロパティとprivateプロパティがどのように機能するかを理解するために、まず例11のコードをスクリプトのOnStart関数内に貼り付け、ファイルをコンパイルしてみてください。通常、この段階ではコンパイルが正常に完了するはずです。

次に、「myVariable.a = 5;」行のコメントを解除して、コードを再度コンパイルしてみてください。このとき、コンパイラは「クラスのprivateメンバーにアクセスしようとしています」という旨のコンパイルエラーを出力します。このように、コンパイラの機能によって、意図しないprivateメンバーの直接操作を防ぐことができます。これにより、オブジェクトのデータが保護され、他のアプローチでは気づきにくい微妙なエラーの発生を防ぐことができます。

class PrivateAndPublic 
  {
private:
    int a;
public:
    int b;
  };

PrivateAndPublic myVariable;

// myVariable.a = 5; // Compiler error! 
myVariable.b = 10;   // Success

例11:クラスのpublicプロパティとprivateプロパティの使用

すべてのクラスを自分で作成しなければならない場合、このアプローチは他のアプローチと何ら変わりがなくなり、ほとんど意味がなくなります。

幸いなことに、多くの標準クラスはすでにMQL5\Includeディレクトリに用意されています。また、コードベースには多数の便利なライブラリが用意されています。多くの場合、他の賢い人々の開発を活用するには、適切なファイルを単にインクルード(以下に説明)するだけで済みます。これにより、プログラマーは非常に助かります。

OOPについては膨大な書籍が出版されており、確かに別の記事を書く価値があります。しかし、この記事の目的は、初心者にプログラム内で複雑なデータ型をどのように使用するかの基本的な理解を提供することにあります。基本クラスの定義方法と他の人のクラスの使用方法を理解したので、次のセクションに進むことにします。


関数データ型(typedef演算子)

警告:このセクションは初心者には難しいかもしれませんので、初めて記事を読む際はスキップしても構いません。

このセクションの内容を理解しなくても、残りの部分を理解するうえで問題はありません。プログラミングにおいて、多くの問題には複数の解決方法があり、関数型の使用は避けることもできます。

しかし、特定の関数を変数に代入できる(そして、場合によってはそれを他の関数の引数として使用できる)機能は便利であり、少なくとも他の人のコードを読めるようになるために、この機能について知っておく価値はあると考えています。

たとえば、別の関数に引数として渡す場合など、「関数」型の変数を作成すると便利な場合があります。

例えば、取引の状況では、買い注文と売り注文は非常に似ていて、違いはわずかなパラメータの違いだけです。ただし、買値は常にAsk、売値は常にBidです。

多くのプログラマーは、特定の注文のニュアンスを考慮して独自のBuy関数やSell関数を作成します。その後、これら2つの関数を組み合わせたTradeのような関数を記述し、取引の方向に応じて、「Buy」または「Sell」の関数を自動で切り替えるようにします。このアプローチにより、プログラマーは注文の方向に関わらず、同じ関数呼び出しで取引を簡潔に扱えるようになります。

例えば、「オートマトン、やれ!」と言って、関数に特定の状況に応じた最適な選択肢を決定させたい場合があります。利益確定時に価格にポイントを足すか引くか?ストップロスを計算するときは?注文を極値でおこなう場合、最大値を探すべきか、最小値を探すべきか?など、様々なシナリオでその選択肢を自動で決めさせることができます。

そのため、このような場合に使われるのが、次に示すアプローチです。

いつものように、まず、必要な変数の型を定義する必要があります。この型は、以下のテンプレートを使用して記述します。

typedef function_result_type (*Function_type_name)(input_parameter1_type,input_parameter1_type ...); 

例12:機能タイプを記述するためのテンプレート

ここで

  • function_result_typeは戻り値の型(intdoubleなどの有効な型)
  • Function_type_nameは、変数を作成するときに使用する型の名前
  • input_parameter1_typeは最初のパラメータの型(パラメータリストは通常の関数リストのルールに従います)

型の名の前のアスタリスク(*)に注意してください。これは非常に重要で、これがなければ何も動作しません。

アスタリスクは、この型の変数が結果や数値を保持するのではなく、関数自体を保持することを意味します。関数自体は完全な機能セットを備えており、そのため、この変数は他の変数と関数の両方に固有の機能を組み合わせたものとなります。

データ型を記述する際に、オブジェクトのデータやその操作結果のコピーではなく、オブジェクトそのもの(関数やクラスのオブジェクトなど)を使用する構造は、ポインタと呼ばれます。

ポインタについては、今後のセクションでさらに詳しく説明します。では、typedef演算子の使用例を見てみましょう。

次に、DiffAddがあり、それぞれを変数に割り当てたいとします。どちらの関数も整数値を返し、2つの整数パラメータを取ります。その実装は非常にシンプルです。

//---
int Add (int a,int b)
  {
    return (a+b);
  }

//---
int Diff (int a,int b)
  {
    return (a-b);
  }

例13:機能型テストのための合計関数と差分関数

これらの関数のいずれかを格納できる変数のTFunc型について説明しましょう。
typedef int (*TFunc) (int,  int);

例14:Add関数とDiff関数を格納できる変数の型宣言


次に、この説明がどのように機能するかを確認しましょう。

void OnStart()
  {
    TFunc operate;       //As usual, we declare a variable of the described type
 
    operate = Add;       // Write a value to a variable (in this case, assign a function)
    Print(operate(3, 5)); // Use the variable as a normal function
                         // Function output: 8

    operate=Diff;
    Print(operate(3, 5)); // Function output: -2
  }

例15:関数型変数の使用

typedef演算子はカスタム関数でのみ機能することに注意してください。

MathMinなどの標準関数を直接使用することはできませんが、それらの「ラッパー」を作成することはできます。次は例です。

//---
double MyMin(double a, double b){
   return (MathMin(a,b));
}

//---
double MyMax(double a, double b){
   return (MathMax(a,b));
}

//---
typedef double (*TCompare) (double,  double);

//---
void OnStart()
  {
    TCompare extremumOfTwo;

    compare= MyMin;
    Print(extremumOfTwo(5, 7));// 5

    compare= MyMax;
    Print(extremumOfTwo(5, 7));// 7
  }

例16:ラッパーを使用して標準関数を操作する


外部ファイルのインクルード(#includeプリプロセッサディレクティブ)

どんなプログラムも複数のモジュールに分割することができます。

特に大規模なプロジェクトに取り組む場合、プログラムを分割することは不可欠です。プログラムのモジュール化により、いくつもの問題を一度に解決できます。

  • まず第一に、モジュール化されたコードはよりナビゲートしやすくなります。
  • 第二に、チームで作業している場合、各モジュールを異なる人が担当することができ、これによって開発が大幅に効率化されます。
  • そして第三に、作成したモジュールを再利用することが可能です。

最も基本的な「モジュール」の一例は関数です。さらに、すべての定数や、いくつかの複雑なデータ型の定義、関連する関数群(例えばオブジェクトの外観を変更する関数や数学関数の集まり)などもモジュールとして作成できます。

大規模なプロジェクトでは、こういったコードのブロックを別々のファイルに分け、必要に応じてそのファイルを現在のプログラムにインクルードする方法が非常に便利です。

プログラムに外部のテキストファイルを組み込むには、#includeプリプロセッサディレクティブを使用します。

#include <SomeFile.mqh>     // Angle brackets specify search relative to MQL5\Include directory 
#include "AnyOtherPath.mqh" // Quotes specify search relative to current file

例17#includeの2つの形式

コンパイラがコード内のどこかで#include命令に遭遇すると、指定されたファイルの内容がこの命令の位置に挿入されます。ただし、これはプログラム内で一度だけ.おこなわれます。すでにそのファイルが使われている場合は、再度インクルードすることはできません。

この動作を次のセクションで紹介するスクリプトを使って確認できます。

ほとんどの場合、インクルードファイルには便利なため拡張子「*.mqh」が付けられますが、実際には拡張子は任意で、他の拡張子を使用することも可能です。


#includeディレクティブの動作をテストするスクリプト

このプリプロセッサディレクティブに遭遇したときのコンパイラの動作をテストするには、2つのファイルを作成する必要があります。

まず、スクリプトディレクトリ(MQL5\Scripts)に「1.mqh」という名前のファイルを作成しましょう。このファイルの内容は非常にシンプルです。

Print("This is include with number "+i);

例18:最も単純なインクルードファイルには、コマンドを1つだけ含めることができる

このコードが何をするのかが明確であることを願っています。変数iがどこかで宣言されていると仮定すると、このコードは変数の値をメッセージに追加してユーザー向けのメッセージを作成し、そのメッセージをログに出力します。

変数iはスクリプトのどの時点で命令が呼び出されたかを示すマーカーになります。このファイルには他に何も書き込みません。次に、同じディレクトリ(ファイル「1.mqh」がある場所)に、次のコードを含むスクリプトを作成します。

//+------------------------------------------------------------------+ 
//| Script program start function                                    | 
//+------------------------------------------------------------------+ 
void OnStart() 
  { 
    //---   
    int i=1; 
#include "1.mqh"   
    i=2; 
#include "1.mqh" 
  } 
//+------------------------------------------------------------------+

// Script output:
// 
//   This is include with number 1
//
// The second attempt to use the same file will be ignored

//+------------------------------------------------------------------+ 

例19:ファイルの繰り返しのインクルードをテストする

このコードでは、ファイル「1.mqh」を2回使用して、2つのトリガーメッセージを受け取ろうとしました。

このスクリプトをターミナルで実行すると、最初のメッセージは期待通りに動作し、メッセージに数字「1」が表示されますが、2回目のメッセージは表示されません。

なぜこの制限が適用されるのでしょうか。なぜファイルを複数回使用できないのでしょう。

これは重要な原則です。なぜなら、インクルードファイルには多くの場合、変数や関数の宣言が含まれているからです。すでにご存知の通り、1つのプログラムのグローバルレベル(すべての関数の外側)では、特定の名前を持つ変数が1つだけ存在すべきです。

たとえば、変数int a;が宣言されている場合、グローバルレベルで同じ名前の変数を再度宣言することはできません。すでに存在する変数を使用することしかできません。関数についても同様で、状況は少し複雑になりますが、基本的な考え方は同じです。すなわち、各関数はプログラム内で一意でなければならないということです。ここで、プログラムが2つの独立したモジュールを使用してて、それぞれのモジュールが<Arrays\List.mqh>ファイル内の同じ標準クラスをインクルードしていると仮定します(図2)。

2つのモジュールで同じクラスを使用する

図2:2つのモジュールで同じクラスを使用する

この制限がなければ、同じクラスを2回宣言することは禁止されているため、コンパイラはエラーメッセージを返します。しかし、この場合、FieldOf_Module1フィールドの記述の後、CListの記述はすでにコンパイラのリストに含まれているため、単にこの記述をモジュール2で使用するだけなので、このような構成は非常に有効です。

この原則を理解すれば、たとえば図3のように、一部のクラス要素が互いに「循環的」に依存している場合など、「多層的な」ネストを作成することができます。

クラス内で同じクラスの変数を記述することも可能です。

これらはすべて許容される構造であり、#includeは1つのファイルに対して1回だけ機能するため、問題なく動作します。

循環依存:各クラスには、他のクラスに依存する要素が含まれている

図3:循環依存関係 - 各クラスには、他のクラスに依存する要素が含まれている

このセクションの結論として、コードに含めることができるMetaTrader 5標準ライブラリファイルMQL5\Includeディレクトリにあることをもう一度思い出していただきたいと思います。このディレクトリをエクスプローラーで開くには、MetaTraderターミナルでメニューの[ファイル]>[データディレクトリを開く]を選択します(図4)。

データディレクトリを開く

図4:データディレクトリを開く方法

このディレクトリのファイルをMetaEditorで開く場合は、ナビゲータパネルでIncludeフォルダを見つけます。同じディレクトリ(できれば別のフォルダ)に独自のインクルードファイルを作成することも、プログラムのディレクトリとそのサブディレクトリを使用することもできます(例17のコメントを参照)。原則として、#includeディレクティブは、他のすべてのアクションが開始される前にファイルの先頭で使用されます。ただし、このルールは厳密なものではなく、すべては特定のタスクによって異なります。


結論

この記事で取り上げたトピックを、もう一度簡単に振り返りましょう。

  1. プリプロセッサディレクティブ#includeについて説明しました。このディレクティブによって、追加のテキストファイル、通常はライブラリなどをプログラムに含めることができます。
  2. 複雑なデータ型について説明しました。これには、構造体共同体オブジェクトクラスに基づく変数)などの複雑なデータ型と関数データ型が含まれます。

この記事で説明したデータ型は、その構造が「複雑」であっても、実際に使用する際にはそれほど複雑ではないことを理解していただけたことを願っています。

言語に組み込まれている単純な型とは異なり、「複雑な」型は最初に宣言し、その後で初めて変数を作成できます。しかし、このようなデータを扱うことは本質的には「単純な」型と変わりありません。変数を作成し、必要に応じてその変数のコンポーネント(メンバー)を呼び出したり、関数型の場合は変数名を関数名として使うだけです。

構造体を使用して作成した変数は、中括弧{}を使用して初期化することができます。

独自の複雑な型を作成し、プログラムを外部ファイルに分割してモジュール化することで、プログラム開発が柔軟かつ効率的になることが理解できたと思います。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14354

多通貨エキスパートアドバイザーの開発(第14回):リスクマネージャーにおける適応型ボリューム変更 多通貨エキスパートアドバイザーの開発(第14回):リスクマネージャーにおける適応型ボリューム変更
以前開発されたリスクマネージャーには基本的な機能のみが含まれていました。取引戦略のロジックに干渉することなく取引結果を向上させるために、どのような開発の可能性があるかを検討してみましょう。
季節性を利用した外国為替スプレッド取引 季節性を利用した外国為替スプレッド取引
この記事では、外国為替取引におけるスプレッド取引時に季節性要因を利用したレポートデータの生成および提供の可能性について検討します。
ニューラルネットワークが簡単に(第97回):MSFformerによるモデルの訓練 ニューラルネットワークが簡単に(第97回):MSFformerによるモデルの訓練
さまざまなモデルアーキテクチャの設計を検討する際、モデルの訓練プロセスには十分な注意が払われないことがよくあります。この記事では、そのギャップを埋めることを目指します。
ニューラルネットワークの実践:擬似逆行列(II) ニューラルネットワークの実践:擬似逆行列(II)
この連載は教育的な性質のものであり、特定の機能の実装を示すことを目的としていないため、この記事では少し異なる方法でおこないます。因数分解を適用して行列の逆行列を取得する方法を示す代わりに、擬似逆行列の因数分解に焦点を当てます。その理由は、特別な方法で一般的な係数を取得することができる場合、一般的な係数を取得する方法を示すことに意味がないからです。さらに良いことに、読者は物事がなぜそのように起こるのかをより深く理解できるようになります。それでは、時間の経過とともにハードウェアがソフトウェアに取って代わる理由を考えてみましょう。