preview
Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы

Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы

MetaTrader 5Примеры | 18 июля 2024, 13:09
100 2
Oleh Fedorov
Oleh Fedorov

Введение

Эта статья — продолжение цикла для начинающих. Здесь я предполагаю, что у читателя уже есть понимание материала двух предыдущих статей.

Первая статья — введение. Она предполагает, что читатель вообще не сталкивался с программированием, и позволяет разобраться с инструментами, нужными программисту для работы, описывает основные типы программ, а также вводит некоторые базовые понятия, в частности, понятие "функция".

Вторая статья описывает работу с данными. В ней введены понятия "литерал", "переменная", "тип данных", "оператор" и т.д., и рассмотрены основные операторы для изменения данных: арифметические, логические, побитовые...

В этой статье я опишу, как программист может создавать сложные типы данных, такие как:

  • структуры;
  • объединения (union);
  • классы (на начальном уровне);
  • типы, позволяющие использовать имя переменной как функцию. Это позволяет, в том числе, передавать функции в виде параметров другим функциям.

Также в статье рассказано, как подключать внешние текстовые файлы с помощью директивы препроцессора #include, чтобы обеспечить нашей программе модульность и гибкость. Напомню, что данные можно организовывать по-разному, но компилятор всегда должен знать, сколько памяти потребуется нашей программе, и поэтому прежде, чем использовать данные, их нужно описать, указав их тип.

Простые типы данных, такие как double, enum, string и другие были описаны во второй статье. Там подробно рассматривались как переменные (данные, которые меняются в процессе работы), так и константы. Однако при программировании часто возникают ситуации, когда из простых данных удобнее составлять более сложные типы. Именно об этих "конструкциях" мы будем говорить в первой части этой статьи.

Чем более модульная структура у программы, тем проще нашу программу разрабатывать и поддерживать. Особенно важно это становится при работе в команде. Правда, "одиночкам" тоже гораздо проще искать ошибки не в "сплошном" куске кода, а в мелких его фрагментах. Особенно, если вы возвращаетесь к коду через длительное время для того, чтобы добавить какие-то возможности вашей программе или исправить какие-то логические ошибки, которые сразу были незаметны.

Если вы обеспечите правильные структуры для данных, выделите удобные функции вместо "сплошного" потока условий и циклов, а также распределите разные логически связанные блоки кода по разным файлам, внесение изменений будет проходить намного легче.


Структуры

Структура описывает сложный набор данных, который удобно хранить в одной переменной. Например, информация о времени совершения внутридневной сделки должна содержать минуты, секунды и часы.

Конечно, можно создать три переменных для каждой составляющей и обращаться к каждой из них при необходимости. Однако поскольку эти данные являются частями одного описания и, чаще всего, используются совместно, удобно описать для таких данных отдельный тип. При этом структура может содержать и дополнительные данные других типов, как, например, часовой пояс или что-нибудь еще, нужное программисту.

В простейшем случае структура описывается следующим образом:

struct IntradayTime {
  int hours;
  int minutes;
  int seconds;
  string timeCodeString;
};  // обратите внимание на точку с запятой после фигурной скобки

Пример 1. Пример структуры для описания времени сделки.

С помощью этого кода мы создали новый тип данных IntradayTime. Между фигурными скобками данного описания перечислены все переменные, которые мы хотим объединить. Следовательно, все переменные типа IntradayTime будут содержать и часы, и минуты, и секунды.

Доступ к каждой части структуры внутри каждой переменной можно будет получить через знак "." (точка).

IntradayTime dealEnterTime;

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

Пример 2. Использование переменных типа структур.

При описании структуры её "внутренние" переменные (чаще их называют "поля") могут иметь любой допустимый тип данных, в том числе можно использовать другие структуры. Например, так:

// Вложенная структура
struct TradeParameters
{
   double stopLoss;
   double takeProfit;
   int magicNumber;
};

// Основная структура
struct TradeSignal
{
   string          symbol;    // Символ финансового инструмента
   ENUM_ORDER_TYPE orderType; // Тип ордера (BUY или SELL)
   double          volume;    // Объем ордера
   TradeParameters params;    // Вложенная структура в качестве типа параметра
};

// Использование структуры
void OnStart()
{

// Описание переменной для структуры
   TradeSignal signal;

// Инициализация полей структуры
   signal.symbol = Symbol();
   signal.orderType = ORDER_TYPE_BUY;
   signal.volume = 0.1;

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

// Использование данных в выражении
   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
     {
      // члены структуры будут выровнены на границу 8 байт
     };

// или

struct MyStruct2 pack(sizeof(long))
     {
      // члены структуры будут выровнены на границу 8 байт
     };

Пример 5. Выравнивание структуры.

В скобках у pack могут быть только числа 1, 2, 4, 8, 16.

Специальная команда offsetof позволит получить смещение в байтах для любого поля структуры относительно начала. Например, если взять структуру TradeParameters из примера 3, для получения смещения поля stopLoss можно использовать следующий код:

Print (offsetof(TradeParameters, stopLoss)); // Результат: 0

Пример 6. Использование оператора offsetof.

Те структуры, которые НЕ содержат строки, динамические массивы, объекты на основе классов и указатели, называются простыми. Переменные простых структур, а также массивы, состоящие из таких элементов, могут быть свободно переданы в импортируемые из внешних библиотек dll функции.

Допустимо копирование простых структур друг в друга с помощью оператора присваивания, но только в двух случаях:

  • переменные принадлежат одному типу;
  • либо типы переменных связаны прямой линией наследования.

    Это значит, что если у нас определены структуры "растения" и "деревья", в любую переменную, созданную на основе "деревьев" можно скопировать любую переменную "растений", и наоборот. Однако если у нас есть еще и "кусты", то из "кустов" в "деревья" (или наоборот) — только поэлементно.

Во всех остальных случаях даже структуры с одинаковыми полями нужно копировать поэлементно.

Те же правила действуют при приведении типов: нельзя напрямую привести "куст" к "дереву", даже если у них одинаковые поля, но можно "растение" — к "кусту"...

Правда, если очень нужно привести тип "куста" к "дереву", можно использовать объединения (union). Главное — помнить про ограничения объединений, описанные в соответствующем разделе этой статьи. А так — любые числовые поля преобразуются просто великолепно.

//---
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. Конвертация структур с помощью объединений (union).

Ну, и на сём, пожалуй, остановлюсь. Полное описание всех возможностей структур содержит несколько больше подробностей и нюансов, чем описано в этой статье. Если вам будет необходимо сравнить структуры MQL5 с другими языками или узнать какие-то тонкости... Ну, я надеюсь, вы уже знаете, куда смотреть.

Но для начинающих, по-моему, написанного про структуры материала вполне достаточно, поэтому я перехожу к следующему разделу.


Объединения (union)

Для некоторых задач может потребоваться интерпретировать данные в одной ячейке памяти как переменные разных типов. Наиболее часто такие задачи встречаются при приведении типов структур. Ещё такие задачи могут возникнуть при шифровании.

Описание таких данных почти ничем не отличается от описания простых структур:

// Создание типа
union AnyNumber {
  long   integerSigned;  // Любые допустимые типы данных (см. ниже по тексту)
  ulong  integerUnsigned;
  double doubleValue;
};

// Использование
AnyNumber myVariable;

myVariable.integerSigned = -345;

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

Пример 8. Использование объединений.

Чтобы избежать ошибок, в объединениях рекомендуется использовать данные, тип которых занимает одинаковое место в памяти (хотя при некоторых преобразованиях это не нужно или даже вредно).

Членами объединения НЕ могут быть следующие типы данных:

  • динамические массивы,
  • строки,
  • указатели на объекты и функции,
  • объекты классов,
  • объекты структур, имеющие конструкторы или деструкторы,
  • объекты структур, имеющие элементы из пунктов 1-5.

Больше ограничений нет.

Но еще раз повторюсь: если в вашей структуре используется какое-то строковое поле, компилятор выдаст ошибку. Имейте это в виду.


Начальное понятие об объектно-ориентированном программировании

В современном программировании очень часто используется объектно-ориентированный подход (ООП). Суть этого подхода в том, что всё, что происходит в программе, разбивается на отдельные блоки. Каждый такой блок описывает отдельную "сущность": файл, прямую, окно, список цен...

Смысл каждого блока в том, чтобы данные и действия, которые необходимы для их обработки, собирались в одном месте. Если такая "сборка" выполнена правильно, то это даёт множество преимуществ:

  • позволяет использовать код повторно — много раз;
  • облегчает работу IDE, которая обретает возможность быстро подставлять нужные нам названия переменных и функций, относящихся к конкретным объектам;
  • облегчает поиск ошибок и сокращает возможности для появления новых;
  • облегчает параллельную работу разным людям (или даже командам) над разными частями кода;
  • позволяет легче изменять свой код, даже если прошло много времени;
  • и всё это в итоге приводит к ускорению написания программ, повышению их надёжности и более лёгкому кодированию в итоге.

И такая компоновка, в общем-то естественна, поскольку использует принципы мышления в обычной жизни. Мы постоянно классифицируем всякие объекты: "Вот эта штука относится к классу животных, эти — растения, это — мебель"... Мебель, в свою очередь, бывает корпусная и мягкая... Ну, и так далее.

Все эти классификации используют определённые признаки объектов, их описания. Скажем, растения имеют ствол и корни, а у животных есть подвижные конечности, на которых они умеют передвигаться. В общем, есть какие-то характерные атрибуты каждого класса. В программировании всё точно так же.

Если задаться целью создать библиотеку по работе с прямыми, то надо понимать, что каждая прямая может делать и что у неё для этого есть в наличии. Например, любая прямая линия имеет точку начала, точку конца, толщину и цвет.

Это её свойства, или атрибуты, или поля класса прямых. Для действий можно использовать глаголы "рисовать", "переместить", "скопировать с определённым смещением", "повернуть на какой-то угол"...

Если объект прямой умеет выполнять все эти действия самостоятельно, то программисты говорят о методах этого объекта.

Свойства и методы вместе называют членами (элементами) класса.

Вот и получается, что для создания прямой с помощью этого подхода надо сначала создать класс (описание) этой прямой — и всех остальных прямых в программе, — а потом просто сообщить компилятору: "Вот эта и эта переменная являются прямыми, а вот эта функция их использует. ..".

Класс — это тип переменных, содержащий описание свойств и методов объектов, относящихся к этому классу.

Причем по способу описания класс очень похож на структуру. Основное отличие том, что по умолчанию все члены класса доступны только внутри этого класса. У структуры же все её члены доступны всем функциям нашей программы. Опишу общую схему создания нужного нам класса:

// описание класса (типа переменных)
class TestClass { // Создание типа

private:          // Описание приватных переменных и функций. 
                  //   Они будут доступны только функциям внутри класса 
  
// Описание данных ("свойств", или "полей" класса)
  double m_privateField; 

// Описание функций ("методов" класса)
  bool  Private_Method(){return false;} 

public:           // Описание общедоступных переменных и функций. 
                  //   Они будут доступны всем функциям, использующим объекты данного класса    

// Описание данных ("свойств", или "полей", или "членов" класса)   
  int m_publicField; 

// Описание функций ("методов" класса)   
  void Public_Method(void)
    {
     Print("Value of `testElement` is ",  testElement );   
    }
 }; 

Пример 9. Описание структуры класса

Ключевые слова public: и private: определяют зоны видимости членов класса.

Всё, что находится ниже слова public:, будет доступно "снаружи" класса, то есть для других функций нашей программы, даже не принадлежащих этому классу.

Всё, что выше этой секции (и ниже слова private:), будет "скрыто": доступ к этим элементам будут иметь только функции этого же класса.

Класс может содержать сколько угодно секций public: и private:.

Однако, несмотря на предложение в рамке, лучше использовать только по одному блоку на область видимости (один private: и один public:), чтобы все данные или функции с одинаковым доступом находились рядом. При этом некоторые опытные разработчики всё же предпочитают создавать четыре секции: по две (приватная и публичная) — для функций и по две — для переменных. Тут уж выбирайте сами.

В принципе, слово private: можно опустить, так как все члены класса, не описанные как public:, будут приватными по умолчанию (в отличие от структур). Но делать это не рекомендуется, поскольку читать такой код станет неудобно.

Нужно помнить, что в общем случае хотя бы одна функция в описанном классе должна быть "публичной", иначе класс будет бесполезен в большинстве случаев. Исключения бывают, но они редки.

Хорошим тоном в программировании считается в секции public: размещать только функции (а НЕ переменные), чтобы защитить данные. Это позволяет изменять переменные класса только с помощью методов данного класса. Такой подход повышает надёжность кода программы.

После того как класс описан, для его использования в нужном месте программы просто создаются переменные нужного типа. Создание переменных происходит как обычно. Доступ к методам и свойствам каждой такой переменной, как правило, происходит через символ точки, так же как в структурах:
// Описание переменной нужного типа
TestClass myTestClassVariable;

// Использование возможностей данной переменной
myTestClassVariable.testElement = 5;
myTestClassVariable.PrintTestElement();

Пример 10. Использование класса.

Чтобы проиллюстрировать работу публичных и приватных свойств, попробуйте вставить код примера 11 внутрь описания функции OnStart вашего скрипта и скомпилируйте файл. Скорее всего, компиляция пройдёт успешно.

Затем попробуйте раскомментировать строку "myVariable.a = 5;" и снова скомпилировать код. В этом случае вы получите ошибку компиляции, сообщающую о попытке доступа к приватным членам класса. Именно эта особенность компилятора позволяет исключить некоторые сложно уловимые ошибки, которые программисты могут совершить при работе в других подходах.

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

PrivateAndPublic myVariable;

// myVariable.a = 5; // Ошибка компилятора! 
myVariable.b = 10;   // Всё в порядке, так можно

Пример 11. Использование публичных и приватных свойств класса.

Если бы все классы приходилось писать самостоятельно, этот подход ничем не выделялся бы из всех остальных, и особого смысла в нём не было бы.

Однако, к моему счастью, множество стандартных классов уже находится в каталоге MQL5\Include. Так же довольно много очень полезных библиотек собрано в кодобазе. Во многих случаях нам достаточно просто подключить соответствующий файл (как описано ниже), чтобы воспользоваться наработками других умных людей. И это сильно облегчает работу программистов.

Об ООП пишут огромные книги, и он точно сто́ит отдельной статьи. Однако цель этой статьи лишь в том, чтобы дать представление новичку об использовании сложных типов данных в программах, которые ему будут встречаться. Вы теперь знаете, как описать простейший класс и как использовать чужие классы, поэтому я просто перейду к следующему разделу.


Функциональный тип данных (оператор typedef)

Предупреждение. Раздел может оказаться сложным для начинающих, поэтому при первом прочтении статьи его можно пропустить.

Понимание материала этого раздела никак не повлияет на усвоение всего остального материала — и даже, возможно, на ваш остальной путь в программировании. Большинство задач могут иметь множество решений, и без функциональных типов вполне можно обойтись.

Однако возможность назначать какой-нибудь переменной определенные функции (а, следовательно, в некоторых случаях использовать их как аргументы других функций) всё-таки бывает удобной, и я считаю, что об этой возможности надо знать, хотя бы для того, чтобы быть способным прочитать чужой код.

Иногда бывает полезно создавать переменные "функционального" типа, например, для того, чтобы передать их в качестве аргумента другой функции.

Например, в ситуации торговли приказы на покупку и на продажу очень сходны и отличаются только одним параметром. Однако цена покупки — всегда Ask, а продажа всегда происходит по цене Bid.

Часто программисты пишут свои функции для покупки и для продажи (Buy и Sell), которые учитывают все нюансы конкретного ордера. А потом пишут еще и функцию типа Trade, которая объединяет эти две возможности и одинаково выглядит как при торговле "вверх", так и при торговле "вниз". Это удобно, потому что Trade сама подставляет вызовы написанных функций Buy или Sell в зависимости от вычисленного направления движения цены, и программист может сосредоточить внимание на чем-то еще.

Можно придумать множество случаев, когда хочется сказать: "Автомат, сделай толково сам!" — и позволить функции самой решить, какой конкретно из "дихотомических" вариантов нужно вызывать в той или иной ситуации. При расчете "тейк профита" — прибавлять или убавлять количество пунктов к цене? А при расчете "стоп лосса"? При постановке ордера по экстремуму искать максимумы или минимумы? И так далее.

Вот в таких случаях иногда используется тот подход, что будет описан ниже.

Как обычно, сначала нужно описать тип нужной переменной. В данном случае этот тип описывается по следующему шаблону:

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

Пример 12. Шаблон для описания функционального типа.

Здесь:

  • function_result_type — это тип возвращаемого значения (любой допустимый, например, int, double или что угодно другое);
  • Function_type_name — имя типа, который мы будем использовать при создании переменных;
  • input_parameter1_type — тип первого параметра. Понятно, что список параметров подчиняется правилам обычных списков для функций.

Обратите внимание на звёздочку (*) перед именем типа. Она важна, и без неё ничего работать не будет.

Обозначает она, что в переменную этого типа будет записан не результат и не число, а сама функция, обладающая полным набором способностей, и, следовательно, данная переменная будет совмещать возможности, присущие как другим переменным, так и функциям.

Такая конструкция, которая при описании типа данных использует сам объект (функцию, объект какого-то класса — и т.д.), а не копию данных этого объекта или результат его работы, называется указатель.

Подробнее про указатели мы поговорим в будущих статьях. Посмотрим рабочий пример использования оператора typedef.

Пусть у нас есть функции Diff и Add, которые мы хотим назначать какой-то переменной. Обе функции возвращают целые значения и принимают по два целых параметра. Реализация у них элементарная:

//---
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;       //Как обычно, объявляем переменную описанного типа
 
    operate = Add;       // Записываем в переменную значение (в данном случае назначаем функцию)
    Print(operate(3, 5)); // Используем переменную как обычную функцию
                         // Вывод функции: 8

    operate=Diff;
    Print(operate(3, 5)); // Вывод функции: -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>     // Угловые скобки задают поиск относительно каталога MQL5\Include 
#include "AnyOtherPath.mqh" // Кавычки задают поиск относительно текущего файла

Пример 17. Две формы директивы #include.

Если компилятор встречает инструкцию #include в любом месте вашего кода, он старается вставить вместо этой инструкции содержимое указанного файла, но только один раз на программу. Если файл уже использован, второй раз он подключаться не будет.

Протестировать это утверждение можно с помощью скрипта, описанного в следующем разделе.

Чаще всего подключаемым файлам дают расширение *.mqh, поскольку это удобно, однако в общем случае расширение может быть любым.


Скрипт для проверки работы директивы #include

Для того, чтобы проверить действия компилятора при встрече данной директивы препроцессора, создадим два файла.

Для начала создадим файл с именем "1.mqh" в каталоге скриптов (MQL5\Scripts). Содержимое этого файла будет очень простым:

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

Пример 18. Простейший подключаемый файл может содержать лишь одну команду.

Что делает этот код, надеюсь, понятно. Предполагая, что где-то была описана переменная i, данный код создаёт сообщение для пользователя, присоединяя к сообщению значение переменной, а затем выводит это сообщение в журнал.

Переменная i будет маркером, обозначающим, в каком месте скрипта сработал (или не сработал) вызов данной инструкции. Ещё раз подчеркну, в этом файле ничего больше писать не нужно. Теперь в том же каталоге (там же, где находится файл "1.mqh") создадим скрипт, содержащий следующий код:

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

// Вывод скрипта:
// 
//   This is include with number 1
//
// Вторая попытка использования того же файла будет проигнорирована

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

Пример 19. Исследование повторного включения файлов.

В этом коде мы попытались использовать файл "1.mqh" дважды, чтобы получить два сообщения о срабатывании.

При запуске этого скрипта в терминале мы увидим, что первое сообщение отрабатывает, как и ожидалось, отображая в сообщении цифру 1, но второе сообщение не появляется.

Зачем так сложно? Почему не вставлять содержимое каждый раз?

Это важный принцип, поскольку подключаемые файлы часто содержат описания переменных и функций. Вы уже знаете, что в одной программе на глобальном уровне (вне всех функций) должна быть только одна переменная с определённым именем.

Если, допустим, описана переменная int a;, то второй раз точно такую же переменную на этом уровне описать нельзя, можно только использовать ту, что есть. С функциями чуть сложнее, но суть та же: каждая функция должна быть уникальна в пределах нашей программы. А теперь представьте, что программа использует два независимых модуля, но в каждом из них подключается один и тот же стандартный класс, находящийся в файле <Arrays\List.mqh> (рисунок 2).

Использование одного класса двумяя модулями

Рисунок 2. Использование одного класса двумя модулями.

Если бы не было "правила одного раза", компилятор выдал бы сообщение об ошибке, поскольку дважды описывать один и тот же класс запрещено. Но в данном случае такая конструкция вполне рабочая, поскольку после описания поля FieldOf_Module1 описание CList уже включено в списки компилятора, и поэтому просто использует это описание для модуля 2.

Понимая этот принцип, вы можете не бояться создавать даже "многослойные" вложения, например, когда какие-то элементы классов зависят друг от друга "циклически", как на рисунке 3.

Можно даже описывать внутри класса переменную этого же класса.

Всё это — допустимые конструкции, и так можно, именно потому, что #include срабатывает строго один раз для одного файла.

Циклическая зависимость: каждый класс содержит элементы, зависящие от другого

Рисунок 3. Циклическая зависимость: каждый класс содержит элементы, зависящие от другого.

В заключение раздела хочу еще раз напомнить, что файлы стандартных библиотек MetaTrader, которые вы можете подключать в ваш код, лежат в каталоге MQL5\Include. Для того чтобы легко открыть этот каталог в проводнике, можно выбрать в терминале MetaTrader меню "Файл"->"Открыть каталог данных" (рисунок 4).

Переход к каталогу данных

Рисунок 4. Переход к каталогу данных.

Если же вы хотите посмотреть на файлы этого каталога в MetaEditor достаточно в панели навигатора найти папку Include.  Свои подключаемые файлы можно создавать либо в том же каталоге (лучше в отдельных папках), либо можно использовать каталог вашей программы и его подкаталоги (см. комментарии в примере 17). Как правило, директивы #include используют в начале файла, до начала всех остальных действий. Однако это "правило", конечно, не строгое: всё зависит от конкретных задач.


Заключение

Ещё раз кратко пройдусь по тем темам, которые были рассмотрены в этой статье.

  1. Была описана директива препроцессора #include. Она позволяет включать в нашу программу дополнительные текстовые файлы, обычно — какие-то библиотеки.
  2. Были рассмотрены сложные типы данных: структуры, объединения и объекты (переменные на основе классов), а также — функциональные типы данных.

Надеюсь, теперь типы данных, описанные в этой статье, для вас "сложные" только по строению, но не по применению.

В отличие от простых типов, которые являются встроенными в язык, "сложные" типы сначала необходимо описать, и только потом создавать переменные. Но после того, как тип описан, работа с ним, по сути, ничем не отличается от работы с "простыми" типами и сводится к созданию переменных и вызовом компонентов (членов) у этих переменных, (если таковые предполагаются) или использованию имени переменной как имени функции, если создали функциональный тип.

Инициализация переменных, созданных с помощью структур, может проходить с помощью фигурных скобок.

И теперь, надеюсь, вы понимаете, что возможность создавать свои сложные типы и разбивать программу на модули, хранящиеся во внешних файлах, делает разработку программы гибкой и удобной.


Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
MrBrooklin
MrBrooklin | 19 июл. 2024 в 05:52

Отношу себя к новичку, который изучил азы в программировании. Прочитал Вашу очередную статью и сделал вывод: новичок с полным отсутствием каких-либо знаний ничего не поймёт из этой статьи. Это моё личное мнение, не претендующее на истину в последней инстанции.

Возьмём для примера раздел статьи Структуры. Начало хорошее и достаточно понятное. Вы рассказали для чего нужна структура и показали как её создать. А затем бабах и новый код!

IntradayTime dealEnterTime;

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

Специально выделил эту часть кода. Вот что должен понять новичок с нулевыми знаниями из этой строки? Что это такое для него? Мне-то уже понятно, а вот для новичка с полным отсутствием знаний это очередной непонятный фрагмент кода. Поэтому желательно расписывать и полностью разжёвывать каждую строчку. Иначе получается, что эта статья не для новичков, а для уже продвинутых программистов.

С уважением, Владимир.

Oleh Fedorov
Oleh Fedorov | 19 июл. 2024 в 08:32
MrBrooklin #:

Отношу себя к новичку, который изучил азы в программировании. Прочитал Вашу очередную статью и сделал вывод: новичок с полным отсутствием каких-либо знаний ничего не поймёт из этой статьи. Это моё личное мнение, не претендующее на истину в последней инстанции.

Возьмём для примера раздел статьи Структуры. Начало хорошее и достаточно понятное. Вы рассказали для чего нужна структура и показали как её создать. А затем бабах и новый код!

Специально выделил эту часть кода. Вот что должен понять новичок с нулевыми знаниями из этой строки? Что это такое для него? Мне-то уже понятно, а вот для новичка с полным отсутствием знаний это очередной непонятный фрагмент кода. Поэтому желательно расписывать и полностью разжёвывать каждую строчку. Иначе получается, что эта статья не для новичков, а для уже продвинутых программистов.

С уважением, Владимир.

Мне кажется - или именно эту структуру я создавал тремя строками раньше? И две строки назад объяснил, что это - тип данных? И это должно значить, что использовать этот тип нужно так же, как все остальные? (Правда, тут уже логика должна подключиться, да ;-)

Хотя, вероятно, Вы и правы, комментарий как минимум к типу не помешал бы... Спасибо.

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Метод Швефеля, Бокса-Мюллера Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Метод Швефеля, Бокса-Мюллера
Эта статья представляет увлекательное погружение в мир социального поведения живых организмов и его влияние на создание новой математической модели — ASBO (Adaptive Social Behavior Optimization). Мы рассмотрим, как принципы лидерства, соседства и сотрудничества, наблюдаемые в обществах живых существ, вдохновляют разработку инновационных алгоритмов оптимизации.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Модифицированный советник Grid-Hedge в MQL5 (Часть III): Оптимизация простой хеджирующей стратегии (I) Модифицированный советник Grid-Hedge в MQL5 (Часть III): Оптимизация простой хеджирующей стратегии (I)
В третьей части мы вернемся к советникам Simple Hedge и Simple Grid, разработанным ранее. Теперь мы займемся совершенствованием советника Simple Hedge с помощью математического анализа и подхода грубой силы (brute force) с целью оптимального использования стратегии. Эта статья углубляется в математическую оптимизацию стратегии, закладывая основу для будущего исследования оптимизации на основе кода в последующих частях.