
От начального до среднего уровня: Передача по значению или по ссылке
Введение
Представленные здесь материалы предназначены только для обучения. Ни в коем случае не рассматривайте его как окончательное приложение, целью которого не является изучение представленных понятий.
В предыдущей статье "От начального до среднего уровня: Операторы", рассказали немного об арифметических и логических операциях. Хотя она была довольно поверхностной, но этого достаточно, чтобы мы могли перейти к другим темам. Со временем, по мере написания статей, мы будем постепенно углублять темы, затронутые на этом начальном этапе.
Поэтому наберитесь терпения и спокойно изучайте материалы, ведь результаты не достигаются в одночасье, они появляются со временем. И если мы будем изучать материал сейчас, то даже не заметим, что нагрузка увеличивается. Однако, как уже говорилось в предыдущей статье, именно здесь мы начнем говорить об операторах управления. Но прежде, чем обратиться к этой теме, было бы интересно поговорить о другом вопросе. Это сделает более увлекательным и полезным наблюдение за тем, что можно сделать с помощью операторов управления.
Для полного и совершенного понимания того, что объяснено и представлено в этой статье, существует необходимое условие: понимание и различие между переменной и константой. Если вы не понимаете или не знаете этой разницы, обратитесь к статье "От начального до среднего уровня: Переменные (I)".
Один из аспектов, который порождает больше сомнений и ошибок у начинающих программистов, - это понимание того, когда использовать значения или ссылки в функциях и процедурах. Это связано с тем, что, в зависимости от конкретного случая, практичнее использовать передачу по ссылке. Однако во многих случаях передача по значению более безопасна. Когда же использовать тот или иной вариант? Всё зависит от ситуации. Четкого и окончательного правила по этому поводу не существует. Это связано с тем, что в некоторых случаях действительно используется передача по ссылке, в то время как в других случаях требуется передача по значению.
Обычно компилятор принимает решения, чтобы сделать исполняемый код как можно более эффективным. Но необходимо понимать, что может потребоваться в каждой конкретной ситуации. Создание безопасного и эффективного кода.
Передача по значению
Чтобы понять это на практике, лучше всего использовать максимально простой код для реализации. Поэтому мы начнем с анализа первой модели использования. Это будет передача по значению, применяемая в приведенном ниже коде.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Код 01
Когда код 01 будет выполнен, в терминале MetaTrader 5 мы увидим нечто подобное тому, что показано на рисунке 01, чуть ниже.
Рисунок 01
Я знаю, что для многих людей рисунок 01 может показаться очень сложным, но раз мы решили узнать, как правильно программировать на MQL5, давайте разберемся, о чем нам говорит данный рисунок. Для этого нам нужно будет следовать и коду 01, и тому коду, который представлен на рисунке 01.
Из предыдущих статей вы уже должны знать, что в шестой строке мы определяем переменную с определенным значением. И в восьмой строке мы печатаем эту же переменную. Однако, рисунке можно увидеть, что на нем появилась дополнительная информация. В данном случае это имя процедуры или функции, в которой находится восьмая строка.
А теперь, уважаемые читатели, будьте внимательны. На этапе компиляции кода 01 компилятор, встретив предопределенный макрос __FUNCTION__, будет искать имя текущей подпрограммы. В данном случае имя задается в четвертой строке (то есть OnStart). Как только данное имя будет определено, компилятор заменит слово __FUNCTION__ на имя OnStart, создав новую строку, которая будет выведена на терминал. Это происходит, потому что команда Print направляется на стандартный вывод, которым в данном случае является терминал, как показано на рисунке 01. По этой причине мы получаем не только информацию из подпрограммы, но и значение переменной, объявленной в шестой строке. Есть и другие предопределенные макросы, каждый из которых очень полезен в различных ситуациях. Рекомендую вам изучать их, так как с их помощью гораздо проще следить за тем, что делает ваш код. Точно так же, как __FUNCTION__ в подпрограмме OnStart заменяется ее именем, __FUNCTION__ в подпрограмме Procedure тоже будет заменена. И это, наконец, объясняет, что означает информация, предшествующая числовым значениям.
Давайте вернемся к тому, как понимаем использование передачи по значению. Поскольку мы используем систему передачи по значению, при выполнении вызова в девятой строке значение переменной, определенной в шестой строке, будет передано в процедуру в строке 13. На данном моменте важно кое-что подчеркнуть. Хотя мы сказали, что передаем, а точнее, копируем значение переменной info в переменную arg1, это не всегда происходит именно так. Во многих случаях компилятор довольно умно заставляет arg1 указывать на переменную info. Однако (и вот тут становится действительно интересно), переменная arg1 не использует ту же память, что и переменная info. Происходит так: компилятор устанавливает, что arg1 "наблюдает" за имеющейся в info в памяти, подобно тому, как если бы вы смотрели через оконное стекло. Однако именно из-за стекла мы не сможем прикоснуться к наблюдаемому объекту. То же самое верно и здесь: для arg1 info воспринимается как константа.
И по этой причине, когда в строке 15 мы просим вывести значение, содержащееся в arg1, во второй строке на рисунке 01 мы видим, что оно показывает абсолютно одинаковые значения для arg1 и info. Однако в строке 16 мы изменяем значение arg1. В этот момент во время компиляции, зная, что это произойдет во время выполнения кода, компилятор резервирует место для хранения того же количества байт, которое содержится в переменной info. Однако это не меняет того факта, что arg1 продолжает наблюдать за info. Но когда выполняется строка 16, arg1 использует значение, присутствующее в переменной info, как если бы info была не переменной, а константой, и это заставляет arg1 создавать для себя локальное значение. С этого момента arg1 полностью отделяется от переменной info и живет "своей жизнью". Поэтому при выполнении строки 17 мы видим, что на терминале выводится третья строка, которая показывает, что значение действительно было изменено.
Однако, когда процедура возвращается, мы подходим к выполнению строки 10, которая приводит к печати четвертой строки рисунка 01, показывая, что info продолжает сохранять свое значение.
Фактически, это механизм, используемый для передачи по значению. Конечно, конкретная реализация зависит от того, как был собран компилятор, но по сути это работает именно так.
Хорошо, это была самая простая часть. Но даже этот механизм передачи по значению может выглядеть в коде несколько иначе. На данном этапе мы не будем вдаваться в подробности. Это связано с тем, что перед тем, как рассказать о других способах использования этого механизма передачи по значению, необходимо объяснить другие аспекты, связанные с операторами и типами данных. Указанные здесь моменты скорее усложнят, чем прояснят ситуацию. Так что давайте не будем торопиться.Однако даже механизм, который мы рассмотрели выше, может иметь несколько иное моделирование. Это необходимо для того, чтобы избежать произошедшее в предыдущем коде. Чтобы изучить данный вопрос как следует, сделаем небольшое предположение. Допустим, что по какой-то причине мы не хотим, чтобы переменная arg1 могла менять свое значение. Мы хотим, чтобы она просто наблюдала за переменной info, и чтобы при использовании arg1 значение, находящееся в info, было тем, которое используется на самом деле.
Как этого добиться? Существуют различные способы. Один из них - быть осторожным и не изменять переменную arg1 в течение всего блока кода, присутствующего в процедуре. Однако, несмотря на свою кажущуюся простоту, это совсем не так. Много раз, сами того не осознавая, мы изменяем значение переменной, и понимаем это только из-за нестандартного поведения приложения при его запуске. Из-за таких проблем можно потратить много времени на решение ошибок. Но есть и другое решение: заставить компилятор работать за нас, чтобы он предупредил нас, если мы попытаемся сделать что-то неправильно.
Чтобы наглядно проиллюстрировать это, давайте посмотрим на приведенный ниже код.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Код 02
Когда мы попытаемся скомпилировать код 02, мы получим следующее сообщение об ошибке.
Рисунок 02
Можно ясно увидеть указание компилятора на то, что ошибка находится в строке 16. Это происходит, потому что мы пытаемся изменить значение переменной arg1. Но подождите секунду. Теперь arg1 больше не переменная, а константа. Таким образом, во всем коде, присутствующем в процедуре, мы больше не сможем изменять значение arg1. Это гарантирует, что риск, который был раньше, теперь отсутствует, так как компилятор сам позаботится о том, чтобы не разрешить модификацию arg1. Другими словами, знание того, что мы делаем, и наличие правильных концепций очень помогают, и это значительно облегчает работу программиста.
Значит, мы не можем изменить значение для печати в строке 17 кода 02? Уважаемые читатели, мы можем изменить значение, если присвоим его другой переменной, даже если это не указано в коде. Таким образом, чтобы получить тот же результат, что и на рисунке 01, но с помощью кода 02, мы можем использовать код, очень похожий на этот:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. Print(__FUNCTION__, " : #2 => ", arg1 + 3); 17. } 18. //+------------------------------------------------------------------+
Код 03
После внесенных изменений, мы получим код 03, в котором arg1 по-прежнему является константой. Но в строке 16 кода 03 мы присваиваем новой переменной результат суммы константы (которой в данном случае является arg1), с другой константой (являющейся значением три). Она будет создана компилятором автоматически, чтобы Print мог отобразить правильное значение. Конечно, мы можем создать переменную специально для этой цели, но я не вижу необходимости в этом, по крайней мере, в данном типе кода.
Передача по ссылке
Другой способ передачи значений между программами - по ссылке. В данном случае необходимо принять дополнительные меры предосторожности. Но прежде, чем продолжить, я хочу сделать небольшую паузу, чтобы объяснить кое-что очень важное на данный момент.
По возможности вам следует избегать использования передачи по ссылке, если только это не является крайне необходимым. Одной из самых распространенных и трудноразрешимых проблем в программировании является неуместное и небрежное использование передачи по ссылке. В некоторых случаях это становится привычкой определенных программистов, что превращает исправление и улучшение получившегося кода в настоящее мучение. Поэтому по возможности избегайте использования передачи по ссылке, особенно если цель состоит в изменении только одного значения. Я покажу вам пример этого через некоторое время, но сначала давайте рассмотрим случай, когда используется передача по ссылке, и как это влияет на приложение. Для этого мы используем данный код:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Код 04
Теперь обратите внимание на то, что мы просмотрим в этой части. Здесь ситуация может усложниться, особенно если работать над действительно сложным кодом. Запустив этот код в терминале MetaTrader 5, мы увидим следующий вывод:
Рисунок 03
Подобные ситуации, когда они возникают непреднамеренно, могут привести к существенной потери времени (от нескольких часов до нескольких месяцев), пока будем пытаться понять, почему код работает не так, как должен. Обратите внимание, что результат полностью отличается от того, что показано на рисунке 01, особенно в отношении четвертой строки на рисунке 03. Но почему это произошло? Очевидно, что если код 04 идентичен коду 01, то нет смысла в том, чтобы четвертая строка рисунка 03 отличалась от четвертой строки рисунка 01.
Хотя внешне они выглядят одинаково, коды 01 и 04 имеют небольшое, но принципиальное различие, которое можно обнаружить в строке 13. Я постарался изложить всё ясно, чтобы вы могли понять. Обратите внимание на наличие безобидного на первый взгляд символа, который есть в коде 04, но нет в коде 01. Этот символ < & >, известный как АМПЕРСАНД является причиной разницы между рисунком 03 и рисунком 01. Обычно этот же символ используется в побитовых логических операциях для выполнения операции AND, когда речь идет о MQL5. Но в C и C++, помимо операций AND, он также используется для ссылки на переменные в памяти.
Ух ты! Вот здесь всё становится серьезным. Ведь если итак сложно понять, что может означать простой символ, только представьте: в контексте одного и того же кода у него могут быть два совершенно разных значения. Это просто безумие. Именно поэтому программирование на C и C++ так сложно и требует много времени, чтобы стать действительно грамотным программистом. Однако в MQL5 всё немного проще. По крайней мере, мы не сталкиваемся со всеми сложностями, которые существуют в C и C++, главным образом потому, что MQL5 не использует напрямую ресурс, который явно представлен в C и C++, а именно указатели.
Для того, чтобы вы правильно поняли, почему наличие этого символа в строке 13 кода 04 влияет на конечный результат, необходимо разобраться, что этот символ означает на самом деле. Для этого мы рассмотрим концепцию указателей на языках C и C++, не вдаваясь в сложные вопросы, связанные с этой концепцией.
Указатель - это как переменная, но не любая переменная. Это та переменная, которая, как следует из ее названия, указывает на что-то. Указатель в данном случае указывает на область памяти, в которой на самом деле существует другая переменная. Я знаю, что это может показаться запутанным. Использование переменной для указания на другую переменную - гораздо сложнее, чем кажется. Однако это понятие широко используется в языках C и C++ для создания самых разнообразных стилей кода, и это одна из причин, по которой C и C++ являются одними из самых быстрых языков программирования, наравне с ассемблером по скорости обработки. Не будем вдаваться в подробности об этих указателях (это может окончательно запутать вас), важно следующее: когда arg1 объявляется также, как в коде 04, мы НЕ используем обычный способ инстанцирования переменной info. На самом деле, компилятор MQL5 рассматривает arg1 как указатель на переменную info.
По этой причине, когда мы делаем что-то вроде суммы в строке 16, то изменяемое нами не является переменной arg1. Мы НЕ добавляем, повторяю, МЫ НЕ ДОБАВЛЯЕМ переменную arg1. На самом деле мы изменяем переменную info, потому что и info, и arg1 занимают одно и то же пространство памяти. Другими словами, в подпрограмме Procedure arg1 и info одинаковы; arg1 - это info, а info - это arg1.
Вас путает это? На самом деле, это лишь простая часть работы с указателями. Поскольку MQL5 НЕ ИСПОЛЬЗУЕТ (вернее, не позволяет использовать) указатели в стиле C и C++, на этом объяснение того, как arg1 может модифицировать info, заканчивается. Нам просто нужно воспринимать arg1 и info как одну и ту же сущность, даже если они находятся в разных местах и, казалось бы, не имеют никакой видимой связи между собой. В языках C и C++ такое поведение является более сложным. И поскольку я не хочу вносить путаницу в ваши мысли, всё сводится к тому, что мы объясняли до этого момента.
Теперь возникает вопрос: есть ли способ заблокировать данную модификацию? Иными словами, как избежать того, чтобы переменная arg1 (даже если она изменена в строке 16) изменила и переменную info? Да, есть способы запретить arg1 изменять info. Один из них - использование передачи по значению, как объяснялось в предыдущей теме. Другой способ - изменить код 04, чтобы он выглядел так:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Код 05
Однако, сделав то, что показано в коде 05, мы столкнемся с той же проблемой, что и в коде 02. Итак, теперь arg1 будет рассматриваться как константа. По этой причине попытка скомпилировать код 05 приведет к тому же результату, что и попытка скомпилировать код 02, что отражено на рисунке 02. Хотя arg1 указывает на info, являющееся переменной, и может показаться, что это ошибка, но это далеко не так. Во многих случаях нам приходится делать что-то вроде того, что осуществляется в коде 05. Но он не скомпилируется именно потому, что строка 16 пытается изменить arg1. Окончательное решение данной проблемы будет заключаться в принятии кода, аналогичного коду 03, что приведет к появлению кода, показанного ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. Print(__FUNCTION__, " : #2 => ", arg1 + 3); 17. } 18. //+------------------------------------------------------------------+
Код 06
Замечательно, благодаря этому у нас получился рабочий код, похожий на код 03. Однако, в отличие от кода 03, в котором используется передача по значению, здесь мы используем передачу по ссылке. Результат выполнения кода 06 будет таким же, как показано на рисунке 01.
В этот момент мы можем вернуться к вопросу, упомянутому в начале этого раздела: избегать передачи по ссылке для изменения значения одной переменной.
Если по каким-то причинам нам нужна подпрограмма для изменения значения переменной, особенно базового типа, лучше использовать функцию, а не подпрограмму. Именно поэтому в языках программирования есть функции. Они были созданы не для того, чтобы украсить код, а для того, чтобы предотвратить проблемы при его выполнении.
Теперь предположим, что вы действительно хотите изменить переменную info не из кода, в котором она находится, а из созданной нами подпрограммы, значит наиболее подходящим способом сделать это будет код, подобный приведенному ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. info = Function(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. int Function(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. 17. return arg1 + 3; 18. } 19. //+------------------------------------------------------------------+
Код 07
Обратите внимание, что в этом коде 07 мы по-прежнему используем передачу по ссылке. Однако никаких нежелательных ситуаций здесь не возникает. Происходит именно так, потому что при рассмотрении строки 09, мы замечаем, что наш код будет полностью контролируемым образом изменять значение info. Код, написанный таким образом (с использованием функций), наверняка не будет создавать проблем. Результат выполнения кода 07 можно увидеть ниже:
Рисунок 04
Обратите внимание, что мы всегда уверены в происходящем. Достаточно взглянуть на код, чтобы понять, почему значение info изменили внутри блока OnStart. Будьте осторожны и не делайте ничего подобного.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. info = Function(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. int Function(int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. 17. return (arg1 = arg1 + 3) + 3; 18. } 19. //+------------------------------------------------------------------+
Код 08
Очень часто некоторые программисты пытаются достичь поставленной цели и в итоге создают полную путаницу. В качестве примера можно привести код 08. Да, это не совсем адекватный пример, ведь подобная ситуация обычно возникает в очень сложных операциях, включающих множество переменных и этапов. Однако данный случай служит лишь иллюстрацией возможного проблемного состояния.
Теперь я спрашиваю вас: Какое значение info должно быть выведено в строке 10 кода 08? Возможно, вы не заметили, но arg1 больше не является константой. Тогда, какое значение информации должно быть выведено в строке 10? Возможно, это покажется вам немного запутанным из-за строки 17 и из-за того, что одновременно изменяется и возвращаемое значение, и значение, присвоенное переменной info. Но прежде, чем объяснять, почему всё не так запутанно, как кажется, давайте посмотрим на результат, выведенный на терминал, который можно увидеть на рисунке 05 ниже.
Рисунок 05
Обратите внимание, что значение не 13, а 16. Почему? Причина в том, что, хотя arg1 является указателем на info и в строке 17 мы присваиваем значение 13, которое будет использоваться в arg1 и, следовательно, в info, возвращаемое значение перезаписывает значение 13. Это происходит, потому что возврат функции добавляет еще 3 к значению, присвоенному в строке 17. Поэтому выводимый результат фактически равен 16, поскольку возврат функции присваивается переменной info.
Но если бы вместо присвоения значения info в строке 9 мы просто проигнорировали значение, возвращаемое функцией, то, вероятно, попали бы в ту же ситуацию, что и в предыдущих кодах. То есть значение info изменилось бы, но попытка разобраться в коде и исправить его займет у нас много времени только на то, чтобы найти ошибку. И запомните этот факт: код, который содержит такие ошибки, обычно состоит из нескольких файлов с сотнями строк в каждом, что означает много работы по поиску места проблемы.
Что касается возвращаемого значения, то довольно часто оно просто игнорируется, поскольку мы не обязаны присваивать его или даже использовать. По этой причине необходимо быть осторожными при внедрении этого типа структуры в свой код. Несмотря на огромные преимущества, которые дает передача по ссылке, вполне возможно, что в какой-то момент у нас возникнут проблемы с этим подходом, особенно если у вас мало опыта в программировании.
Ну и напоследок стоит сказать, что есть еще одна проблема, связанная с передачей по ссылке. Во многих случаях это происходит из-за ошибки при передаче аргументов в подпрограмму, которая их обрабатывает. При работе с передачей по значению вероятность ошибок, возникающих при случайной замене одного аргумента другим, имеет ограниченное влияние на общий код. Часто в процессе компиляции мы получаем какое-то предупреждение компилятора об этом, если ожидаемые типы данных отличаются. Но когда мы используем передачу по ссылке, иногда компилятор предупреждает нас об ошибке, хотя мы не обнаруживаем, что причина в неправильном порядке аргументов. Это может привести к ошибке, которая еще больше усложнит поиск ошибок в приложении. Например, предположим, что нам нужно создать функцию, в которой мы указываем конкретную дату и время. Функция складывает указанное нами количество часов и, кроме того, переводит время в секунды в переменной, соответствующей количеству часов. Это кажется довольно простой задачей, и ее можно решить с помощью приведенного ниже кода:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. uint info = 10; 07. datetime dt = D'17.07.2024'; 08. 09. Print(__FUNCTION__, " : #1 => ", info, " :: ", dt); 10. dt = Function(dt, info); 11. Print(__FUNCTION__, " : #2 => ", info, " :: ", dt); 12. } 13. //+------------------------------------------------------------------+ 14. datetime Function(ulong &arg1, datetime arg2) 15. { 16. Print(__FUNCTION__, " : #1 => ", arg1, " :: ", arg2); 17. 18. return arg2 + (arg1 = arg1 * 3600); 19. } 20. //+------------------------------------------------------------------+
Код 09
Однако, когда предлагаем компилятору создать исполняемый файл, он выдает предупреждение, как показано ниже: Данный тип предупреждения не препятствует компиляции кода.
Рисунок 06
Но поскольку программисты всегда обращают внимание на каждое отправленное компилятором сообщение, мы немедленно проверим упомянутую строку и внесем соответствующее исправление. В данном случае просто указывается, что значение типа ulong должно быть преобразовано в тип datetime. Важная деталь: оба типа имеют одинаковую длину в битах. По этой причине многие люди часто игнорируют такие предупреждения. Таким образом, строка 18 кода 09 изменяется таким образом, как показано ниже.
return arg2 + (datetime)(arg1 = arg1 * 3600);
Теперь компилятор больше не выдает предупреждение. Но когда мы запускаем код, к нашему удивлению и полному недоумению, результат оказывается таким:
Рисунок 07
В этот момент мы чувствуем себя полностью дезориентированными, поскольку наш код, похоже, не работает. И здесь кроется важная деталь: наш код на самом деле не так уж плох. В нем есть только одна небольшая ошибка, которую трудно обнаружить. В данном случае его легко определить, потому что код, во-первых, простой и довольно короткий. Во-вторых, мы демонстрируем лишь некоторые базовые функции. На практике мы редко пишем небольшой кусок кода для немедленного тестирования. Обычно это происходит так: мы создаем серию элементов, которые взаимодействуют друг с другом. В определенный момент мы проводим тест, чтобы проверить, всё ли работает. Иногда мы обнаруживаем ошибку, иногда нет. Это делает поиск ошибки в более сложном коде особенно трудным, поскольку зачастую подпрограмма, вызывающая проблему, делает это в некоторых частях кода, в то время как в других она, кажется, работает без проблем. В подобных сценариях решение проблемы становится действительно сложным.
В заключение давайте разберемся, где здесь кроется ошибка. Она расположена точно на десятой линии. Сейчас, возможно, вы думаете: «Как ошибка может находиться в строке 10? Скорее всего, ошибка находится внутри подпрограммы в строке 14, возможно, в строке 18. Это точно не десятая строка».
Но тут вы ошибаетесь. Будьте внимательны и поразмышляйте вместе со мной. Мы хотим, чтобы переменная info содержала конечное значение, выраженное в секундах, а переменная dt начиналась с даты, объявленной в седьмой строке, но скорректированной в часах на основе переменной info. Данная корректировка должна производиться в строке 14 подпрограммы, а именно в строке 18. Пока всё идет хорошо. Однако в коде есть небольшая ошибка: поскольку тип datetime занимает 8 байт и тип ulong также занимает 8 байт, подпрограмма не будет выдавать ошибки из-за разницы в типах данных. Но обратите внимание, что info имеет тип uint. Можно подумать, что проблема находится здесь, но это не так. Поскольку 24 часа содержат 86 400 секунд, тип uint, занимающий 4 байта, будет достаточен для хранения правильного значения. Можно использовать меньший тип данных, но это увеличит риск ошибок в возвращаемом значении.
Так как arg1 - это указатель, он будет использовать передачу по ссылке, а arg2 - передачу по значению. Таким образом, ошибка фактически находится в десятой строке, поскольку первым аргументом должна быть переменная info, так как именно она будет изменена. Переменная dt, с другой стороны, должна быть установлена только на значение, которое возвращает функция. Исходя из этого, исправим десятую строку с помощью кода, показанного ниже:
dt = Function(info, dt);
При попытке скомпилировать код компилятор выдает предупреждение:
Рисунок 08
Отчаявшись заставить код работать, чего бы это ни стоило, и зная, что нам не нужно 8-байтовое значение, потому что достаточно 4-байтового, мы решаем изменить строку 14, как показано ниже:
datetime Function(uint &arg1, datetime arg2)
Наконец, код компилируется, и после выполнения, как по волшебству, долгожданный результат представляется корректно, как показано на следующем рисунке.
Рисунок 09
Заключительные идеи
В данной статье рассказывается о тонкостях, присутствующих в реальном программировании. Конечно, все приведенные здесь примеры просты и ориентированы на обучение, но даже в этом случае объяснять, как всё работает на самом деле, очень весело и полезно. Я знаю, что многим такой материал кажется неинтересным, особенно тем, кто уже давно занимается созданием и разработкой программ. Но мне интересно: Сколько времени у вас ушло на то, чтобы ясно понимать то, что объясняется в этой простой статье? Я сам долго пытался понять, почему мои коды на C/C++ иногда делают странные и бессмысленные вещи, пока наконец не разобрался во всех деталях.
Как только я понял всё это, остальное стало намного проще и понятнее. Сейчас я с удовольствием программирую на нескольких языках и даже получаю удовольствие от решения проблем, которые они создают. И это благодаря тому, что мне удалось построить прочный и надежный фундамент благодаря C/C++. MQL5 - это гораздо более приятный, простой и понятный язык, чем все те сложности, которые присутствуют в языке C/C++. Однако, если вы сумеете понять то, что было объяснено и продемонстрировано в этой статье, вы сможете гораздо быстрее научиться создавать отличные приложения на MQL5. В следующей статье мы наконец-то сможем приступить к более занимательному материалу. До скорой встречи!
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/15345





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Здравствуйте. Неточность в тексте (подчеркнул красными линиями)
С уважением, Владимир.
Здравствуйте. Неточность в тексте (подчеркнул красными линиями)
С уважением, Владимир.
Спасибо, поправили