От начального до среднего уровня: Классы (II)
Введение
В предыдущей статье От начального к среднему уровню: Классы (I), мы начали знакомиться с объектно-ориентированным программированием. Однако там мы говорили только о конструкторах классов. Иногда нам нужно реализовать ещё одну специальную функцию-член, присущую классам: деструктор.
Чтобы правильно понять, как работают деструкторы, можно провести параллель между объектно-ориентированным программированием, в котором создаются и используются классы, и событийно-ориентированным программированием в MetaTrader 5. "Ничего себе! Что это за странное сравнение вы собираетесь провести?" Успокойтесь, уважаемый читатель. Скоро вы всё поймёте. Уверяю вас: так вам будет гораздо легче понять, почему существуют деструкторы и как их следует реализовывать, когда это необходимо.
Пришло время отложить в сторону всё, что может вас отвлекать, и сосредоточиться на том, что мы рассмотрим в этой статье. Итак, перейдём к следующей теме.
Классы (II)
Одним из самых сложных для понимания понятий в объектно-ориентированном программировании является деструктор. Во многих случаях практический смысл деструкторов неочевиден. Кроме того, поскольку вы как программист не можете явно вызвать деструктор, то понять, зачем он вообще существует, становится ещё сложнее. Это совершенно не помогает начинающему программисту понять концепцию деструктора.
Программисты старой школы, такие как я, которые буквально наблюдали за эволюцией программирования на протяжении многих лет, тоже с трудом понимали некоторые из появлявшихся концепций и механизмов. А теперь представьте себе программиста, который сталкивается со всеми этими уже реализованными концепциями и должен в них разобраться. Ну и путаница! Прежде всего потому, что многие из этих понятий обычно объясняют очень плохо, превращая то, что изначально просто, в чудовище, похожее на гидру: чем больше голов ему отрубаешь, пытаясь с ним покончить, тем сложнее становится ситуация. Деструктор — как раз одно из таких понятий. В этой статье вы поймёте, что, хотя он и кажется сложным, на самом деле всё очень просто. Когда вы правильно поймёте принцип его работы, вам будет гораздо проще определить, когда и как реализовать его в своих классах.
Хорошо, для начала давайте на мгновение забудем об объектно-ориентированном программировании и перейдём к чему-то более простому и практичному: событийно-ориентированному программированию. Именно эту парадигму мы используем при разработке индикаторов и советников для MetaTrader 5. Хорошо, но какая связь между событийно-ориентированным программированием и объектно-ориентированным программированием? Честно говоря, уважаемый читатель, у них не так много общего. Однако, поскольку мы используем MQL5 — язык, разработанный для того, чтобы программист мог управлять работой MetaTrader 5, — установить связь между этими двумя парадигмами очень просто. Это сравнение позволит нам показать, как деструктор работает на практике.
Для начала мы создадим очень простой индикатор, код которого приведён ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. int OnInit() 06. { 07. return INIT_SUCCEEDED; 08. }; 09. //+------------------------------------------------------------------+ 10. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 11. { 12. return rates_total; 13. }; 14. //+------------------------------------------------------------------+ 15. void OnDeinit(const int reason) 16. { 17. }; 18. //+------------------------------------------------------------------+
Код 01
Как мы уже видели в первых статьях об индикаторах из этой же серии, и индикаторы, и советники работают на основе событий, которые в большинстве случаев генерирует и запускает MetaTrader 5. Бывают и ситуации, когда сам код может инициировать события, но сейчас это неважно. Важно понимать, что эти события исходят от MetaTrader 5.
Более подробную информацию можно найти в предыдущих статьях. В частности, От начального к среднему уровню: События (I) поможет вам понять некоторые концепции, необходимые для понимания данной статьи.
Мы можем рассматривать код 01 как полноценный класс. "Как это так? Я не понимаю, что вы хотите сказать". Спокойно, сейчас вы всё поймёте. Помните, в предыдущей статье мы говорили о двух специальных функциях-членах класса? Конструктор отвечает за инициализацию класса, а деструктор — за уничтожение того, что класс создал. Итак, давайте ещё раз посмотрим на код 01. Поскольку это индикатор, когда MetaTrader 5 поместит его на график, сначала он вызовет и выполнит код функции OnInit. Когда мы удалим этот индикатор с графика, MetaTrader 5 вызовет и выполнит функцию OnDeinit, если она реализована. Таким образом, мы сможем удалить с графика любой элемент, который захотим убрать после удаления индикатора.
Исходя из этой простой идеи и сходства между объектно-ориентированным программированием и работой кода индикатора, мы можем кое-что реализовать в коде 01 и начать понимать, как на самом деле работает деструктор.
Мы добавим объект на график и, когда индикатор будет удалён, тоже удалим этот объект. Это очень простая задача, которая уже объяснялась в других статьях этой же серии.
В качестве основы для того, что мы будем делать, мы используем статью От начального к среднему уровню: Объекты (I). В ней я показал, как добавлять и удалять объекты с графика. Изменив код 01 в соответствии с тем, что объяснялось в этой статье, мы получим код, показанный ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, 0, 0); 10. 11. return INIT_SUCCEEDED; 12. }; 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. }; 18. //+------------------------------------------------------------------+ 19. void OnDeinit(const int reason) 20. { 21. ObjectDelete(0, def_NameChannel); 22. ChartRedraw(); 23. }; 24. //+------------------------------------------------------------------+
Код 02
Цель кода 02 — создать объект OBJ_REGRESSION и отобразить его на графике. Как только индикатор будет удалён, объект тоже будет удалён. Однако, запустив код, вы заметите, что на графике не появляется ни одного объекта, хотя в строке 09 он действительно создаётся. Как мы можем это решить? Это очень просто, уважаемый читатель, достаточно изменить код, как показано ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo" 04. //+------------------------------------------------------------------+ 05. #define def_NameChannel "Demo" 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. datetime dt0 = TimeCurrent(), 10. dt1[20]; 11. 12. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 13. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 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. ObjectDelete(0, def_NameChannel); 26. ChartRedraw(); 27. }; 28. //+------------------------------------------------------------------+
Код 03
Единственное необходимое изменение заключается в получении дат, которые мы будем использовать для создания и позиционирования объекта регрессии. Это делается в строках 9 и 12. Результат показан ниже:

Рисунок 01
А теперь обратите внимание на следующую деталь. Когда мы добавляем данный индикатор на график, объект, определённый в строке 13, создаётся и размещается. При удалении индикатора этот же объект удаляется с графика благодаря строке 25. То есть обработчик события OnInit выполняет роль конструктора, а обработчик события OnDeinit — роль деструктора. "Хорошо, но какое отношение это имеет к объектно-ориентированному программированию? Я всё ещё не понимаю, к чему вы клоните".
Итак, уважаемый читатель, теперь вы это поймёте, потому что мы подошли к самой интересной части: преобразованию кода 03 в программу, использующую объектно-ориентированное программирование. Будьте очень внимательны, чтобы понять, как мы это сделаем. Поскольку код 03 является индикатором, а для применения в нём объектно-ориентированного программирования нам пришлось бы использовать две инструкции, которые мы ещё не объясняли, мы изменим подход и преобразуем код 03 в скрипт. Результат показан ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. datetime dt0 = TimeCurrent(), 09. dt1[20]; 10. 11. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 12. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 13. ChartRedraw(); 14. 15. Sleep(2000); 16. 17. ObjectDelete(0, def_NameChannel); 18. ChartRedraw(); 19. } 20. //+------------------------------------------------------------------+
Код 04
Обратите внимание, что теперь мы используем скрипт вместо индикатора. Однако это не меняет сути того, что мы продолжаем делать. Мы просто используем такой подход, при котором будет проще объяснить и наглядно показать работу конструктора и деструктора класса, который мы создадим позже. Чтобы объект не исчез сразу после создания, мы используем строку 15, чтобы сделать небольшую паузу между его размещением и удалением. При выполнении код будет вести себя так, как показано в следующей анимации:

Анимация 01
Отлично, скрипт работает правильно. А теперь начинается самое интересное. Нам нужно лишь изменить код 04 таким образом:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. class C_Regression 07. { 08. private : 09. //+----------------+ 10. public : 11. //+----------------+ 12. C_Regression() 13. { 14. datetime dt0 = TimeCurrent(), 15. dt1[20]; 16. 17. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 18. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 19. ChartRedraw(); 20. } 21. //+----------------+ 22. }; 23. //+------------------------------------------------------------------+ 24. void OnStart(void) 25. { 26. C_Regression channel; 27. 28. Sleep(2000); 29. 30. ObjectDelete(0, def_NameChannel); 31. ChartRedraw(); 32. } 33. //+------------------------------------------------------------------+
Код 05
А теперь будьте очень внимательны, уважаемый читатель. Код 05 даст тот же результат, что мы видели в анимации 01. Тем не менее, именно здесь мы начинаем видеть, как реализовать это поведение с помощью объектно-ориентированного программирования. Поскольку часть, связанная с конструктором, уже была объяснена в предыдущей статье, мы можем сосредоточиться на деструкторе. И именно здесь многие новички приходят в полное замешательство, потому что не понимают, как это работает. Это происходит прежде всего потому, что многие авторы не объясняют вещи так, как я пытаюсь объяснить их здесь.
Чтобы добавить деструктор в код 05 и полностью привести его в соответствие с принципами объектно-ориентированного программирования, достаточно изменить его, как показано ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. class C_Regression 07. { 08. private : 09. //+----------------+ 10. public : 11. //+----------------+ 12. C_Regression() 13. { 14. datetime dt0 = TimeCurrent(), 15. dt1[20]; 16. 17. CopyTime(NULL, NULL, dt0, dt1.Size(), dt1); 18. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0); 19. ChartRedraw(); 20. } 21. //+----------------+ 22. ~C_Regression() 23. { 24. ObjectDelete(0, def_NameChannel); 25. ChartRedraw(); 26. } 27. //+----------------+ 28. }; 29. //+------------------------------------------------------------------+ 30. void OnStart(void) 31. { 32. C_Regression channel; 33. 34. Sleep(2000); 35. } 36. //+------------------------------------------------------------------+
Код 06
А теперь запустите код 06 в терминале MetaTrader 5 и удивитесь результату, уважаемый читатель. Так вы убедитесь в том, что результат совпадает с тем, что показано в анимации 01. Почему? Помните, что MetaTrader 5 будет выполнять только код, содержащийся в процедуре OnStart. Однако, если присмотреться к этой процедуре, мы увидим, что она содержит всего две строки. Тем не менее код сохраняет своё поведение: создаёт объект, помещает его на график, даёт пройти некоторому времени, а затем удаляет его. Итак, снова возникает вопрос: как это произошло? Ответ в том, что вызывается деструктор.
"Как это так? Я не вижу совершенно никаких вызовов, кроме двух строк в процедуре OnStart. Единственное отличие, которое я заметил в коде 06, заключается в том, что вы создали ещё одну специальную функцию-член внутри класса с тем же именем, но с тильдой в начале. Внимательно присмотревшись, я замечаю, что она содержит код, который находился в строках 30 и 31 кода 05. Больше ничего не добавлено. Тем не менее вы утверждаете — и я могу в этом убедиться, запустив код, — что объект удаляется. Теперь я вообще ничего не понимаю. Для меня это совершенно не имеет смысла. Я знаю: это может быть только делом рук ДЬЯВОЛА. Потому что БОГ не стал бы делать что-то настолько сложное".
Действительно, уважаемый читатель, в деструкторах есть нечто любопытное. Чтобы правильно их понять, нам нужно познакомиться ещё с одним понятием. Это понятие объяснялось в первых статьях этой серии, когда мы говорили о переменных. Внимательно прочитайте, и вы поймёте, почему работает код 06, а также как объявлять и использовать деструктор.
В статье «От начального к среднему уровню: Переменные (I)», мы говорили о времени жизни и видимости переменных. Если у вас есть какие-либо сомнения на этот счёт, обратитесь к той статье за более подробной информацией. Понимание обоих понятий поможет вам понять, когда компилятор вызывает деструктор, а также позволит вам более эффективно использовать объектно-ориентированное программирование.
Хорошо, давайте разберём всё по порядку. Во-первых, объявлять деструктор нужно так, как показано в строке 22 кода 06. То есть перед именем класса нужно добавить символ (~). Таким образом, компилятор поймёт, что объявляемая вами специальная функция-член является деструктором самого класса. Поскольку деструкторы и конструкторы НИКОГДА НЕ ВОЗВРАЩАЮТ НИКАКОГО ЗНАЧЕНИЯ, вы не можете использовать их для этой цели. Вы должны доверять тому, что ваш код сделает всё наилучшим образом.
Второй момент: в отличие от конструкторов, которые могут принимать аргументы инициализации, как мы видели в предыдущей статье, деструкторы НИКОГДА, НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ и НИ ЗА ЧТО не принимают аргументов. Попытка сделать это приведёт к ошибке, которая не позволит скомпилировать код и, следовательно, создать исполняемый файл.
В-третьих: деструкторы всегда вызываются неявно, за исключением случаев, когда мы используем два оператора, о которых поговорим чуть позже. То есть теоретически ВЫ НЕ КОНТРОЛИРУЕТЕ, когда и где будет вызван деструктор. Компилятор определяет это на основе времени жизни переменной. Повторю: в теории это именно так. На практике существуют способы контролировать, когда и где будет вызван деструктор. Пока что мы сосредоточимся на той части, которую понять проще всего.
Хорошо, но теперь возникает вопрос: что такое время жизни переменной? Что ж, уважаемый читатель, об этом мы говорили в первой статье этой серии. Тем не менее, мы можем кратко повторить это здесь.
Переменная возникает в момент её объявления и перестаёт существовать по окончании блока, в котором была объявлена.
Хорошо, но это мне совершенно не помогло, потому что я всё равно ничего не понимаю. В таком случае я предлагаю вам вернуться к первым статьям этой серии и внимательно их изучить. Чёткое понимание этих понятий позволит вам максимально эффективно использовать MQL5. Без них забудь об этом: вы не сможете понять объяснение.
Продолжим. Когда в строке 32 мы объявляем переменную channel, тип которой — класс C_Regression, вызывается конструктор по умолчанию этого класса. Об этом рассказывалось в предыдущей статье. Однако по завершении блока кода, начинающегося со строки 31, то есть при переходе к строке 35, объявленные в нём переменные уничтожаются. В данном случае единственной переменной в блоке является channel. Поскольку компилятор знает, что `channel` — это экземпляр класса, он будет искать деструктор в `C_Regression`. Если деструктор не определён, компилятор сгенерирует его, чтобы объект мог быть корректно уничтожен. Запомните следующее: компилятор НЕ ЗНАЕТ, КАК УНИЧТОЖИТЬ ОБЪЕКТ. Он лишь сгенерирует неявный деструктор в соответствии с правилами языка. Таким образом, можно будет скомпилировать код и создать исполняемый файл.
Однако, поскольку в нашем классе C_Regression мы определили и реализовали деструктор в строке 22, компилятор сгенерирует вызов для выполнения этого кода, когда закончится время жизни объекта, связанного с переменной channel. По этой причине, хотя в коде это явно не указано, объект OBJ_REGRESSION удаляется с графика.
"Как интересно! Теперь я действительно начинаю понимать, как работает код 06 и как вызывается деструктор. Однако у меня только что возник один вопрос. Я читал эти статьи и узнал много интересного в очень доступной форме. Тем не менее, то, что здесь было показано, похоже, применимо только к этому конкретному случаю. А нельзя ли сделать это как-то иначе?" Что вы имеете в виду, уважаемый читатель? Постарайтесь быть немного конкретнее, чтобы я мог вам помочь.
"Я хочу понять следующее: в коде 06 мы используем скрипт, чтобы продемонстрировать то, что ранее видели в индикаторе, созданном в коде 03. Пока что всё ясно. В индикаторе, когда мы просили MetaTrader 5 удалить его, вызывалась функция OnDeinit. В коде 06 это было бы эквивалентно вызову деструктора в строке 22, как объяснялось ранее. То же самое происходит и с конструктором. В коде 03 эту задачу выполняла функция OnInit в строке 7. В коде 06 эту задачу выполняет конструктор в строке 12, который вызывается при выполнении строки 32. У меня следующий вопрос: можем ли мы использовать тот же класс из кода 06 в коде 03? И если да, то как нам следует это сделать?"
Отличный вопрос, уважаемый читатель. Ответ — ДА. Мы можем без каких-либо проблем использовать тот же класс, реализованный в коде 06, в коде 03. Чтобы вы могли ясно понять, как это сделать, мы перенесём класс в заголовочный файл. Таким образом, мы сможем использовать его как в скрипте, так и в любом другом коде — будь то индикатор, советник или даже сервис. Однако в случае с сервисами нам потребуется выполнить несколько дополнительных шагов, о которых мы расскажем позже.
С учётом этого мы создаём показанный ниже заголовочный файл:
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. //+------------------------------------------------------------------+
Код 07
Обратите внимание, что код класса остался без изменений. Чтобы использовать его в скрипте, подобном коду 06, мы поступим немного иначе. Новый код показан ниже:
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. //+------------------------------------------------------------------+
Код 08
Если вы не понимаете код 08, рекомендую ознакомиться со статьёй «От начального к среднему уровню: Директива Include», в которой объясняется, как работает данная директива и какие меры предосторожности нужно принять, чтобы код работал правильно. Также вам нужно понять, как работают определения. Для этого вы можете прочитать статью «От начального к среднему уровню: Определения (I)», в которой объясняется всё необходимое для полного понимания кода 08.
Итак, теперь у нас есть заголовочный файл, показанный в коде 07, который можно использовать в других типах кода MQL5. Давайте посмотрим, как использовать этот же класс, но теперь в коде, эквивалентном коду 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. int OnInit() 10. { 11. return INIT_SUCCEEDED; 12. }; 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. }; 18. //+------------------------------------------------------------------+ 19. void OnDeinit(const int reason) 20. { 21. }; 22. //+------------------------------------------------------------------+
Код 09
Чтобы правильно использовать класс в этом коде 09, нам следует вернуться к теме переменных. И снова: не понимая самых простых понятий, вы не сможете понять более сложные. Поэтому, если у вас есть какие-либо сомнения, вернитесь к предыдущим статьям. Наша проблема здесь — время жизни переменной. Иногда это понятие немного усложняет ситуацию.
В коде 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. //+------------------------------------------------------------------+
Код 10
Код 10 выглядит довольно удивительно, если внимательно его проанализировать. При этом мы ничего не создавали явно. Нам нужно лишь объявить в строке 09 переменную типа C_Regression, определённую в файле, подключённом в строке 07. Возможно, вы думаете: "Надо же, этот код вообще ничего не сделает, ведь внутри функции OnInit не выполняется ни одной операции. То же самое происходит и с другими обработчиками событий, в чём можно убедиться, проанализировав OnCalculate и OnDeinit. Итак, этот код совершенно бесполезен. И всё".
Хм. Думаю, вы не до конца поняли, как компилятор управляет работой классов. Поэтому вы и утверждаете, что код 10 совершенно бесполезен. Однако, в отличие от кода 09, который действительно ничего не делает, то же самое нельзя сказать о коде 10. Он действительно выполняет одну операцию: размещает на графике объект, определённый внутри класса C_Regression. "Но почему это происходит, если, по-видимому, внутри функций и процедур, которые будет выполнять MetaTrader 5, нет никакого фрагмента кода? Если это действительно работает, я никак не могу понять, почему. Не могли бы вы объяснить мне это так, чтобы я тоже смог понять?"
Итак, уважаемый читатель, вы должны помнить, что, объявляя переменную, мы выполняем операцию. Однако при работе с классами, в отличие от других типов данных и структур записей, выполняется несколько более сложная операция. Как я уже объяснял, когда компилятор встречает объявление переменной типа класса, он ищет подходящий конструктор. Если класс не объявляет ни одного конструктора, компилятор создаёт его, чтобы можно было правильно инициализировать объект. Если в вашем коде определён подходящий конструктор, компилятор генерирует вызов для выполнения именно того кода, который в нём реализован.
Следовательно, при выполнении строки 09 будет вызван конструктор класса. Поскольку этот конструктор находится в строке восемь кода 07, будет выполнено всё его содержимое и создан объект, с которым мы работали с самого начала статьи. Этот объект будет оставаться на графике до тех пор, пока индикатор не будет окончательно удалён; в данном случае это код 10. Когда это произойдёт, будет уничтожена глобальная переменная, объявленная в строке девять кода 10. Именно в этот момент будет вызван деструктор. То есть будет выполнен весь код, реализованный в деструкторе в строке 18 кода 07.
Как я и сказал в начале, в объектно-ориентированном программировании всё не всегда так, как кажется. Поэтому вам нужно учиться и практиковаться, прежде чем считать, что вы уже освоили то, чего на самом деле ещё не понимаете.
"Хорошо, уважаемый автор. В этом вопросе я с вами согласен. Однако прежде чем закончить, я хотел бы спросить вас кое о чём. Если код 10 работает так, как вы только что объяснили, то, прежде чем закончить, я хотел бы узнать вот что: есть ли какой-нибудь способ контролировать момент, когда будет выполняться код класса? Я хочу избежать странного кода, в котором простое объявление переменной типа класса уже приводит к вызову конструктора. Итак, можем ли мы управлять этим поведением?" Да, уважаемый читатель. Но, чтобы не усложнять эту статью ещё больше, мы пока не будем это рассматривать.
Заключительные замечания
Возможно, именно эта статья оставит больше всего людей в недоумении по поводу того, что делать дальше. Хотя я постарался изложить эту тему как можно проще и доступнее, понять весь материал, просто прочитав статью, практически невозможно. Вам нужно изучить и отработать на практике то, что мы здесь рассмотрели, чтобы по-настоящему понять, как работают эти механизмы и почему они ведут себя именно так.
Я знаю, что поначалу эта тема кажется довольно запутанной. Поэтому я посвятил много времени изучению C++ и попыткам его понять. Хотя некоторые считают, что этому можно научиться за несколько дней, с сожалением должен сказать, что на практике это не так. Чтобы овладеть C++, нужны месяцы, а то и годы, прежде чем вы сможете сказать: "Да, я программист на C++". Однако MQL5 — это не C++. На самом деле MQL5 гораздо проще, чем C++. Это не значит, что знания, накопленные за годы программирования, мне не пригодились. Совсем наоборот. Большая часть того, что я объясняю и показываю в этой статье, основана на моём опыте и на тех трудностях, с которыми я столкнулся, изучая объектно-ориентированное программирование.
Поэтому, уважаемый читатель, хорошо усвойте знания, которыми я делюсь, и начинайте практиковаться и изучать то, что показано в этой небольшой серии статей. Начиная со следующей статьи всё начнёт немного усложняться, если вы не поняли как следует то, что мы здесь рассмотрели и объяснили. Тогда до встречи в следующей статье, где я объясню, как лучше контролировать использование классов, чтобы избежать того, что происходит в коде 10.
| Файл MQ5 | Описание |
|---|---|
| Индикатор\Код 01 | Демонстрационный файл |
| Индикатор\Код 02 | Демонстрационный файл |
| Скрипт\Код 01 | Демонстрационный файл |
| Скрипт\Код 02 | Демонстрационный файл |
| Скрипт\Код 03 | Демонстрационный файл |
| Скрипт\Код 04 | Демонстрационный файл |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/16764
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности
Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh)
Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала
Автоматизация индикатора Market Memory Zones: Зоны, куда цена с высокой вероятностью вернется
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования