От начального до среднего уровня: Классы (III)
Введение
В предыдущей статье «От базового к среднему уровню: Классы (II)» я попытался объяснить одну из самых запутанных тем объектно-ориентированного программирования: понятие деструктора и основы его использования. Кроме того, это почти наверняка одна из самых сложных тем. Однако там мы рассмотрели лишь самую простую и приятную сторону гораздо более сложного вопроса. То, что было объяснено там, относится лишь к небольшой части того, что на самом деле происходит на практике.
Сложность объектно-ориентированного программирования заключается не в самом программировании, а в нечётком определении некоторых понятий, необходимых для его правильного применения. Многие специалисты академической среды объясняют их поверхностно, а то и вовсе неверно. Вы, уважаемый читатель, должны овладеть ими, чтобы понять и использовать всё, что может предложить нам объектно-ориентированное программирование. Я публикую этот вводный материал сейчас, поскольку изложенные в нём понятия понадобятся нам для продолжения реализации механизма деревьев. Мы сделали небольшую паузу в этой реализации, чтобы чётко разъяснить эти понятия объектно-ориентированного программирования.
Вполне вероятно, что эта статья завершит первый этап необходимых объяснений. Так мы сможем вернуться к реализации механизма деревьев с того места, где остановились. Без лишних предисловий перейдём к основной теме этой статьи.
Классы (III)
В предыдущей статье мы реализовали два варианта кода: скрипт и индикатор. Оба использовали один и тот же заголовочный файл. Поэтому при запуске в MetaTrader 5 оба могут вести себя очень похоже, так как используют код класса, определённого в заголовочном файле.
Вернёмся к тому месту, на котором мы остановились в предыдущей статье, чтобы то, что я объясню здесь, имело смысл. Давайте сделаем следующее: рассмотрим фрагменты кода из предыдущей статьи. Ниже вы можете увидеть их целиком.
01. //+------------------------------------------------------------------+ 02. class C_Regression 03. { 04. private : 05. //+----------------+ 06. public : 07. //+----------------+ 08. C_Regression() 09. { 10. datetime dt0 = TimeCurrent(), 11. dt1[20]; 12. 13. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 14. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 15. ChartRedraw(); 16. } 17. //+----------------+ 18. ~C_Regression() 19. { 20. ObjectDelete(0, def_NameChannel); 21. ChartRedraw(); 22. } 23. //+----------------+ 24. }; 25. //+------------------------------------------------------------------+
Код 01
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 01.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel; 11. 12. Sleep(2000); 13. } 14. //+------------------------------------------------------------------+
Код 02
01. //+------------------------------------------------------------------+ 01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. #include <Tutorial\File 01.mqh> 08. //+------------------------------------------------------------------+ 09. C_Regression gl_Channel; 10. //+------------------------------------------------------------------+ 11. int OnInit() 12. { 13. return INIT_SUCCEEDED; 14. }; 15. //+------------------------------------------------------------------+ 16. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 17. { 18. return rates_total; 19. }; 20. //+------------------------------------------------------------------+ 21. void OnDeinit(const int reason) 22. { 23. }; 24. //+------------------------------------------------------------------+
Код 03
Итак, код 01 соответствует заголовочному файлу, код 02 — скрипту, а код 03 — индикатору. И скрипт, и индикатор ведут себя схожим образом. Оба добавляют на график объект, определённый в строке 14 фрагмента кода 01: линию регрессии. Тем не менее, они работают немного по-разному. Скрипт сохраняет линию регрессии на графике в течение периода, указанного в строке 12 фрагмента кода 02. Индикатор же сохраняет её, пока сам остаётся на графике. В случае с индикатором именно вы решаете, когда удалить линию регрессии, поскольку она исчезает, как только вы убираете индикатор.
Такое поведение скрипта и индикатора очень удобно и кажется идеальным. Однако, хотя всё и кажется идеально работающим, есть одна небольшая проблема. Используя класс так, как это сделано во фрагментах кода 02 и 03, вы, как программист, не контролируете должным образом его жизненный цикл.
Не поймите меня неправильно, мой дорогой читатель. Я говорю, что у вас нет этого контроля, потому что вы не можете указать, когда объект должен создаваться и когда уничтожаться. Компилятор определяет оба этих момента. Вы лишь определяете, как будут вести себя объекты этого класса, но не когда они будут созданы и не как долго будут существовать. Я знаю, что отсутствие контроля над жизненным циклом объектов кажется довольно странным и в это трудно поверить. Вам кажется, что вы контролируете, что делает код и как он работает. Однако, если вы правильно поняли то, что было объяснено в двух последних статьях, то вам хорошо известно, что контроль программиста не является абсолютным.
Когда вы объявляете переменную типа класса, компилятор вызывает соответствующий конструктор, чтобы инициализировать связанный с ней объект. Точно так же, когда жизненный цикл объявленной переменной завершается, компилятор вызывает деструктор. Таким образом, деструктор корректно завершает жизненный цикл объекта так, как это предусмотрели вы как программист.
Однако мы всё ещё не можем точно контролировать момент создания и уничтожения объекта. Если мы не будем контролировать эти моменты, всё начнёт накапливаться очень быстро. Если вы не поняли содержание предыдущих статей, прошу вас остановиться и вернуться к ним, пока не усвоите материал должным образом. Следующее объяснение может вас полностью запутать: пришло время научиться использовать два новых оператора. Мы уже рассматривали их ранее, но именно здесь всё действительно усложняется. Я имею в виду операторы new и delete.
Ну, насколько мне известно, операторы new и delete служат для выделения памяти. Верно? Да, мой дорогой читатель. Однако функция выделения памяти в основном относится к C++. В MQL5 операторы new и delete позволяют управлять жизненным циклом объекта с помощью переменной, которая на него ссылается. Насколько я смог убедиться, их нельзя использовать так же, как в C++, что в некотором смысле даже к лучшему. Это кажущееся ограничение по сравнению с C++ уменьшает некоторые проблемы, которые у других программистов могли бы возникнуть при интерпретации кода.
Вернёмся к теме. Чтобы упростить объяснение и избежать ненужных сложностей, мы будем работать исключительно с кодом индикатора. Пока что мы сосредоточимся на коде 03.
Как я объяснял в предыдущей статье, когда выполняется строка 9 кода 03, компилятор вызывает конструктор класса, определённый в строке 8 кода 01. Позже, когда переменная из строки 9 кода 03 перестанет использоваться, компилятор вызовет деструктор класса, определённый в строке 18 кода 01. Всё просто. Однако мы можем изменить способ выполнения этих вызовов. Достаточно заменить код 03 на тот, что показан ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. #include <Tutorial\File 01.mqh> 08. //+------------------------------------------------------------------+ 09. C_Regression *gl_Channel; 10. //+------------------------------------------------------------------+ 11. int OnInit() 12. { 13. return INIT_SUCCEEDED; 14. }; 15. //+------------------------------------------------------------------+ 16. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 17. { 18. return rates_total; 19. }; 20. //+------------------------------------------------------------------+ 21. void OnDeinit(const int reason) 22. { 23. }; 24. //+------------------------------------------------------------------+
Код 04
Теперь код 04 будет вести себя иначе, чем код 03. Я не вижу никаких изменений. Вы уверены, что код 04 работает по-другому? На мой взгляд, фрагменты кода 03 и 04 одинаковы. Никакой разницы нет. Обратите внимание, мой дорогой читатель. Между этими двумя фрагментами кода действительно есть разница. Прочитайте фрагмент кода 04 чуть внимательнее.
Вы уже заметили разницу? Я по-прежнему не вижу никакой разницы, кроме звёздочки, которую вы добавили в фрагмент кода 04. Кроме этой звёздочки, всё остальное одинаково. Действительно, оба фрагмента кода выглядят одинаково, но их поведение различается. Добавленная звёздочка полностью изменила поведение кода 04. Теперь компилятор больше не будет неявно вызывать конструктор. То есть объявления переменной в девятой строке уже недостаточно, чтобы создать объект. Теперь мы должны явно указать компилятору:
Я хочу, чтобы ты вызвал конструктор класса именно в этот момент.
Однако, если мы явно вызываем конструктор, нам также придётся указать компилятору, когда он должен вызвать деструктор. Это связано с тем, что компилятор больше не знает, когда следует вызвать деструктор. Ох, как всё сложно. Чем больше я читаю твои статьи, тем сложнее становится эта тема. Думаю, программирование — не для меня. Я сдаюсь. Спокойно, мой дорогой читатель, мы ещё даже не начали играть по-настоящему. Возвращайтесь, потому что с каждым днём тема становится всё интереснее. То, что мы сейчас видим, — просто детская забава по сравнению с тем, что мы действительно можем делать. И вы уже думаете сдаться? (СМЕХ).
Хорошо, продолжим те, кто не сдался. Решили вернуться? Отлично, я рад видеть такую решимость. Чтобы показать, что мы действительно контролируем, когда и в какой точке кода вызываются конструктор и деструктор, внесём небольшое изменение в заголовочный файл. Изменения показаны ниже.
01. //+------------------------------------------------------------------+ 02. class C_Regression 03. { 04. private : 05. //+----------------+ 06. public : 07. //+----------------+ 08. C_Regression() 09. { 10. datetime dt0 = TimeCurrent(), 11. dt1[20]; 12. 13. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 14. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 15. ChartRedraw(); 16. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 17. } 18. //+----------------+ 19. ~C_Regression() 20. { 21. ObjectDelete(0, def_NameChannel); 22. ChartRedraw(); 23. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 24. } 25. //+----------------+ 26. }; 27. //+------------------------------------------------------------------+
Код 05
Хорошо, теперь мы можем прямо в терминале увидеть, когда вызываются конструктор и деструктор. Сообщения, сгенерированные строками 16 и 23 кода 05, фиксируют соответственно вызов конструктора и деструктора. Чтобы сделать это ещё интереснее, мы также изменим код 04, который относится к индикатору. Изменение показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. #include <Tutorial\File 01.mqh> 08. //+------------------------------------------------------------------+ 09. C_Regression *gl_Channel; 10. //+------------------------------------------------------------------+ 11. int OnInit() 12. { 13. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 14. gl_Channel = new C_Regression; 15. 16. return INIT_SUCCEEDED; 17. }; 18. //+------------------------------------------------------------------+ 19. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 20. { 21. return rates_total; 22. }; 23. //+------------------------------------------------------------------+ 24. void OnDeinit(const int reason) 25. { 26. delete gl_Channel; 27. 28. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 29. }; 30. //+------------------------------------------------------------------+
Код 06
А теперь будьте предельно внимательны, мой дорогой читатель. Оставьте всё, чем вы сейчас занимаетесь, и сосредоточьтесь исключительно на объяснении в статье. Следующее объяснение с этого момента будет иметь большое значение.
Обратите внимание на строки 14 и 26 кода 06. Мы делаем то, что для большинства не имеет никакого смысла, но для вас скоро начнёт его обретать. Недавно я утверждал, что мы можем контролировать, когда и где создаётся объект класса, а также когда и где он уничтожается. Если утверждение о контроле жизненного цикла верно, то сообщение, сгенерированное в строке 13 кода 06, появится раньше, чем сообщение конструктора, сгенерированное в строке 16 кода 05. Аналогично, сообщение деструктора, сгенерированное в строке 23 кода 05, появится раньше, чем сообщение из строки 28 кода 06. В промежутке между вызовом конструктора и вызовом деструктора объект будет доступен для использования. Если я ошибаюсь, сообщения из строк 13 и 28 кода 06 появятся после сообщений из кода 05. Чтобы проверить порядок вызовов, мы должны запустить этот индикатор на графике. Следующая анимация показывает результат.

Анимация 01
Поскольку анимация прошла слишком быстро, мы зафиксируем конкретный момент. Снимок показан ниже.

Рисунок 01
Теперь внимательно посмотрите на сообщения на рисунке 01. Первое сообщение указывает, что выполняется OnInit в файле Code 01.mq5, в строке 13. Первое сообщение полностью соответствует ожидаемому, поскольку код 06 относится к файлу Code 01.mq5. Второе сообщение указывает, что конструктор выполняется в файле File 01.mqh, в строке 16. Второе сообщение также полностью соответствует ожидаемому, поскольку код 05 относится к файлу File 01.mqh. Таким образом, объект инициализируется в нужный момент и в нужном месте.
Давайте теперь рассмотрим уничтожение объекта. Следующее сообщение указывает, что деструктор выполняется в файле File 01.mqh, в строке 23. Отлично. Последнее сообщение указывает, что OnDeinit выполняется в файле Code 01.mq5, в строке 28. Просто фантастика. Код сработал отлично и позволил нам определить, где и когда создаётся и уничтожается объект. Раньше у нас не было контроля над моментом создания и уничтожения объекта. Если не верите, давайте сделаем следующее: воспользуемся кодом 03, который соответствует исходной версии индикатора, и перекомпилируем его, внеся небольшое изменение. Мы добавим те же сообщения, что и в коде 06. Код 03 будет выглядеть следующим образом.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. #include <Tutorial\File 01.mqh> 08. //+------------------------------------------------------------------+ 09. C_Regression gl_Channel; 10. //+------------------------------------------------------------------+ 11. int OnInit() 12. { 13. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 14. 15. return INIT_SUCCEEDED; 16. }; 17. //+------------------------------------------------------------------+ 18. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 19. { 20. return rates_total; 21. }; 22. //+------------------------------------------------------------------+ 23. void OnDeinit(const int reason) 24. { 25. Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__); 26. }; 27. //+------------------------------------------------------------------+ 28.
Код 07
Обратите внимание, что в коде 07, в отличие от кода 06, операторы new и delete больше не используются. Тем не менее код 07 воспроизводит поведение кода 03. Так мы сможем увидеть, когда и где создаётся и уничтожается объект. Следующая анимация показывает выполнение кода 07.

Анимация 02
Поскольку эта анимация, вероятно, тоже прошла слишком быстро, давайте посмотрим на её заключительный момент. Вы можете спокойно рассмотреть это на следующем рисунке.

Рисунок 02
Теперь посмотрите на изображение 02. Поведение, показанное на рисунке 02, явно отличается от поведения, показанного на рисунке 01. На изображении 02 конструктор вызывается ДО того, как MetaTrader 5 вызывает обработчик событий OnInit, а деструктор вызывается ПОСЛЕ того, как MetaTrader 5 вызывает обработчик событий OnDeinit. Разница в порядке вызовов показывает, что даже небольшая деталь в реализации кода может полностью изменить конечный результат.
Что ж, думаю, я начинаю понимать, как всё это работает и как мы должны это реализовать. Но что происходит в скрипте? Я спрашиваю об этом, потому что переменная, ссылающаяся на объект, является глобальной в коде индикатора и может использоваться из любой точки этого кода. В то же время переменная, объявленная во фрагменте кода 02 скрипта, является локальной. Имеет ли значение то, что переменная локальная, или в конечном счёте всё работает одинаково? Это очень уместный вопрос, мой дорогой читатель. Ответить на него тоже непросто. До сих пор у нас не было возможности правильно это объяснить, потому что ответ зависит от того, как вы собираетесь использовать код своего класса.
Я постараюсь объяснить этот вопрос проще и понятнее. Простое утверждение о том, что скрипт ведёт себя так же или иначе, чем индикатор, нисколько не помогло бы; напротив, оно вызвало бы ещё больше сомнений. Чтобы проанализировать поведение обеих программ, нам нужно будет добавить в код несколько элементов. Я предоставлю вам все файлы, использованные в статье, чтобы вы могли спокойно их изучить. Кроме того, мы создадим ещё один заголовочный файл. По сути, это будет почти точная копия заголовочного файла, представленного в коде 01. Создание нового заголовочного файла позволит избежать сомнений относительно того, почему код реализован именно так, а также упростит сравнение обоих файлов. Второй файл показан ниже.
01. //+------------------------------------------------------------------+ 02. class C_Regression 03. { 04. private : 05. //+----------------+ 06. public : 07. //+----------------+ 08. C_Regression() 09. { 10. datetime dt0 = TimeCurrent(), 11. dt1[20]; 12. 13. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 14. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 15. ChartRedraw(); 16. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 17. } 18. //+----------------+ 19. ~C_Regression() 20. { 21. ObjectDelete(0, def_NameChannel); 22. ChartRedraw(); 23. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 24. } 25. //+----------------+ 26. void PrintMsg(const string msg) 27. { 28. Print(msg); 29. } 30. //+----------------+ 31. }; 32. //+------------------------------------------------------------------+
Код 08
Обратите внимание, что строки 16 и 23 кода 08 немного отличаются от строк 16 и 23 кода 05. Из-за различий между строками 16 и 23 в обоих фрагментах кода мы будем использовать процедуру, указанную в строке 26 фрагмента кода 08. Обратите внимание: файл 08, включённый в приложения, будет отличаться, так как позже мы его изменим.
Хорошо, давайте теперь перейдём к файлу, содержащему скрипт. Мы внесём небольшое изменение в его код, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 02.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel; 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Sleep(2000); 15. } 16. //+------------------------------------------------------------------+
Код 09
Обратите внимание, что в строке 12 фрагмента кода 09 мы можем отправить сообщение классу с помощью метода, который только что реализовали. На этом этапе, полагаю, ни у кого из вас не останется сомнений относительно того, какое сообщение будет выведено в терминал. В любом случае, это можно увидеть на следующем рисунке.

Рисунок 03
Хорошо, давайте перейдём к первой из проблем и посмотрим, как её решить. В статье «От базового к среднему уровню: Перегрузка» я упомянул несколько способов использовать одну и ту же процедуру или функцию, сохраняя её имя, но для разных целей. Хотя та статья не привлекла большого внимания, важно, чтобы вы ознакомились с тем, что в ней объясняется. Теперь мы применим очень похожий принцип. Мне даже казалось, что я уже говорил в другой статье о том, что мы собираемся сделать. Если и рассказывал, то не помню, поскольку часть материала была опубликована в моём другом профиле автора. В любом случае, давайте посмотрим, как применить этот принцип.
В статье «От базового к среднему уровню: Передача по значению или по ссылке» я объяснил некоторые понятия, которые мы здесь разовьём, поскольку они поднимают очень интересный вопрос, который стоит рассмотреть подробнее.
А теперь представьте себе следующую ситуацию, мой дорогой читатель. Как вы, наверное, уже знаете, глобальные переменные не должны быть первым выбором; обычно лучше начинать с локальных переменных. В статьях о переменных я кратко объяснил использование глобальных и локальных переменных. Статья «От базового к среднему уровню: Переменные (II)» поможет вам лучше понять выбор между глобальными и локальными переменными. Во фрагменте кода 09 мы объявляем локальную переменную, которая позволяет нам ссылаться на объект этого класса. Однако предположим, что вам нужно получить доступ к объекту, связанному с локальной переменной, из другого места в коде, например из другой процедуры.
У нас уже есть локальная переменная, объявленная в строке 10 кода 09; переменная, объявленная в строке 10, ссылается на объект. Создание другого экземпляра не гарантирует, что он будет воспроизводить состояние исходного объекта. Новый экземпляр совпадал бы с предыдущим лишь до тех пор, пока не было изменено ни одно значение, а на практике так обычно не бывает. Таким образом, возникает первая проблема: нам нужно передать в процедуру ссылку, объявленную в строке 10. Так функция сможет получить доступ к исходному объекту. Мы можем сделать это, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 02.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel; 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. } 18. //+------------------------------------------------------------------+ 19. void Checking(C_Regression channel) 20. { 21. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 22. } 23. //+------------------------------------------------------------------+
Код 10
А теперь обратите внимание на детали. Во-первых, если вы попытаетесь скомпилировать фрагмент кода 10, то увидите, что компилятор выдаст следующее сообщение:

Рисунок 04
Сообщение компилятора указывает, что параметр функции должен передаваться по ссылке. Чтобы решить эту проблему, мы изменили код, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 02.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel; 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. } 18. //+------------------------------------------------------------------+ 19. void Checking(C_Regression &channel) 20. { 21. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 22. } 23. //+------------------------------------------------------------------+
Код 11
Теперь код скомпилируется без проблем. Однако здесь возможна проблема. Передавая в процедуру или функцию ссылку на класс, мы рискуем тем, что что-то изменится, а мы этого даже не заметим. Мы не хотим, чтобы процедура или функция случайно вносили подобные изменения — из-за невнимательности или по любой другой причине. Следовательно, мы должны ограничить набор разрешённых операций над экземпляром, переданным по ссылке. Об этом уже говорилось в предыдущих статьях, но в них не был рассмотрен второй шаг, который нам тоже необходимо сделать. Сначала мы снова изменим скрипт, чтобы он выглядел так, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 02.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel; 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. } 18. //+------------------------------------------------------------------+ 19. void Checking(const C_Regression &channel) 20. { 21. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 22. } 23. //+------------------------------------------------------------------+
Код 12
Итак, благодаря изменениям в коде 12 мы предотвращаем случайное изменение каких-либо данных класса со стороны процедуры или функции. Однако теперь компилятор выдаёт другую ошибку, показанную ниже.

Рисунок 05
Ошибка компилятора в MQL5 действительно любопытна и даже забавна. В C++, напротив, ситуация может стать действительно опасной, если программист использует определённые средства, чтобы обойти эту ошибку. Но эти механизмы C++ здесь не относятся к делу. Главное, что компилятор указывает, что мы пытаемся вызвать неконстантный метод через константную ссылку. В данном контексте квалификатор const ограничивает допустимые операции над экземпляром, на который ссылаются: если объект нельзя изменять через ссылку, то и значения, доступные через неё, тоже нельзя изменять.
Нам нужно исправить объявление метода, чтобы код продолжал работать корректно. Именно здесь пригодилась бы перегрузка. В данном конкретном случае перегрузка не потребуется, поскольку она не даст существенных отличий в конечном результате. Итак, вернёмся к заголовочному файлу и изменим его, как показано ниже.
01. //+------------------------------------------------------------------+ 02. class C_Regression 03. { 04. private : 05. //+----------------+ 06. public : 07. //+----------------+ 08. C_Regression() 09. { 10. datetime dt0 = TimeCurrent(), 11. dt1[20]; 12. 13. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 14. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 15. ChartRedraw(); 16. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 17. } 18. //+----------------+ 19. ~C_Regression() 20. { 21. ObjectDelete(0, def_NameChannel); 22. ChartRedraw(); 23. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 24. } 25. //+----------------+ 26. void PrintMsg(const string msg) const 27. { 28. Print(msg); 29. } 30. //+----------------+ 31. }; 32. //+------------------------------------------------------------------+
Код 13
Обратите внимание, что нам нужно лишь сделать константным метод, объявленный в строке 26. Если объявить его как константный метод, компилятор позволит вызывать его через константную ссылку и сможет правильно интерпретировать код, который мы хотим построить. После компиляции вы увидите результат в следующей анимации.

Анимация 03
Отлично, теперь мы внесём последнее изменение, которое идеально вписывается в содержание этой статьи. Кроме того, это поможет нам понять, как следует действовать в определённых ситуациях. В предыдущих статьях мы видели, что можем передавать аргументы в конструктор, чтобы управлять инициализацией системы. С учётом всего сказанного до сих пор вы, вероятно, уже знаете, как действовать в большинстве ситуаций. Однако что делать, если вам нужно использовать операторы new и delete, чтобы решать, когда и где объект создаётся и уничтожается? Мало кто задумывается об этом до того, как столкнётся с проблемой, но здесь мы рассмотрим, как решить её ещё до её появления.
Для начала обратите внимание, что в фрагментах кода 12 и 13 не нужно передавать в конструктор никаких аргументов. Теперь мы изменим пример так, чтобы в конструктор нужно было передавать значение. Так мы будем знать, как действовать, если в будущем нам понадобится использовать операторы new и delete.
01. //+------------------------------------------------------------------+ 02. class C_Regression 03. { 04. private : 05. //+----------------+ 06. public : 07. //+----------------+ 08. C_Regression(const string msg) 09. { 10. datetime dt0 = TimeCurrent(), 11. dt1[20]; 12. 13. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 14. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 15. ChartRedraw(); 16. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 17. PrintMsg("Message received: " + msg); 18. } 19. //+----------------+ 20. ~C_Regression() 21. { 22. ObjectDelete(0, def_NameChannel); 23. ChartRedraw(); 24. PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 25. } 26. //+----------------+ 27. void PrintMsg(const string msg) const 28. { 29. Print(msg); 30. } 31. //+----------------+ 32. }; 33. //+------------------------------------------------------------------+
Код 14
В коде 14 теперь нужно передать аргумент в конструктор. Та же информация будет выведена в терминал в строке 17. Посмотрим, как теперь выглядит код скрипта.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 03.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression channel(StringFormat("Init in line %d", __LINE__)); 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. } 18. //+------------------------------------------------------------------+ 19. void Checking(const C_Regression &channel) 20. { 21. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 22. } 23. //+------------------------------------------------------------------+
Код 15
Внимательно посмотрите на строку 10 в коде 15. Когда мы запустим этот код, получим результат, показанный ниже.

Рисунок 06
Просто идеально. Результат показывает, что всё работает так, как мы и предполагали. Теперь мы добавим в скрипт операторы new и delete. Это изменение показано в следующем коде.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 03.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression *channel(StringFormat("Init in line %d", __LINE__)); 11. 12. channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. } 18. //+------------------------------------------------------------------+ 19. void Checking(const C_Regression &channel) 20. { 21. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 22. } 23. //+------------------------------------------------------------------+
Код 16
При попытке скомпилировать код 16 компилятор выведет следующее сообщение.

Рисунок 07
То есть объявление в строке 10 фрагмента кода 16 неверно. Проблема не в том, что мы делаем нечто похожее на то, что было сделано в коде 04. Проблема в том, что объект нужно инициализировать НЕ ТАК. Разница в следующем: код 04 скомпилируется, хотя на графике не будет показан никакой результат. Напротив, код 16 НЕ СКОМПИЛИРУЕТСЯ, потому что объявление в десятой строке пытается инициализировать объект неправильно.
Чтобы конструктор получил аргумент через объявление, аналогичное тому, что использовано в коде 04, нужно применить немного иной подход. Чтобы код 16 смог скомпилироваться, нужно оформить объявление так, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. #include <Tutorial\File 03.mqh> 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_Regression *channel = new C_Regression(StringFormat("Init in line %d", __LINE__)); 11. 12. (*channel).PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__)); 13. 14. Checking(channel); 15. 16. Sleep(2000); 17. 18. delete channel; 19. } 20. //+------------------------------------------------------------------+ 21. void Checking(const C_Regression &channel) 22. { 23. channel.PrintMsg("Demonstrating the passage from a class to a procedure."); 24. } 25. //+------------------------------------------------------------------+
Код 17
Обычно объявление в десятой строке кода 17 можно разделить на два этапа. Однако, поскольку я хочу инициализировать объект при объявлении переменной, проще записать объявление так, как показано выше. Тем не менее, мы делаем нечто очень похожее на то, что было сделано в коде 06. С помощью операторов new и delete мы управляем тем, где и как инициализируется и уничтожается объект класса.
Способ передачи аргументов конструктору не меняется. Нам нужно лишь внести ещё несколько дополнительных изменений. После их применения мы получаем результат, показанный ниже.

Рисунок 08
Заключительные замечания
В этой статье мы увидели, как лучше контролировать код при использовании объектно-ориентированного программирования. Мы всё ещё находимся лишь в самом начале всего того, что можно изучить в объектно-ориентированном программировании. Тем не менее, того, что было объяснено до сих пор, уже достаточно, чтобы мы могли вернуться к реализации деревьев. Итак, мой дорогой читатель, практикуйтесь и спокойно, с большим вниманием и усердием изучайте концепции, представленные в трёх последних статьях серии. Эти понятия будут сопровождать вас на протяжении всей работы с MQL5. Кроме того, они облегчат вам путь, если позже вы захотите научиться программировать на C++.
Ну а пока я с вами прощаюсь. Увидимся в следующей статье, где мы вернёмся к теме очередей, списков и деревьев.
| Файл MQ5 | Описание |
|---|---|
| Индикатор\Код 01 | Демонстрационный файл |
| Индикатор\Код 02 | Демонстрационный файл |
| Скрипт\Код 01 | Демонстрационный файл |
| Скрипт\Код 02 | Демонстрационный файл |
| Скрипт\Код 03 | Демонстрационный файл |
| Скрипт\Код 04 | Демонстрационный файл |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/16765
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Моделирование рынка: Position View (XV)
Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования