Параметры-значения и параметры-ссылки

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

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

Для описания параметра-ссылки необходимо добавить справа от типа знак амперсанда '&'. Многие программисты предпочитают присоединять амперсанд к имени параметра, тем самым подчеркивая, что именно параметр является ссылкой на заданный тип. Например, следующие записи эквивалентны:

void func(int &parameter);
void func(int & parameter); 
void func(intparameter);

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

В качестве аргумента для параметра-ссылки можно указывать только переменную (LValue, см. раздел Оператор присваивания). В противном случае мы получим ошибку "параметр передается по ссылке, ожидается переменная" ("parameter passed as reference, variable expected").

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

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

Первый пункт особенно актуален для переменных с потенциально большим размером, таких как строки или массивы.

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

Неприменение модификатора const к параметрам-ссылкам, там где это возможно, способно тянуть за собой проблемы по всей иерархии вызовов функций. Дело в том, что вызовы подобных функций будут требовать неконстантных аргументов. Иначе будет возникать ошибка "переменная-константа не может передаваться по ссылке" ("constant variable cannot be passed as reference"). В результате может постепенно оказаться, что все параметры во всех функциях следует лишить модификатора const в угоду компилируемости кода. На самом деле, это фактически расширяет простор для потенциальных ошибок с непреднамеренной порчей переменных. Исправлять ситуацию следует обратным образом: ставить const везде, где не требуется возврат и модификация значений.

Для сравнения способов передачи параметров в скрипте FuncDeclaration.mq5 реализовано несколько функций: FuncByValue — передача по значению, FuncByReference — передача по ссылке, FuncByConstReference — передача по константной ссылке.

void FuncByValue(int v)
{
   ++v;
   // делаем что-то еще с v
}
 
void FuncByReference(int &v)
{
   ++v;
}
 
void FuncByConstReference(const int &v)
{
   // ошибка
   // ++v; // 'v' - constant cannot be modified
   Print(v); 
}

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

void OnStart()
{
   int i = 0;
   FuncByValue(i);          // i не может измениться
   Print(i);                // 0
   FuncByReference(i);      // i меняется
   Print(i);                // 1
   FuncByConstReference(i); // i не может измениться, 1
   const int j = 1;
   // error
   // 'j' - constant variable cannot be passed as reference
   // FuncByReference(j);
   
   FuncByValue(10);         // ok
   // error: '10' - parameter passed as reference, variable expected
   // FuncByReference(10);
}

Литерал может быть передан только в функцию FuncByValue, так как остальные функции требуют ссылки, то есть переменной в качестве аргумента.

Функция FuncByReference не может быть вызвана с переменной j, поскольку последняя описана как константа, а данная функция декларирует возможность (или намерение) изменить свой параметр, так как он не снабжен модификатором const. Из-за этого генерируется ошибка "переменная-константа не может передаваться по ссылке" ("constant variable cannot be passed as reference").

Также в скрипте описана функция Transpose: она транспонирует матрицу 2x2, переданную в виде двумерного массива по ссылке.

void Transpose(double &m[][2])
{
   double temp = m[1][0];
   m[1][0] = m[0][1];
   m[0][1] = temp;
}

Её вызов из OnStart демонстрирует ожидаемое изменение содержимого локального массива a.

double a[2][2] = {{-12}, {30}};
Transpose(a);
ArrayPrint(a);

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

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

Размеры массива по всем размерностям кроме первой должны совпадать у параметра и аргумента. В противном случае мы получим ошибку "конвертация параметра запрещена" ("parameter conversion not allowed"). В частности, в примере определена функция TransposeVector:

void TransposeVector(double &v[])
{
}

Попытка вызвать её для двумерного массива a закомментирована в OnStart, поскольку генерирует вышеуказанную ошибку: размерности массивов не совпадают.

Помимо передачи параметров по значению или по ссылке существует еще один вариант — передача указателя. В отличие от C++, MQL5 поддерживает указатели только для объектных типов (классов). Мы рассмотрим эту особенность в третьей Части.