Внешние переменные

Материал данного раздела является одновременно сложным и необязательным. Он требует знания концепций, некоторые из которых основываются на аналогии с языком C++, а некоторые изучаются позднее в этой книге. В то же время, эффект от описываемой языковой конструкции может быть достигнут другим способом, а её гибкость — потенциальный источник ошибок.

MQL5 позволяет описывать переменные внешними. Это делается с помощью ключевого слова extern и допускается только в глобальном контексте.

Для внешней переменной синтаксис в целом повторяет обычное описание, с добавлением extern, но инициализация при этом запрещена:

extern type identifier;

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

Предполагается, что в одном из файлов данная переменная будет полностью определена. Если переменная нигде не определена без ключевого слова extern, выдается ошибка компиляции "unresolved extern variable" (аналогично ошибке линковщика в C++ в подобных случаях).

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

Применение extern в MQL5 не является столь же насущным, как в C++, и может быть, в большинстве случаев, заменено на включение заголовочного файла с общими определениями тех переменных, которые предполагалось объявить extern. Эти определения достаточно выполнить обычным способом. Компилятор гарантирует, что каждый подключенный файл добавляется в исходный код лишь однажды. А учитывая, что в MQL5 программа всегда состоит из одного компилируемого модуля mq5, здесь не существует проблемы C++ с потенциальной ошибкой множественного определения одной и той же переменной за счет подключения заголовка в разные модули.

Даже если в директиве #include подключен некий дополнительный mq5-файл (а не mqh), он не конкурирует на равных с главным модулем, для которого запущена компиляция, а рассматривается как один из заголовков.

В отличие от C++, MQL5 не позволяет указывать для внешней переменной начальное значение (в C++ инициализация приводит к тому, что слово extern игнорируется). Попытка указать начальное значение сгенерирует при компиляции ошибку "extern variable initialization is not allowed".

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

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

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

extern return_type name([параметры]);
       return_type name([параметры]);

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

Вы можете использовать extern как в компилируемом mq5-модуле, так и в подключаемых заголовочных файлах.

Рассмотрим несколько вариантов применения extern: они разнесены по разным файлам — главный скрипт ExternMain.mq5 и 3 подключаемых файла: ExternHeader1.mqh, ExternHeader2.mqh, ExternCommon.mqh.

В главном файле подключены только ExternHeader1.mqh и ExternHeader2.mqh, а файл ExternCommon.mqh нам потребуется чуть позже.

// исходный код из mqh-файлов будет неявным образом подставлен
// в главный mq5-файл вместо этих директив
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"

В заголовочных файлах определены две условно-полезные функции: в первом — функция inc для инкремента переменной x, во втором — функция dec для декремента переменной x. Как раз переменная x описана в обоих файлах как внешняя:

// ExternHeader1.mqh
extern int x;
void inc()
{
   x++;
}
// -----------------
// ExternHeader2.mqh
extern int x;
void dec()
{
   x--;
}

За счет этого описания каждый из mqh-файлов нормально компилируется. И когда они вместе включены в mq5-файл, вся программа целиком также компилируется.

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

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

int x = 2;
   
void OnStart()
{
   inc();  // использует x
   dec();  // использует x
   Print(x); // 2
   ...
}

В файле ExternHeader1.mqh есть определение переменной short z (без extern). В главном скрипте закомментировано похожее определение. Если эту строку сделать активной, мы получим упомянутую раньше ошибку о повторном определении ("variable already defined"). Это сделано для наглядной демонстрации потенциальной проблемы.

Также в ExternHeader1.mqh описана extern long y. При этом в файле ExternHeader2.mqh одноименная внешняя переменная имеет другой тип: extern short y. Если бы не тот факт, что последнее описание предусмотрительно "убрано" в комментарий, здесь возникла бы ошибка о несовместимости типов ("variable 'y' already defined with different type"). Резюме: типы либо должны совпадать, либо переменные не должны быть внешними. Если оба варианта не подходят, значит — опечатка в имени одной из переменных.

Дополнительно следует обратить внимание на то, что переменная y не инициализируется явно. Однако главный скрипт успешно к ней обращается и выводит в журнал 0:

long y;
   
void OnStart()
{
   ...
   Print(y); // 0
}

Наконец, в скрипте предусмотрена возможность попробовать альтернативу внешним переменным-двойникам на примере уже известной переменной x. Вместо описания extern int x каждый из файлов ExternHeader1.mqh и ExternHeader2.mqh способен подключить другой общий заголовок ExternCommon.mqh, в котором находится определение int x (без extern). Оно становится единственным определением x в проекте.

Этот альтернативный режим сборки программы включается при активации макроса USE_INCLUDE_WORKAROUND: он присутствует в комментарии в начале скрипта:

#define USE_INCLUDE_WORKAROUND // эта строка была в комментарии
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"

В такой конфигурации компилируемость отдельных включаемых файлов сохранится, как и всего проекта в целом. В реальном проекте, при использовании данного способа, общий mqh-файл был бы подключен в ExternHeader1.mqh и ExternHeader2.mqh безусловно (без условий на USE_INCLUDE_WORKAROUND). В данном примере переключение между двумя ветками инструкций на основе USE_INCLUDE_WORKAROUND нужно только для демонстрации обоих режимов. Например, ExternHeader2.mqh в упрощенной версии должен выглядеть так:

// ExternHeader2.mqh
#include "ExternCommon.mqh" // int x; теперь здесь
 
void dec()
{
   x--;
}

В журнале MetaEditor можно убедиться, что файл ExternCommon.mqh подгружается единожды, несмотря на то, что на него есть ссылка и в ExternHeader1.mqh, и в ExternHeader2.mqh.

'ExternMain.mq5'
'ExternHeader1.mqh'
'ExternCommon.mqh'
'ExternHeader2.mqh'
code generated

Если переменная x "прописалась" в ExternCommon.mqh, мы уже не имеем права переопределять её (без extern) в главном или любом другом модуле, т.к. будет ошибка компиляции, но мы можем просто присвоить ей нужное значение в начале алгоритма.