Основы программирования на MQL5 - Массивы

Dmitry Fedoseev | 3 ноября, 2012

Введение

Наряду с переменными и функциями, массивы являются практически неотъемлемой частью любого языка программирования. Замечено, что некоторые начинающие изучать программирование, панически боятся массивов. Удивительно, но факт! Смею заверить вас, что бояться их не нужно. Массивы, по своей сути, это все те же обычные переменные. Если не вдаваться в особенности нотации, то нет большой разницы, записать ли выражение с использованием простых переменных:

Variable0=1;
Variable1=2;

Variable2=Variable0+Variable1;

или же использовать массивы:

double Variable[3];

Variable[0]=1;
Variable[1]=2;

Variable[2]=Variable[0]+Variable[1];

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

Может быть, сложности применения массивов каким-то образом связаны с использованием символов "[" и "]"? Эти символы редко когда используются, кроме как в программировании при работе с массивами, поэтому их расположение на клавиатуре может забываться и вызывать неудобства. На самом деле, их расположение на клавиатуре очень легко запомнить - эти две клавиши рядом с клавишей "Enter" расположены в логическом порядке: открывающая скобка и закрывающая скобка.


Определение и основные свойства массива

Итак, массив - это пронумерованный набор переменных с одинаковым именем. Основные свойства массива это: имя массива, тип переменных массива (int, double и пр.) и размер массива. Отсчет элементов массива начинается с нуля. В отношении нумерации элементов массива лучше употреблять слово "индекс", но не "номер", что как бы подразумевает, что отсчет выполняется с нуля (а нумерация обычно выполняется с единицы). При такой индексации, последний элемент имеет значение индекса на единицу меньше чем количество элементов в массиве.

Если массив объявлен следующим образом:

double Variable[3];

он имеет следующие элементы: Variable[0], Variable[1], Variable[2].

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

Для определения размера массива в языке MQL5 используется функция ArraySize():

double Variable[3];

int Size=ArraySize(Variable);

После выполнения этого кода переменная Size будет иметь значение 3.


Статические массивы и динамические массивы

Массивы могут быть статическими и динамическими. Если размер массива указан при его объявлении, массив является статическим:

double Variable[3];

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

#define SIZE 3

double Variable[SIZE];

Если при объявлении массива не указать его размер, массив будет динамическим:

double Variable[];

Прежде чем использовать такой массив, ему нужно установить размер. Установка размера выполняется функцией ArrayResize():

ArrayResize(Variable,3);

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

Если массив нужно полностью освободить, используется функция ArrayFree():

ArrayFree(Variable);

При выполнении этой функции массиву устанавливается размер 0. Действие этой функции идентично действию:

ArrayResize(Variable,0);

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

Выяснить, является ли массив статическим или динамическим, можно с помощью функции ArrayIsDynamic():

bool dynamicArray=ArrayIsDynamic(Variable);

Если массив динамический, у переменной dynamicArray будет значение true, если статический - false.


Инициализация массива

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

Объявляем массив и сразу присваиваем значения его элементам:

string Variable[] = {"Кнопка 1", "Кнопка 2", "Кнопка 3"};

При таком объявлении, не смотря на то, что размер массива не указан, он все равно является статическим, потому что количество его элементов определено списком значений (в фигурных скобках).

Не будет ошибкой, если вы укажите количество элементов массива:

string Variable[3] = {"Кнопка 1", "Кнопка 2", "Кнопка 3"};

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

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

ArrayInitialize(Variable,1);

После выполнения этого кода у всех элементов массива Var будет значение 1. Для присвоения одинакового значения только некоторым элементам массива используется функция ArrayFill():

double Variable[4];

ArrayFill(Variable,0,2,1);
ArrayFill(Variable,2,2,2);

После выполнения этого кода элементы 0 и 1 будут иметь значения 1, а элементы 2 и 3 значения 2.


Перебор массива в цикле

Чаще всего работа с массивами ведется с использованием цикла for. Если используется статический массив и его размер известен заранее, в зависимости от решаемой задачи, проходим по массиву в прямом или обратном направлении:

//--- в прямом направлении
for(int i=0; i<SIZE; i++){ 
  // здесь какие-нибудь действия с элементом Variable[i]
}

//--- в обратном направлении
for(int i=SIZE-1; i>=0; i--){
  // здесь какие-нибудь действия с элементом Variable[i]
}

Если массив динамический, непосредственно перед выполнением цикла необходимо объявить переменную для размера массива, получить размер массива, затем выполнить цикл:

int Size=ArraySize(Var);

for(int i=0; i<Size; i++){
  // здесь какие-нибудь действия с элементом Variable[i]
}

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

for(int i=0; i<ArraySize(Variable); i++){
   // здесь какие-нибудь действия с элементом Variable[i]
}
Не рекомендуется использовать такой код.

Если же алгоритм программы допускает проход цикла в обратном направлении, можно не использовать переменную для размера:

for(int i=ArraySize(Variable)-1; i>=0; i--){
  // здесь какие-нибудь действия с элементом Variable[i]
}

В этом случае функция ArraySize() будет вызвана только один раз в начале цикла, и цикл будет работать быстро.


Многомерные массивы

Пока что рассматривались только одномерные массивы, их можно представить в виде линейки:

Одномерный массив

Массивы могут быть многомерными. Если у одномерного массива одному индексу соответствует одно значение, то у многомерного несколько значений. Многомерные массивы объявляются следующим образом:

double Variable[10][3];

Это означает, что в первом измерении массива находится десять элементов, во втором - три. Графически такой массив можно изобразить следующим образом:

Многомерный массив

Для простоты понимания двухмерный массив можно представить как плоскость. Размер первого измерения определяет длину, размер второго - ширину, а значение элемента определяет характеристику данной точки плоскости, например - высоту над уровнем моря.

Так же массив может быть 3-х мерным:

double Variable[10][10][10];

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

В MQL5 максимальное количество измерений у массива равно 4.

Многомерный массив может быть статическим или динамическим только по первому измерению, все последующие измерения являются статическими. Таким образом, функция ArrayResize() позволяет менять только размер по первому измерению. Размеры по остальным измерениям должны указываться при объявлении массива:

double Variable[][3][3];

При определении размера многомерного массива функцией ArraySize() следует учитывать одну особенность: при изменении размера массива функцией ArrayResize(), второй параметр функции означает размер первого измерения массива, однако функция ArraySize() возвращает не размер первого измерения, а общее количество элементов:

double Variable[][3][3]; 

ArrayResize(Variable,3); 
int Size = ArraySize(Variable);

После выполнения этого кода переменная Size будет иметь значение 27. Помните об этой особенности при переборе многомерных массивов в цикле, если хотите вычислить размер первого измерения:

double Variable[][3][3];
 
ArrayResize(Variable,3); 

int Size=ArraySize(Variable)/9; // Определение размера первого измерения

for(int i=0; i<Size; i++) {
   for(int j=0; j<3; j++) {
      for(int k=0; k<3; k++) {
            //  здесь какие-нибудь действия с элементом Var[i][j][k];
      }   
   }   
}

Как упоминалось ранее, при кодировании желательно придерживаться принципа сокращения редактируемых мест для выполнения дальнейших доработок. В только что приведенном примере кода использовалась число 9, однако его можно вычислить. Для этого можно воспользоваться функцией ArrayRange(), которая возвращает количество элементов в указанном измерении массива. Если количество измерений массива известно, можно выполнить простое вычисление:

int Elements=ArrayRange(Variable,1)*ArrayRange(Variable,2);
int Size=ArraySize(Variable)/Elements;

Можно сделать универсально:

int Elements=1; // Для одномерного массива один элемент
int n=1; // Начнем со второго измерения (измерения пронумерованы от нуля)

while(ArrayRange(Variable,n) > 0){ // До тех пор, пока в измерении есть элементы
   Elements*=ArrayRange(Variable,n); // Умножение количества элементов
   n++; // Увеличение номера измерения
}

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

#define SIZE1 3
#define SIZE2 3
#define TOTAL SIZE1*SIZE2 

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

Допустим, имеется массив:

double Variable[3][3];

Этот массив состоит из трех массивов по три элемента:

double Variable[][3]={{1, 2, 3},{ 4, 5, 6},{7, 8, 9}};

Аналогично с трехмерным массивом, запись можно делать построчно, что бы было легче ориентироваться в структуре массива:

double Variable[][3][3]={
   {
      {1, 2, 3},
      {4, 5, 6},
      {7, 8, 9}
   },
   {
      {10, 20, 30},
      {40, 50, 60},
      {70, 80, 90}
   },
   {
      {100, 200, 300},
      {400, 500, 600},
      {700, 800, 900}
   }
};

Инициализация многомерного массива функцией ArrayInitialize() выполняется точно так же, как инициализация одномерного массива:

ArrayInitialize(Variable,1);

После выполнения этого кода все элементы массива будут иметь значение 1. Так же и с функцией ArrayFill():

double var[3][3][3];

ArrayFill(Variable,0,9,1);
ArrayFill(Variable,9,9,10);
ArrayFill(Variable,18,9,100);

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


Передача массива в функцию

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

Если переменная передается в функцию обычным образом (по значению), значение переданной переменной не может быть изменено функцией:

int x=1;
Func(x);

void  Func(int arg){
   arg=2;
}

После выполнения функции Func(), значение x остается равным 1.

Если переменная передается по ссылке (обозначается знаком &), функция может изменить значение переданной ей переменной:

int x=1;
Func(x);

void  Func(int &arg){
   arg=2;
}

После выполнения функции Func() значение x становится равным 2.

При передаче массива в функцию необходимо указать, что аргумент передается по ссылке и что передается массив (квадратные скобки):

void Func(double &arg[]){
   // ...
}

При передаче многомерных массивов в функцию необходимо указывать размеры измерений кроме первого:

double var[][3][3];

void Func(double &arg[][3][3]){
   // ...
}

В этом случае предпочтительнее пользоваться константами:

#define SIZE1 3
#define SIZE2 3

double Var[][SIZE1][SIZE2];

void Func(double &arg[][SIZE1][SIZE2]){
   // ...
}


Сохранение и загрузка массивов из файла

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

bool SaveArrayToFile(string FileName,string &Array[])
  {
//--- Открытие файла
   int h=FileOpen(FileName,FILE_TXT|FILE_WRITE);
   if(h==-1) return(false); // Ошибка открытия файла
//--- Запись в файл
   FileWriteInteger(h,ArraySize(Array),INT_VALUE); // Запись размера массива
   FileWriteArray(h,Array); // Запись массива
//--- Закрытие файла
   FileClose(h);
   return(true); // Сохранение выполнено
  }

Получилась вполне универсальная функция для сохранения одномерных массивов.

При загрузке из файла сначала прочитаем размер массива, затем изменим размер массива и прочитаем массив:

bool LoadArrayFromFile(string FileName,double &Array[])
  {
//--- Открытие файла
   int h=FileOpen(FileName,FILE_BIN|FILE_READ);
   if(h==-1) return(false); // Ошибка открытия файла
//--- Чтение файла
   int Size=FileReadInteger(h,INT_VALUE); // Чтение количества элементов массива
   ArrayResize(Array,Size); // Изменение размера массива. 
                            // У одномерного массива размер первого измерения равен количеству элементов массива.
   FileReadArray(h,Array); // Чтение массива из файла
//--- Закрытие файла
   FileClose(h);
   return(true); // Чтение выполнено
  }

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

bool LoadArrayFromFile3(string FileName,double &Array[][SIZE1][SIZE2])
  {
//--- Открытие файла
   int h=FileOpen(FileName,FILE_BIN|FILE_READ);
   if(h==-1)return(false); // Ошибка открытия файла
//--- Чтение файла   
   int SizeTotal=FileReadInteger(h,INT_VALUE); // Чтение количества элементов массива
   int Elements=SIZE1*SIZE2; // Вычисляем количество элементов 
   int Size=SizeTotal/Elements; // Вычисляем размер первого измерения
   ArrayResize(Array,Size); // Изменение размера массива
   FileReadArray(h,Array); // Чтение массива
//--- Закрытие файла
   FileClose(h);
   return(true); // Чтение выполнено
  }

Может получиться так, что в файл сохранялся массив, например, с размерами 2 и 3, а мы пытаемся читать его как массив 3 на 3. Вы можете проконтролировать соответствие размеров - умножьте рассчитанный размер первого измерения на количество элементов, если полученное значение будет равным общему количеству элементов массива, значит, есть соответствие.

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

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

Универсальность в этом случае не нужна: все равно через внешние параметры программы не будет происходить управление размерами измерений массива (кроме первого измерения). Если же вам понадобится возможность управления размерами других измерений, это задачу можно решить путем использования многомерного массива заведомо большего размера и дополнительных переменных, либо при помощи объектно ориентированного программирования (ООП). Мы рассмотрим второй подход немного позже в этой статье.


Применение динамических массивов

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

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

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

class CDynamicArray
  {
private:
   int               m_ChunkSize;    // Размер блока
   int               m_ReservedSize; // Реальный размер массива
   int               m_Size;         // Количество занятых элементов массива
public:
   double            Element[];      // Собственно массив. Находится в секции public, 
                                     // чтобы в случае необходимости работать с ним напрямую
   //+------------------------------------------------------------------+
   //|   Конструктор                                                    |
   //+------------------------------------------------------------------+
   void CDynamicArray(int ChunkSize=1024)
     {
      m_Size=0;                            // Количество занятых элементов
      m_ChunkSize=ChunkSize;               // Размер блока
      m_ReservedSize=ChunkSize;            // Реальный размер массива
      ArrayResize(Element,m_ReservedSize); // Подготовка массива
     }
   //+------------------------------------------------------------------+
   //|   Функция добавления в конец массива                             |
   //+------------------------------------------------------------------+
   void AddValue(double Value)
     {
      m_Size++; // Увеличение количества занятых элементов
      if(m_Size>m_ReservedSize)
        { // Требуемое количество больше реального размера массива
         m_ReservedSize+=m_ChunkSize; // Рассчитываем новый размер массива
         ArrayResize(Element,m_ReservedSize); // Увеличиваем реальный размер массива
        }
      Element[m_Size-1]=Value; // Добавляем значение
     }
   //+------------------------------------------------------------------+
   //|   Функция получения количества занятых элементов массива         |
   //+------------------------------------------------------------------+
   int Size()
     {
      return(m_Size);
     }
  };

Этот класс находятся в файле "CDynamicArray.mqh" приложения. Файл должен располагаться в каталоге "MQL5\Include" каталога данных терминала.

Сравним быстродействие при последовательном увеличении массива на 1 и при увеличении размера массива блоками:

int n=50000;
   double ar[];
   CDynamicArray da;

//--- Вариант 1 (увеличение размера по 1-му элементу)
   long st=GetTickCount(); // Запомним время начала 
   ArrayResize(ar,0); // Установка нулевого размера массива 
   for(int i=0;i<n;i++)
     {
      ArrayResize(ar,i+1); // Последовательное изменение размера массива
      ar[i]=i;
     }
   Alert("Вариант 1: "+IntegerToString(GetTickCount()-st)+" мс"); // Сообщение о времени, затраченном на первый вариант

//--- Вариант 2 (увеличение размера блоками)
   st=GetTickCount(); // Запомним время начала 
   for(int i=0;i<n;i++)
     {
      da.AddValue(i); // Добавляем элемент
     }
   Alert("Вариант 2: "+IntegerToString(GetTickCount()-st)+" мс"); // Сообщение о времени, затраченном на второй вариант

  }

Этот тест выполнен в виде скрипта, находится в файле "sTest_Speed.mq5" приложения. Файл должен располагаться в каталоге" MQL5\Scripts" каталога данных терминала.

На работу первого варианта затрачено несколько секунд, второй вариант отработал практически мгновенно.


Направление индексации массива

Обычно при изменении размера массива новые элементы добавляются в конец массива:

double ar[]; // Массив
ArrayResize(ar,2); // Подготовка массива
ar[0]=1; // Установка значений
ar[1]=2; 
ArrayResize(ar,3); // Увеличение размера массива
ar[2]=3; // Установка значения новому элементу массива
Alert(ar[0]," ",ar[1]," ",ar[2]); // Вывод значений массива

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

Массивы могут иметь обратную индексацию элементов. Тип индексации устанавливается функцией ArraySetAsSeries():

ArraySetAsSeries(ar,true); // установка обратной индексации
ArraySetAsSeries(ar,false); // установка нормальной индексации

При изменении размера массива с обратной индексацией, новый элемент добавляется в начало массива:

double ar[]; // Массив
ArrayResize(ar,2); // Подготовка массива
ar[0]=1; // Установка значений
ar[1]=2; 
ArraySetAsSeries(ar,true); // Смена направления индексации
ArrayResize(ar,3); // Увеличение размера массива
ar[0]=3; // Установка значения новому элементу массива
Alert(ar[0]," ",ar[1]," ",ar[2]); // Вывод значений массива

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

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

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

Узнать направление индексации массива можно функцией ArrayIsSeries():

bool series=ArrayIsSeries(ar);

Если массив имеет обратную индексацию, функция возвращает true.

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


Копирование массивов

Самый простой способ - пройтись по массиву в цикле и поэлементно копировать элемент одного массива в другой, однако в MQL5 существует специальная функция для копирования массивов - ArrayCopy():

double ar1[]={1,2,3};
double ar2[];

ArrayCopy(ar2,ar1);

После выполнения этого кода массив ar2 будет состоять из трех элементов, которые будут иметь такие же значения как массиве ar1: 1, 2, 3.

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

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

Функцию ArrayCopy() можно использовать не только для копирования элементов одного массива в другой массив, но и для копирования элементов одного и того же массива:

double ar1[]={1,2,3,4,5};
ArrayCopy(ar1,ar1,1,2);

Берем данные, начиная с элемента с индексом 2, и располагаем их, начиная с индекса 1. После выполнения этого кода в массиве будут значения: 1, 3, 4, 5, 5.

Функция ArrayCopy(), так же, позволят сдвинуть данные вправо:

double ar1[]={1,2,3,4,5};
ArrayCopy(ar1,ar1,2,1);

Берем данные начиная с элемента с индексом 1 и располагаем их начиная с индекса 2. После выполнения этого кода в массиве будут значения: 1, 2, 2, 3, 4.

Функция ArrayCopy() применима и к многомерным массивам, функция работает так, как будто массив одномерен, а все его элементы расположены последовательно:

double ar[3][2]={{1, 2},{3, 4},{5, 6}};
ArrayCopy(ar,ar,2,4);

После выполнения этого кода в массив будет иметь следующий вид: {1, 2}, {5, 6}, {5, 6}.


Сортировка массива

Сортировка или упорядочивание массива может выполняться при помощи функции ArraySort():

double ar[]={1,3,2,5,4};
ArraySort(ar);

После выполнения этого кода значения в массиве будут располагаться в следующем порядке: 1, 2, 3, 4, 5.

Функция ArraySort() неприменима к многомерным массивам. По вопросу сортировки многомерных массивов или структур данных можно обратиться к статье Электронные таблицы на MQL5.


Для выполнения бинарного поиска применяется функция ArrayBsearch(). Эта функция будет правильно работать только с упорядоченным (отсортированным) массивом. Бинарный поиск имеет такое название потому, что при его выполнении происходит последовательное деление массива на две части. Сначала искомое значение сравнивается с центральным элементом массива. Так определяется половина, в которой находится элемент: в начальной или в конечной. Затем выполняется сравнение с центральным элементом половины, и т.д.

Функция ArrayBsearch() возвращает индекс элемента с искомым значением:

double ar[]={1,2,3,4,5};

int index=ArrayBsearch(ar,3);

После выполнения этого кода у переменной index будет значение 2.

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

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

Для поиска в неупорядоченном массиве существует только один способ - перебор массива:

int FindInArray(int &Array[],int Value){
   int size=ArraySize(Array);
      for(int i=0; i<size; i++){
         if(Array[i]==Value){
            return(i);
         }
      }
   return(-1);
}

В этом примере функция возвращает индекс элемента с искомым значением. Если элемент с искомым значением отсутствует, функция возвращает -1.


Поиск максимума и минимума

Для поиска максимального и минимального значений в массиве используются функции ArrayMaximum() и ArrayMinimum(). Функции возвращают индекс элемента с максимальным и минимальным значением, соответственно:

double ar[]={3,2,1,2,3,4,5,4,3};

int MaxIndex=ArrayMaximum(ar);
int MinIndex=ArrayMinimum(ar);
double MaxValue=ar[MaxIndex];
double MinValue=ar[MinIndex];

После выполнения этого кода в переменной MaxIndex значение 6, а переменной MinIndex будет значение 2, в переменной MaxVaue будет значение 5, а в переменной MinValue будет значение 1.

Функции ArrayMaximum() и ArrayMinimum() позволяют ограничить диапазон поиска в массиве - указать индекс элемента, с которого выполняется поиск, и количество элементов, которые необходимо просмотреть:

int MaxIndex=ArrayMaximum(ar,5,3);
int MinIndex=ArrayMinimum(ar,5,3);

В этом случае в MaxIndex будет значение 6, в MinIndex - 5. Обратите внимание, в указанном диапазоне минимальное значение 4 встречается два раза: на позиции 5 и на позиции 7, функция вернула индекс элемента, ближайшего к началу массива. Таким же образом эти функции работают и с массивами, имеющими обратную индексацию - возвращают наименьший индекс.

На этом все стандартные функции языка MQL5 для работы с массивами рассмотрены.


Многомерный массив с применением ООП

Набор классов для создания многомерного массива включает в себя три класса: базовый и два потомка. В зависимости от потомка, выбранного на этапе создания объекта, объект может представлять собой массив переменных double или массив объектов. Каждый элемент массива объектов может представлять собой другой массив объектов или массив переменных.

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

//+------------------------------------------------------------------+
//|   Базовый класс                                                  |
//+------------------------------------------------------------------+
class CArrayBase
  {
public:
   CArrayBase       *D[];
   double            V[];

   void ~CArrayBase()
     {
      for(int i=ArraySize(D)-1; i>=0; i--)
        {
         if(CheckPointer(D[i])==POINTER_DYNAMIC)
           {
            delete D[i];
           }
        }
     }
  };
//+------------------------------------------------------------------+
//|   Класс-потомок 1                                                |
//+------------------------------------------------------------------+
class CDim : public CArrayBase
  {
public:
   void CDim(int Size)
     {
      ArrayResize(D,Size);
     }
  };
//+------------------------------------------------------------------+
//|   Класс-потомок 1                                                |
//+------------------------------------------------------------------+
class CArr : public CArrayBase
  {
public:
   void CArr(int Size)
     {
      ArrayResize(V,Size);
     }
  };

Эти классы находятся в файле "CMultiDimArray.mqh" приложения. Файл должен располагаться в каталоге "MQL5\Include" каталога данных терминала.

Пока применим этот класс для создания подобия одномерного массива:

CArrayBase * A; // Объявление указателя
   A=new CArr(10); // Загружаем экземпляр класса потомка, масштабирующего массив переменных. 
                   // Массив будет состоять из 10 элементов.

//--- Теперь массивом можно пользоваться:
   for(int i=0; i<10; i++)
     {
      //--- Присваиваем каждому элементу массива последовательные значениея от 1 до 10
      A.V[i]=i+1;
     }
   for(int i=0;i<10;i++)
     {
      //--- Проверяем значения
      Alert(A.V[i]);
     }
   delete A; // Удаление объекта
  }

Этот пример выполнен в виде скрипта, который находится в файле "sTest_1_Arr.mq5" приложения. Файл должен располагаться в каталоге "MQL5\Scripts" каталога данных терминала.

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

CArrayBase*A;  // Объявление указателя
   A=new CDim(3); // Первое измерение представляет собой массив объектов

//--- Каждый объект первого измерения представляет собой массив переменных разного размера 
   A.D[0]=new CArr(1);
   A.D[1]=new CArr(2);
   A.D[2]=new CArr(3);
//--- Присваиваем значения
   A.D[0].V[0]=1;

   A.D[1].V[0]=10;
   A.D[1].V[1]=20;

   A.D[2].V[0]=100;
   A.D[2].V[1]=200;
   A.D[2].V[2]=300;
//--- Проверяем значения
   Alert(A.D[0].V[0]);

   Alert(A.D[1].V[0]);
   Alert(A.D[1].V[1]);

   Alert(A.D[2].V[0]);
   Alert(A.D[2].V[1]);
   Alert(A.D[2].V[2]);
//---
   delete A; // Удаление объекта

Этот пример выполнен в виде скрипта, находится в файле "sTest_2_Dim.mq5" приложения. Файл должен располагаться в каталоге "MQL5\Scripts" каталога данных терминала.

Полученные массивы являются как бы статическими, так как в классах нет методов для изменения размеров массивов, но массивы D[] и V[] находятся в открытой секции класса, поэтому они доступны для любых манипуляций с ними. Можно без каких-либо сложностей масштабировать массив V[]. При масштабировании массивов D[] и при сокращении их размеров предварительно следует выполнять удаление объектов, на которые указывают удаляемые элементы, а при увеличении размера - загружать в них объекты.

При желании можно придумать и другие способы реализации многомерных массивов при помощи ООП или структур данных.


Заключение

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

  1. ArraySize(), ArrayResize() - функции без которых невозможно обойтись.

  2. ArrayMaximum(), ArrayMinimum(), ArrayCopy(), ArrayInitialize(), ArrayFill(), ArrayFree() - функции, значительно упрощающие работу с массивами.

  3. ArraySort() - важная и полезная функция, но она редко используется из-за ее недостаточной функциональности.

  4. ArrayBsearch() - редко используемая функция, но очень важная в редких исключительных случаях.

  5. ArraySetAsSeries(), ArrayRange(), ArrayGetAsSeries(), ArrayIsDynamic(), ArrayIsSeries() - очень редко используемые функции, почти невостребованные.

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