Основы программирования на MQL5 - Массивы
Наряду с переменными и функциями, массивы являются практически неотъемлемой частью любого языка программирования. Замечено, что некоторые начинающие изучать программирование, панически боятся массивов. Удивительно, но факт! Смею заверить вас, что бояться их не нужно. Массивы, по своей сути, это все те же обычные переменные. Если не вдаваться в особенности нотации, то нет большой разницы, записать ли выражение с использованием простых переменных:
или же использовать массивы:
Как видим, разница не очень велика, разве что при использовании массивов в именах переменных есть квадратные скобки. Есть еще одно более существенное отличие - при объявлении переменных нужно специально записывать имя каждой переменной, а при объявлении массива достаточно записать имя один раз, а в квадратных скобках указать количество переменных (количество элементов массива). При решении многих практических задач программирования, преимущества применения массивов по сравнению с переменными будут еще более очевидны.
Может быть, сложности применения массивов каким-то образом связаны с использованием символов "[" и "]"? Эти символы редко когда используются, кроме как в программировании при работе с массивами, поэтому их расположение на клавиатуре может забываться и вызывать неудобства. На самом деле, их расположение на клавиатуре очень легко запомнить - эти две клавиши рядом с клавишей "Enter" расположены в логическом порядке: открывающая скобка и закрывающая скобка.
Определение и основные свойства массиваИтак, массив - это пронумерованный набор переменных с одинаковым именем. Основные свойства массива это: имя массива, тип переменных массива (int, double и пр.) и размер массива. Отсчет элементов массива начинается с нуля. В отношении нумерации элементов массива лучше употреблять слово "индекс", но не "номер", что как бы подразумевает, что отсчет выполняется с нуля (а нумерация обычно выполняется с единицы). При такой индексации, последний элемент имеет значение индекса на единицу меньше чем количество элементов в массиве.
Если массив объявлен следующим образом:
он имеет следующие элементы: Variable[0], Variable[1], Variable[2].
На первый взгляд несоответствие количества элементов и индекса последнего элемента может казаться неудобным. На самом деле оно дает значительные преимущества по сравнению с языками программирования, в которых отсчет элементов массива начинается с 1 или размер массива определяется индексом его последнего элемента, а не действительным количеством элементов в массиве.
Для определения размера массива в языке MQL5 используется функция ArraySize():
После выполнения этого кода переменная Size будет иметь значение 3.
Статические массивы и динамические массивыМассивы могут быть статическими и динамическими. Если размер массива указан при его объявлении, массив является статическим:
Размер статического массива будет невозможно изменить в программе. При объявлении массива его размер можно указать непосредственно числом (как показано в последнем примере), а можно с помощью предопределенной константы:
Если при объявлении массива не указать его размер, массив будет динамическим:
Прежде чем использовать такой массив, ему нужно установить размер. Установка размера выполняется функцией ArrayResize():
Размер динамического массива можно менять сколь угодно раз в процессе выполнения программы, в этом его принципиальное отличие от статического массива.
Если массив нужно полностью освободить, используется функция ArrayFree():
При выполнении этой функции массиву устанавливается размер 0. Действие этой функции идентично действию:
Освобождение массива может быть полезным, когда массив больше не нужен для дальнейшей работы программы (это позволяет снизить объем используемой программой оперативной памяти), либо в начале выполнения какой-либо функции (если массив используется для сбора каких-нибудь данных).
Выяснить, является ли массив статическим или динамическим, можно с помощью функции ArrayIsDynamic():
Если массив динамический, у переменной dynamicArray будет значение true, если статический - false.
Инициализация массиваИногда бывает нужно заполнить массив значениями сразу при его объявлении. Допустим, нужно создать несколько однотипных кнопок и расположить их в ряд, на каждой кнопке должна быть своя надпись. Как раз в этом случае открываются огромные преимущества массивов. Нет необходимости выписывать код по созданию каждой из кнопок (их может быть даже несколько десятков), и даже нет необходимости вызывать одну функцию несколько раз. Можно, прокручивая массив в цикле, создать необходимое количество кнопок, записав код вызова функции только один раз.
Объявляем массив и сразу присваиваем значения его элементам:
При таком объявлении, не смотря на то, что размер массива не указан, он все равно является статическим, потому что количество его элементов определено списком значений (в фигурных скобках).
Не будет ошибкой, если вы укажите количество элементов массива:
Но лучше этого не делать - при выполнении каких-нибудь доработок программы может потребоваться изменить список значений массива, да и самих элементов может потребоваться больше или меньше. В тех местах кода, где выполняется использование этого массива, для определения его размера рекомендуется использовать функцию ArraySize(), а не конкретное числовое значение. При таком подходе будет достаточно изменить только список значений, не вмешиваясь в основной код. Более оптимально будет объявить переменную для размера массива и при инициализации программы присвоить ей значение, полученное функцией ArraySize().
Если статический массив не инициализируется списком значений, для указания размера массива будет лучше использовать константу. В общем, придерживаемся принципа сокращения количества редактируемых мест кода в случае, если потребуется выполнение доработки программы. Если все элементы массива необходимо заполнить одинаковыми значениями, для этого используется функция ArrayInitialize():
После выполнения этого кода у всех элементов массива Var будет значение 1. Для присвоения одинакового значения только некоторым элементам массива используется функция ArrayFill():
После выполнения этого кода элементы 0 и 1 будут иметь значения 1, а элементы 2 и 3 значения 2.
Перебор массива в циклеЧаще всего работа с массивами ведется с использованием цикла for. Если используется статический массив и его размер известен заранее, в зависимости от решаемой задачи, проходим по массиву в прямом или обратном направлении:
Если массив динамический, непосредственно перед выполнением цикла необходимо объявить переменную для размера массива, получить размер массива, затем выполнить цикл:
Если не использовать переменную для размера, а вызывать функцию ArraySize() при проверке условия в цикле for, это может значительно увеличить время выполнения этого цикла, потому что функция ArraySize() будет вызываться на каждом проходе цикла, а вызов функции требует больше времени, чем обращение к переменной:
Не рекомендуется использовать такой код.
Если же алгоритм программы допускает проход цикла в обратном направлении, можно не использовать переменную для размера:
В этом случае функция ArraySize() будет вызвана только один раз в начале цикла, и цикл будет работать быстро.
Многомерные массивыПока что рассматривались только одномерные массивы, их можно представить в виде линейки:
Массивы могут быть многомерными. Если у одномерного массива одному индексу соответствует одно значение, то у многомерного несколько значений. Многомерные массивы объявляются следующим образом:
Это означает, что в первом измерении массива находится десять элементов, во втором - три. Графически такой массив можно изобразить следующим образом:
Для простоты понимания двухмерный массив можно представить как плоскость. Размер первого измерения определяет длину, размер второго - ширину, а значение элемента определяет характеристику данной точки плоскости, например - высоту над уровнем моря.
Так же массив может быть 3-х мерным:
Такой массив можно представить в виде куба или параллелограмма: одно измерение определяет длину, второе - ширину, третье - высоту, а значение - характеристику точки пространства.
В MQL5 максимальное количество измерений у массива равно 4.
Многомерный массив может быть статическим или динамическим только по первому измерению, все последующие измерения являются статическими. Таким образом, функция ArrayResize() позволяет менять только размер по первому измерению. Размеры по остальным измерениям должны указываться при объявлении массива:
При определении размера многомерного массива функцией ArraySize() следует учитывать одну особенность: при изменении размера массива функцией ArrayResize(), второй параметр функции означает размер первого измерения массива, однако функция ArraySize() возвращает не размер первого измерения, а общее количество элементов:
После выполнения этого кода переменная Size будет иметь значение 27. Помните об этой особенности при переборе многомерных массивов в цикле, если хотите вычислить размер первого измерения:
Как упоминалось ранее, при кодировании желательно придерживаться принципа сокращения редактируемых мест для выполнения дальнейших доработок. В только что приведенном примере кода использовалась число 9, однако его можно вычислить. Для этого можно воспользоваться функцией ArrayRange(), которая возвращает количество элементов в указанном измерении массива. Если количество измерений массива известно, можно выполнить простое вычисление:
Можно сделать универсально:
Здесь может возникнуть желание создать функцию для такого подсчета, но, к сожалению, это невозможно, потому что в функцию невозможно передать произвольный массив. При объявлении аргумента функции требуется четко указать количество элементов во всех измерениях массива кроме первого, т.е. смысл функции теряется, проще сделать эти вычисления при инициализации программы. При объявлении массива желательно использовать константы, определяющие размеры измерений:
Инициализация многомерных массивов списком значений выполняется подобно инициализации одномерных массивов. Но, поскольку один массив как бы включает в себе несколько других массивов, то каждый из этих массивов отделяется фигурными скобками.
Допустим, имеется массив:
Этот массив состоит из трех массивов по три элемента:
Аналогично с трехмерным массивом, запись можно делать построчно, что бы было легче ориентироваться в структуре массива:
Инициализация многомерного массива функцией ArrayInitialize() выполняется точно так же, как инициализация одномерного массива:
После выполнения этого кода все элементы массива будут иметь значение 1. Так же и с функцией ArrayFill():
После выполнения этого кода все элементы, относящиеся к первому элементу первого измерения, будут иметь значение 1, относящиеся ко второму элементу - 10, к третьему - 100.
Передача массива в функциюВ отличие от переменных массивы в функцию можно передавать только по ссылке. Это означает, что функция не создает свой экземпляр массива, а работает непосредственно с переданным ей массивом, т.е. все изменения, выполняемые в массиве функцией, отражаются на исходном массиве.
Если переменная передается в функцию обычным образом (по значению), значение переданной переменной не может быть изменено функцией:
После выполнения функции Func(), значение x остается равным 1.
Если переменная передается по ссылке (обозначается знаком &), функция может изменить значение переданной ей переменной:
После выполнения функции Func() значение x становится равным 2.
При передаче массива в функцию необходимо указать, что аргумент передается по ссылке и что передается массив (квадратные скобки):
При передаче многомерных массивов в функцию необходимо указывать размеры измерений кроме первого:
В этом случае предпочтительнее пользоваться константами:
Сохранение и загрузка массивов из файлаПри сохранении и загрузке массива из файла следует учитывать разницу значений размера массива по первому измерению и общее количество элементов массива. При сохранении массива сначала запишем в файл размер массива (общее количество элементов определяемое функцией ArraySize()), затем весь массив:
Получилась вполне универсальная функция для сохранения одномерных массивов.
При загрузке из файла сначала прочитаем размер массива, затем изменим размер массива и прочитаем массив:
При загрузке многомерного массива из файла потребуется вычислить размер первого измерения. Например, читаем из файла трехмерный массив:
Может получиться так, что в файл сохранялся массив, например, с размерами 2 и 3, а мы пытаемся читать его как массив 3 на 3. Вы можете проконтролировать соответствие размеров - умножьте рассчитанный размер первого измерения на количество элементов, если полученное значение будет равным общему количеству элементов массива, значит, есть соответствие.
Однако массив Var[2][3] будет соответствовать массиву Var[3][2]. Если такой случай тоже требуется проконтролировать, то при сохранении многомерного массива следует сохранять больше информации о нем, например, сначала сохранить количество элементов массива, потом количество измерений массива, затем, размеры каждого из измерений и сам массив.
Последняя функция не будет универсальной, а будет предназначена только для чтения трехмерных массивов, у которых размер второго измерения равен SIZE1, а размер третьего измерения равен SIZE2. Поскольку отсутствует возможность динамического изменения размеров всех измерений массива кроме первого, это не является проблемой: создаем функции для тех массивов, с которыми планируется работать в программе.
Универсальность в этом случае не нужна: все равно через внешние параметры программы не будет происходить управление размерами измерений массива (кроме первого измерения). Если же вам понадобится возможность управления размерами других измерений, это задачу можно решить путем использования многомерного массива заведомо большего размера и дополнительных переменных, либо при помощи объектно ориентированного программирования (ООП). Мы рассмотрим второй подход немного позже в этой статье.
Применение динамических массивовДинамические массивы применяются в тех случаях, когда заранее неизвестно какого размера должен быть массив. Если размер массива зависит от параметров, установленных в окне свойств программы, применение динамических массивов не создаст проблем: при инициализации программы один раз меняется размер массива и всё.
Массив может использоваться для динамического сбора какой-либо информации, например об отложенных ордерах. Их количество может меняться, т.е. требуемый размер массива заранее неизвестен. В таком случае проще всего перед проходом по ордерам изменить размер массива до нуля, а при проходе для каждого ордера увеличивать массив на один элемент. Такой способ будет работать, но очень медленно.
Можно перед проходом по ордерам один раз изменить размер массива в соответствии с количество ордеров, в этом случае потребуется еще одна переменная для индекса последнего занятого элемента в массиве (или не для индекса, а количества фактически занятых элементов массива). Такой способ подойдет, если заранее известен максимальный размер массива. Если максимальный размер массива неизвестен, ускорить работу с ним можно за счет изменения размера массива блоками, как показано в следующем классе:
Этот класс находятся в файле "CDynamicArray.mqh" приложения. Файл должен располагаться в каталоге "MQL5\Include" каталога данных терминала.
Сравним быстродействие при последовательном увеличении массива на 1 и при увеличении размера массива блоками:
Этот тест выполнен в виде скрипта, находится в файле "sTest_Speed.mq5" приложения. Файл должен располагаться в каталоге" MQL5\Scripts" каталога данных терминала.
На работу первого варианта затрачено несколько секунд, второй вариант отработал практически мгновенно.
Направление индексации массиваОбычно при изменении размера массива новые элементы добавляются в конец массива:
После выполнения этого кода в массиве будут содержаться значения 1, 2, 3.
Массивы могут иметь обратную индексацию элементов. Тип индексации устанавливается функцией ArraySetAsSeries():
При изменении размера массива с обратной индексацией, новый элемент добавляется в начало массива:
После выполнения этого кода в массиве будут содержаться значения 3, 2, 1.
Получается так, что в обоих случаях к массиву добавляется новый элемент с одной и той же стороны массива, отличается только порядок индексации элементов. Эта функция не может использоваться для добавления элементов в начало массива с нормальной индексацией. Если массив с нормальной индексацией, для добавления элемента в конец массива достаточно увеличить размер массива и присвоить значение последнему элементу.
Для добавления элемента в начало массива нужно увеличить размер массива, переместить все значения, присвоить нулевому элементу новое значение. В массив с обратной индексацией легко добавить новый элемент в начало массива. Если же нужно добавить новый элемент в конец массива, то после увеличения размера массива нужно сдвинуть все значения к началу и после этого присвоить новое значение последнему элементу. Манипуляции с порядком индексации для решения такой задачи бесполезны.
Узнать направление индексации массива можно функцией ArrayIsSeries():
Если массив имеет обратную индексацию, функция возвращает true.
Основная область применения массивов с обратной индексацией - это советники. При их программировании удобнее вести отсчет баров справа налево, и соответственно, копировать ценовые данные и индикаторные буферы в массивы с обратной индексацией.
Копирование массивовСамый простой способ - пройтись по массиву в цикле и поэлементно копировать элемент одного массива в другой, однако в MQL5 существует специальная функция для копирования массивов - ArrayCopy():
После выполнения этого кода массив ar2 будет состоять из трех элементов, которые будут иметь такие же значения как массиве ar1: 1, 2, 3.
Если массив, в который выполняется копирование, меньше, чем количество копируемых элементов, его размер автоматически увеличивается (массив должен быть динамическим), если же больше - размер остается неизменным.
Функция ArrayCopy() позволят производить частичное копирование массивов. У функции есть необязательные параметры, позволяющие указывать: начало копируемых элементов в массиве с которого выполняется копирование, индекс, начиная с которого копируемые элементы будут располагаются в массиве, и количество копируемых элементов.
Функцию ArrayCopy() можно использовать не только для копирования элементов одного массива в другой массив, но и для копирования элементов одного и того же массива:
Берем данные, начиная с элемента с индексом 2, и располагаем их, начиная с индекса 1. После выполнения этого кода в массиве будут значения: 1, 3, 4, 5, 5.
Функция ArrayCopy(), так же, позволят сдвинуть данные вправо:
Берем данные начиная с элемента с индексом 1 и располагаем их начиная с индекса 2. После выполнения этого кода в массиве будут значения: 1, 2, 2, 3, 4.
Функция ArrayCopy() применима и к многомерным массивам, функция работает так, как будто массив одномерен, а все его элементы расположены последовательно:
После выполнения этого кода в массив будет иметь следующий вид: , , .
Сортировка массиваСортировка или упорядочивание массива может выполняться при помощи функции ArraySort():
После выполнения этого кода значения в массиве будут располагаться в следующем порядке: 1, 2, 3, 4, 5.
Функция ArraySort() неприменима к многомерным массивам. По вопросу сортировки многомерных массивов или структур данных можно обратиться к статье Электронные таблицы на MQL5.
Бинарный поискДля выполнения бинарного поиска применяется функция ArrayBsearch(). Эта функция будет правильно работать только с упорядоченным (отсортированным) массивом. Бинарный поиск имеет такое название потому, что при его выполнении происходит последовательное деление массива на две части. Сначала искомое значение сравнивается с центральным элементом массива. Так определяется половина, в которой находится элемент: в начальной или в конечной. Затем выполняется сравнение с центральным элементом половины, и т.д.
Функция ArrayBsearch() возвращает индекс элемента с искомым значением:
После выполнения этого кода у переменной index будет значение 2.
Если искомого значения нет в массиве, функция вернет индекс элемента с ближайшим меньшим значением. Благодаря этому свойству функция применима для поиска баров по времени. Если бар с заданным временем отсутствует, в вычислениях следует использовать бар с меньшим временем.
Если искомое значение отсутствует в массиве и выходит за диапазон значений массива, функция вернет 0 (искомое значение меньше наименьшего значения) или последний индекс (искомое значение больше наибольшего значения).
Для поиска в неупорядоченном массиве существует только один способ - перебор массива:
В этом примере функция возвращает индекс элемента с искомым значением. Если элемент с искомым значением отсутствует, функция возвращает -1.
Поиск максимума и минимумаДля поиска максимального и минимального значений в массиве используются функции ArrayMaximum() и ArrayMinimum(). Функции возвращают индекс элемента с максимальным и минимальным значением, соответственно:
После выполнения этого кода в переменной MaxIndex значение 6, а переменной MinIndex будет значение 2, в переменной MaxVaue будет значение 5, а в переменной MinValue будет значение 1.
Функции ArrayMaximum() и ArrayMinimum() позволяют ограничить диапазон поиска в массиве - указать индекс элемента, с которого выполняется поиск, и количество элементов, которые необходимо просмотреть:
В этом случае в MaxIndex будет значение 6, в MinIndex - 5. Обратите внимание, в указанном диапазоне минимальное значение 4 встречается два раза: на позиции 5 и на позиции 7, функция вернула индекс элемента, ближайшего к началу массива. Таким же образом эти функции работают и с массивами, имеющими обратную индексацию - возвращают наименьший индекс.
На этом все стандартные функции языка MQL5 для работы с массивами рассмотрены.
Многомерный массив с применением ООПНабор классов для создания многомерного массива включает в себя три класса: базовый и два потомка. В зависимости от потомка, выбранного на этапе создания объекта, объект может представлять собой массив переменных double или массив объектов. Каждый элемент массива объектов может представлять собой другой массив объектов или массив переменных.
Ни в базовом классе, ни в классах потомках, практически нет никаких функций, кроме деструктора в базовом классе и конструкторов в классах потомках. Деструктор в базовом классе предназначен для удаления всех объектов по завершению работы программы или функции. Конструкторы в классах потомках только масштабируют массивы в соответствии с размером, указанным в параметрах конструктора: в одном классе масштабируется массив объектов, в другом - массив переменных. Ниже представлен код этих классов:
Эти классы находятся в файле "CMultiDimArray.mqh" приложения. Файл должен располагаться в каталоге "MQL5\Include" каталога данных терминала.
Пока применим этот класс для создания подобия одномерного массива:
Этот пример выполнен в виде скрипта, который находится в файле "sTest_1_Arr.mq5" приложения. Файл должен располагаться в каталоге "MQL5\Scripts" каталога данных терминала.
Теперь попробуем создать двухмерный массив. В каждом элементе первого измерения будет находиться разное количество элементов второго измерения, в первом - один, во втором два и т.д.:
Этот пример выполнен в виде скрипта, находится в файле "sTest_2_Dim.mq5" приложения. Файл должен располагаться в каталоге "MQL5\Scripts" каталога данных терминала.
Полученные массивы являются как бы статическими, так как в классах нет методов для изменения размеров массивов, но массивы D[] и V[] находятся в открытой секции класса, поэтому они доступны для любых манипуляций с ними. Можно без каких-либо сложностей масштабировать массив V[]. При масштабировании массивов D[] и при сокращении их размеров предварительно следует выполнять удаление объектов, на которые указывают удаляемые элементы, а при увеличении размера - загружать в них объекты.
При желании можно придумать и другие способы реализации многомерных массивов при помощи ООП или структур данных.
ЗаключениеВ статье рассмотрены все стандартные функции языка MQL5, предназначенные для работы с массивами, рассмотрены особенности и некоторые наиболее важные приемы работы с массивами. Всего в языке насчитывается 15 функций, некоторые из них имеют первостепенную важность, а некоторые могут оказаться совершенно невостребованными, за исключением случаев решения каких-нибудь очень нестандартных задач. По важности и частоте использования функции можно расположить в следующем порядке:
ArraySize(), ArrayResize() - функции без которых невозможно обойтись.
ArraySort() - важная и полезная функция, но она редко используется из-за ее недостаточной функциональности.
ArrayBsearch() - редко используемая функция, но очень важная в редких исключительных случаях.
Из приемов программирования, рассмотренных в статье, особое внимание следует уделить работе с динамическими массивами, поскольку это в значительной степени влияет и, можно сказать, определяет быстродействие программы.