
Разработка системы репликации (Часть 64): Нажатие кнопки воспроизведения в сервисе (V)
Введение
В предыдущей статье "Разработка системы репликации (часть 63): Нажатие кнопки воспроизведения в сервисе (IV)" был разработан и реализован механизм, позволяющий пользователю управлять, скажем так, максимальным количеством тиков, которые будут использоваться для рисования бара на графике. Хотя данный элемент управления предназначен для того, чтобы обеспечить построение гистограммы без вмешательства в другие критические места приложения репликации/моделирования, он не влияет на отображаемые значения объема и не искажает их.
Однако, если вы обратили внимание на видео в статье или проверили, работает ли приложение репликации/моделирования, то заметили, что время от времени система переходила в режим паузы. Интересно, что это произошло без изменения состояния индикатора управления. Это в некотором смысле означало, что мы перешли из режима воспроизведения в режим паузы. Признаюсь, это довольно странно. Вы, наверное, задаетесь вопросом, как такое могло произойти. На самом деле, я тоже задавал себе такой вопрос, и пытался понять, почему система переходит в режим паузы в определенные моменты. В следующем разделе мы расскажем о том, какое решение я придумал и как можно понять суть проблемы.
Понимание и решение проблемы автоматического режима паузы
Главная проблема и самый большой недостаток заключается в том, что система репликации/моделирования автоматически переходит в режим паузы. Настоящая проблема заключается в том, что мы поняли вопрос, который, скорее всего, будет вскоре исправлен разработчиками MetaTrader 5, но на момент написания статьи он создает значительные затруднения при использовании некоторых тестов и концепций, связанных с определенными графическими объектами. И это связано с тем, что такие объекты могут изменять свое состояние, а вы не поймете истинной причины.
Возможно, я требую слишком многого. Но давайте вспомним, насколько важны объекты для нашего приложения репликации/моделирования и как мы используем их для управления его работой. Это актуально для тех, кто не следил за серией статей о системе репликации/моделирования.Когда мы запускаем сервис репликации/моделирования, он открывает график, загружает имеющиеся в файле тики, создает пользовательский символ и, наконец, устанавливает на график индикатор. Данный индикатор - наш индикатор управления, функция которого заключается именно в этом: следить за тем, что делает система репликации/моделирования.
На данном этапе мы больше не используем глобальные переменные терминала для доступа или передачи информации между индикатором управления и сервисом репликации/моделирования. Теперь мы используем другую технику, при которой информация передается между двумя программами таким образом, что пользователь не может вмешиваться в передаваемые данные.
По сути, сервис использует пользовательские события для передачи информации на индикатор управления. Индикатор управления передает часть информации через буфер в сервис. Я говорю "часть информации", потому что есть часть данных, которая передается другим способом, чтобы сервис не считывал буфер постоянно. Чтение буфера означает передачу большего количества информации, чем нам на самом деле нужно. По этой причине мы предоставляем сервису прямой доступ к объекту, содержащему индикатор управления, не изменяя его. Данный объект представляет собой кнопку, отображающую текущее состояние выполнения, т.е. кнопку, с помощью которой пользователь указывает, должна ли система находиться в режиме воспроизведения или паузы.
Можно проверить это, изучая код в файле C_Replay.mqh. Чтобы объяснить это более наглядно, давайте посмотрим на следующий фрагмент, который принадлежит именно классу C_Replay.mqh:35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. static bool bTest = false; 39. double Buff[]; 40. 41. if (m_IndControl.Handle == INVALID_HANDLE) return; 42. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 43. { 44. if (bTest) 45. m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1 ? C_Controls::ePause : C_Controls::ePlay); 46. else 47. { 48. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 49. m_IndControl.Memory.dValue = Buff[0]; 50. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 51. if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)) 52. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 53. } 54. }else 55. { 56. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 57. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 58. m_IndControl.Memory._8b[7] = 'D'; 59. m_IndControl.Memory._8b[6] = 'M'; 60. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 61. bTest = false; 62. } 63. } 64. //+------------------------------------------------------------------+
Оригинальный фрагмент исходного кода в файле C_Replay.mqh
Обратите внимание, что в строке 38 у нас есть статическая переменная, которая сообщает процедуре, можем ли мы читать объект напрямую или должны читать буфер индикатора управления. Когда в строке 60 мы отправляем пользовательское событие, в строке 61 мы заставляем следующий вызов прочитать буфер индикатора управления. Таким образом, это чтение происходит не постоянно, а только через определенные промежутки времени, что позволяет избежать значительного влияния на работу сервиса репликации/моделирования. Но главная хитрость, на самом деле, заключается в том, что в строке 51 мы указываем, что следующие чтения будут производиться не через буфер, а через прямой доступ к графическому объекту. Однако это произойдет только в том случае, если мы находимся в режиме воспроизведения. Если мы находимся в режиме паузы, время отклика не будет столь критичным фактором.
Затем, если мы находимся в режиме воспроизведения, с третьего вызова будет выполнена строка 45. Это будет продолжаться до тех пор, пока пользователь явно не включит режим паузы. Когда это произойдет, функция LoopEventOnTime, которая находится в классе C_Replay, будет завершена. Но поскольку пользователь только указал, что сервис должен перейти в режим паузы, функция LoopEventOnTime будет вызвана снова, что заставит предыдущий фрагмент опять наблюдать за индикатором управления. Тем не менее, мы продолжим рассматривать индикатор через графический объект. Здесь есть один недостаток, который затрудняет применение иного подхода, но это не является причиной для автоматического режима паузы. Режим паузы не активируется из сервиса, он активируется на индикаторе управления. И причина заключается именно в пользовательском событии, присутствующем в строке 60 приведенного выше фрагмента. Теперь ситуация усложняется: почему нечто, используемое нами для запуска пользовательского события, делает так, что сервис ошибочно получает информацию от индикатора управления о том, что пользователь включил режим паузы? Какое странное дело! Это действительно необычно. Но каким-то образом пользовательское событие заставляет индикатор изменять значение свойства OBJPROP_STATE объекта, за которым следит сервис. Когда это свойство изменяется, строка 45 приведенного выше фрагмента приводит к тому, что сервис получает ложное уведомление о том, что индикатор перешел в режим паузы. Это приводит к выходу и повторному запуску функции LoopEventOnTime. Однако, когда данная функция перепроверяет значение свойства OBJPROP_STATE, она обнаруживает неверное значение. При этом сервис переходит в режим паузы, а индикатор дальше показывает пользователю, что система работает нормально, т.е. в режиме воспроизведения.
Хорошо. Если вы действительно разобрались в ошибке, то наверное подумали, что проблема заключается в том, что мы смотрим на объект на графике, вместо того, чтобы проверять содержимое буфера индикатора. И вы правы. Вся проблема заключается в том, что мы заставляем сервис наблюдать за значением свойства OBJPROP_STATE объекта на графике, за которым в действительности сервис наблюдать не должен. Я полностью согласен, но это не оправдывает того, что свойство OBJPROP_STATE изменилось из-за срабатывания пользовательского события. Одна ошибка не оправдывает другую. В любом случае, есть два более простых решения этой проблемы. Один из вариантов - использовать другое свойство, которое позволяет сервису наблюдать только за тем, что делает объект на графике. Хотя данное решение справилось бы с проблемой, я не буду его реализовывать. Дело в том, что есть еще одна проблема, которую нам нужно исправить, или, скорее, реализовать.
Хотя это означает небольшое увеличение времени доступа к буферу индикатора, вместо того, чтобы смотреть на объект на графике, я предпочитаю смотреть на буфер индикатора. Это связано с тем, что я буду реализовывать функцию, которой сейчас нет, но которая была доступна в предыдущих версиях данного сервиса репликации/моделирования. Данный ресурс - не что иное, как ускоренная перемотка. Поэтому после внесения изменений и удаления статической переменной из подпрограммы результат будет таким:
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
Измененный фрагмент исходного кода в файле C_Replay.mqh
Несмотря на все неудобства, это будет лучшим решением для нас, даже если это означает, что происходит что-то странное и что свойства некоторых объектов не совсем надежны. По крайней мере, так можно сказать на момент написания статьи. Однако, если нам по каким-либо причинам необходимо, чтобы сервис считывал данные из объекта на графике, я рекомендую использовать свойство OBJPROP_TOOLTIP. Несмотря на то, что это тип string, я не столкнулся с проблемами при использовании его в тестах на передачу информации, чтобы определить, находится ли индикатор управления в режиме паузы или воспроизведения. Однако, хотя такое решение доступа к объекту было подходящим, оно не позволяло нам реализовать быструю перемотку. Потребуется несколько дополнительных изменений в коде, и в итоге нам всё равно придется изменить процедуру UpdateIndicatorControl, хотя она и останется такой, как показано на рисунке.
Хорошо, теперь, когда мы решили эту проблему, давайте разберемся с другой, не менее раздражающей проблемой, которая была представлена на видео в статье "Разработка системы репликации (часть 62)". Нажатие кнопки воспроизведения в сервисе (III)". Чтобы объяснить, как всё это решилось, мы перейдем к другой теме.
Решение проблемы утечки памяти
Вполне вероятно, что многие из вас удивились, когда увидели, что приложение внезапно дало сбой без какого-либо предупреждения. Это происходило при завершении приложения, поскольку график закрывался или уже был закрыт. Менее опытный программист может сразу же ассоциировать эту проблему с MetaTrader 5 или MQL5. На самом деле, проще обвинить кого-то другого, чем взять на себя ответственность за то, что программа ведет себя неадекватно или содержит ошибки, которые мы еще не успели исправить или изучить. Это может быть связано с отсутствием достаточных знаний или опыта по устранению ошибок или с тем, что мы больше сосредоточены на разработке конкретной функции, а не на исправлении ошибок. В принципе, ожидать, что платформа сделает всё за нас, - значит расписаться в своем невежестве или наивности. В действительности платформа будет делать только то, для чего она предназначена. Вся остальная работа по исправлению ошибок и обеспечению правильного функционированию приложения ложится на нас, как на программистов.
Некоторое время я откладывал решение отдельных проблем и улучшение частей кода. Я был настолько сосредоточен на развитии других идей, что отбросил всё это, ведь я не был уверен, что удастся достичь определенных целей. В таких случаях нас просто не волнует исправление ошибок или улучшение кода; мы просто хотим, чтобы он работал или имел определенные функции. Тратить время на работу над ошибками, а потом избавляться от всего исправленного кода, если что-то оказалось неприемлемым для реализации или поддержки, может быть удручающим. Поэтому исправления и улучшения происходят только тогда, когда код оказывается достаточно надежным, чтобы уделять ему необходимое внимание.
Вы можете подумать, что я слишком много хожу вокруг да около. Или я мог бы показать гораздо более продвинутый код с самого начала. Но на самом деле это создаст ложную иллюзию у начинающих, заставив их поверить, что код рождается готовым, или, что еще хуже, что он должен появляться со всеми уже реализованными функциями и возможностями. Однако те, кто занимается программированием уже много лет, знают, что это не так. Я хочу показать, как на самом деле разрабатывается код и как мы справляемся с возникающими проблемами и решаем их.
В любом случае, пришло время показать, как исправить ошибку, которая присутствует в коде с тех пор, как мы заставили его работать по-другому. Если вы не просматривали код, чтобы выяснить причины, то можете подумать, что это связано с различными недостатками, но это не так. Мне жаль, но если вы так думаете, то, скорее всего, вы не стремитесь изучать и понимать эти статьи. То, что вы ищете, - это просто умный код, без всякого интереса к изучению программирования. Цель этих статей - показать вам то, что уже опробовал или разрабатывает другой программист, чтобы вы могли узнать новые техники или открыть для себя другие способы достижения определенных результатов.
Но давайте вернемся к делу. Теперь рассмотрим, как решить проблему утечки памяти. Первое, что нужно сделать, - посмотреть на содержание сообщений, которые выдает нам MetaTrader 5. Чтобы было понятнее, покажем одно из таких сообщений на рисунке ниже:
Рисунок 01 - Отображение ошибок, указанных MetaTrader 5
Обратите внимание: на этом изображении выделены две линии. Остальные линии связаны с ними. Если присмотреться, можно увидеть, где кроется причина. А это происходит так, потому что некоторые объекты не удаляются с графика. Вы можете спросить: как такое возможно? Почему объекты не удаляются из графика? Может быть я забыл удалить их? Нет. Объекты действительно удаляются; данный процесс происходит в деструкторе класса. Это можно подтвердить, проверив код индикатора управления. Сбой происходит именно там, как показано на рисунке выше:
Но если объекты удаляются, почему MetaTrader 5 предупреждает нас об объектах, которые не удаляются? Хуже того, эти объекты принадлежат классу C_DrawImage.
Столкнувшись с этой ситуацией, вполне нормально, что вы не знаете, что делать. Это происходит, потому что доступ к классу C_DrawImage осуществляется не напрямую, а косвенно. Чтобы лучше объяснить это, давайте рассмотрим код класса C_Controls, который отвечает за управление всеми объектами. Одна из их задач - получить доступ к данному классу. Запомните: MetaTrader 5 предупреждает нас об ошибке, связанной с удалением объектов типа C_DrawImage. Таким образом, проблема не в классе C_DrawImage, а в коде, который использует данный класс, т.е. в классе C_Controls.
Поскольку для понимания проблемы и ее решения нет никакой нужды видеть весь код, ниже можно ознакомиться с наиболее важными фрагментами. Однако есть одна маленькая деталь: в коде, показанном ниже, решение проблемы утечки памяти уже реализовано.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Auxiliar\C_DrawImage.mqh" 005. #include "..\Defines.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_PathBMP "Images\\Market Replay\\Control\\" 008. #define def_ButtonPlay def_PathBMP + "Play.bmp" 009. #define def_ButtonPause def_PathBMP + "Pause.bmp" 010. #define def_ButtonLeft def_PathBMP + "Left.bmp" 011. #define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" 012. #define def_ButtonRight def_PathBMP + "Right.bmp" 013. #define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" 014. #define def_ButtonPin def_PathBMP + "Pin.bmp" 015. #resource "\\" + def_ButtonPlay 016. #resource "\\" + def_ButtonPause 017. #resource "\\" + def_ButtonLeft 018. #resource "\\" + def_ButtonLeftBlock 019. #resource "\\" + def_ButtonRight 020. #resource "\\" + def_ButtonRightBlock 021. #resource "\\" + def_ButtonPin 022. //+------------------------------------------------------------------+ 023. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A)) 024. #define def_PosXObjects 120 025. //+------------------------------------------------------------------+ 026. #define def_SizeButtons 32 027. #define def_ColorFilter 0xFF00FF 028. //+------------------------------------------------------------------+ 029. #include "..\Auxiliar\C_Mouse.mqh" 030. //+------------------------------------------------------------------+ 031. class C_Controls : private C_Terminal 032. { 033. protected: 034. private : 035. //+------------------------------------------------------------------+ 036. enum eMatrixControl {eCtrlPosition, eCtrlStatus}; 037. enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)}; 038. //+------------------------------------------------------------------+ 039. struct st_00 040. { 041. string szBarSlider, 042. szBarSliderBlock; 043. ushort Minimal; 044. }m_Slider; 045. struct st_01 046. { 047. C_DrawImage *Btn; 048. bool state; 049. short x, y, w, h; 050. }m_Section[eObjectControl::eNull]; 051. C_Mouse *m_MousePtr; 052. //+------------------------------------------------------------------+ ... 071. //+------------------------------------------------------------------+ 072. void SetPlay(bool state) 073. { 074. if (m_Section[ePlay].Btn == NULL) 075. m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay); 076. m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0); 077. if (!state) CreateCtrlSlider(); 078. } 079. //+------------------------------------------------------------------+ 080. void CreateCtrlSlider(void) 081. { 082. if (m_Section[ePin].Btn != NULL) return; 083. CreteBarSlider(77, 436); 084. m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock); 085. m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock); 086. m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin); 087. PositionPinSlider(m_Slider.Minimal); 088. } 089. //+------------------------------------------------------------------+ 090. inline void RemoveCtrlSlider(void) 091. { 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 093. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 094. { 095. delete m_Section[c0].Btn; 096. m_Section[c0].Btn = NULL; 097. } 098. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B")); 099. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 100. } 101. //+------------------------------------------------------------------+ ... 132. //+------------------------------------------------------------------+ 133. public : 134. //+------------------------------------------------------------------+ 135. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 136. :C_Terminal(Arg0), 137. m_MousePtr(MousePtr) 138. { 139. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 140. if (_LastError != ERR_SUCCESS) return; 141. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 142. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 143. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 144. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 145. { 146. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 147. m_Section[c0].y = 25; 148. m_Section[c0].Btn = NULL; 149. } 150. m_Section[ePlay].x = def_PosXObjects; 151. m_Section[eLeft].x = m_Section[ePlay].x + 47; 152. m_Section[eRight].x = m_Section[ePlay].x + 511; 153. m_Slider.Minimal = eTriState; 154. } 155. //+------------------------------------------------------------------+ 156. ~C_Controls() 157. { 158. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 159. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 160. delete m_MousePtr; 161. } 162. //+------------------------------------------------------------------+ ... 172. //+------------------------------------------------------------------+ 173. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 174. { 175. short x, y; 176. static ushort iPinPosX = 0; 177. static short six = -1, sps; 178. uCast_Double info; 179. 180. switch (id) 181. { 182. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 183. info.dValue = dparam; 184. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 185. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 186. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 187. break; 188. case CHARTEVENT_OBJECT_DELETE: 189. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 190. { 191. if (sparam == def_ObjectCtrlName(ePlay)) 192. { 193. delete m_Section[ePlay].Btn; 194. m_Section[ePlay].Btn = NULL; 195. SetPlay(m_Section[ePlay].state); 196. }else 197. { 198. RemoveCtrlSlider(); 199. CreateCtrlSlider(); 200. } 201. } 202. break; 203. case CHARTEVENT_MOUSE_MOVE: 204. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 205. { 206. case ePlay: 207. SetPlay(!m_Section[ePlay].state); 208. if (m_Section[ePlay].state) 209. { 210. RemoveCtrlSlider(); 211. m_Slider.Minimal = iPinPosX; 212. }else CreateCtrlSlider(); 213. break; ... 249. //+------------------------------------------------------------------+
Фрагменты исходного кода C_Controls.mqh
Как уже ранее говорилось, если вы только начинаете изучать программирование, вы можете не сразу понять, как было получено решение. Это связано с тем, что при беглом просмотре кода вы не заметите существенной разницы. Причина в том, что решение не требует большого объема кода и не влечет за собой серьезных изменений в классе. Исправление заключалось в добавлении одной строки в код класса. То, что вы только что прочитали - верно. Чтобы решить проблему утечки, необходимо было добавить очень простой, но крайне важный кусок кода, который трудно представить начинающему программисту. Однако фрагмент состоит из одной линии, которую каждый может понять. Это тривиально, но без него произойдет вышеупомянутая утечка памяти, указывающая на то, что объекты, принадлежащие классу C_DrawImage, не удаляются правильно.
Теперь давайте посмотрим, почему проблема возникает в классе C_Controls, а не в классе C_DrawImage. Как уже говорилось, доступ к классу C_DrawImage осуществляется не напрямую, а косвенно. И это происходит так, потому что доступ к классу C_DrawImage осуществляется через указатель, который объявлен в строке 47. Примечание: на класс C_DrawImage ссылается указатель. Однако, поскольку указатели в MQL5 несколько отличаются от указателей в C/C++, можно рассматривать строку 47 как переменную. Однако это всё равно указатель. Проблема не в объявлении переменной как указателя, а в том, как мы работаем с этим указателем.
Значительный недостаток указателей и, возможно, причина, по которой они отличаются в MQL5 от C/C++, заключается в том, что проблемы, связанные с указателем, может быть очень сложно решить. Некоторые из упомянутых ошибок настолько сложны, что возникают только при определенных взаимодействиях, что делает их исправление очень сложным. Можно протестировать программу сотни раз, но в одном из этих взаимодействий возникнет проблема, и часто это происходит именно тогда, когда мы не отлаживаем программу. Это, несомненно, худшая из возможных ситуаций.
В соответствии с правилами программирования, когда мы объявляем что-то как указатель, или функцию как указатель, мы должны инициализировать это как можно скорее. И это относится и к переменным в целом. Поскольку наш код представляет собой класс, мы должны выполнить инициализацию в конструкторе этого класса. Это уже происходило, как можно увидеть в строках 144-149 (если быть точным, указатель инициализируется в строке 148). Обратите внимание, что мы инициализируем его со значением NULL.
А теперь самое интересное. Конструктор класса можно вызвать двумя разными способами. Первый - когда на класс ссылаются как на обычную переменную. В данном случае серьезных проблем не возникает, так как компилятор сам резервирует память, необходимую для стабильной работы класса. Второй способ - когда на класс ссылаются указатели. В данном случае мы должны вызвать конструктор класса с помощью оператора new и вызвать деструктор с помощью оператора delete.
Хорошо. В строке 156 реализован деструктор класса C_Controls. Теперь обратите внимание на следующий факт, поскольку он имеет решающее значение для понимания ошибки. Когда деструктор в строке 156 выполняет строку 158, он вызывает деструктор класса C_DrawImage. Данный деструктор может существовать или нет, но важно то, что при его выполнении освобождается память, которая была выделена указателю. Если всё сделано правильно, MetaTrader 5 не генерирует предупреждений о проблемах с утечкой памяти. Другими словами, память освобождается без сбоев. Однако на самом деле всё обстоит иначе. Возникает вопрос: почему?
Причина в том, что где-то в коде что-то мешает оператору DELETE, а единственным элементом, способным это сделать является оператор NEW. Таким образом, проблема заключается именно в нем. Но почему происходит сбой? Причина в том, что оператор NEW в MQL5, похоже, работает так же, как и в C/C++. В некоторых языках программирования оператор NEW ведет себя иначе, но в данном случае это не так. Давайте тогда разберемся в том, что происходит на самом деле.
Когда необходимо создать объекты, с которыми будет работать класс C_DrawImage, это делается с помощью оператора new. Данный тип операции можно увидеть в следующих строках:
- В строке 75 создается кнопка, отображающая изображение воспроизведения и паузы.
- В строках 84-86 создаются кнопки, которые будут использоваться в ползунке.
Однако только в двух точках память класса C_DrawImage эффективно освобождается. Они находятся в деструкторе класса C_Controls и в строке 95. Теперь я хочу обратить внимание на следующий факт: в строке 96, сразу после освобождения памяти с помощью оператора DELETE, указатель снова устанавливается в NULL. Почему было сделано именно так? Причина в том, что если мы попытаемся сослаться на область памяти, указатель которой не указывает на что-то действительное, мы можем прочитать «мусорные» данные, записать критическую информацию или даже выполнить вредоносный код. По этой причине хорошей практикой является обеспечение того, чтобы недействительные указатели всегда имели значение NULL. ВСЕГДА. Однако такой код уже существовал, а ошибка всё равно возникала.
Тогда вернемся к точкам доступа операторов NEW. Строка 74 уже существовала с самого начала написания кода этого класса. Это гарантирует, что, когда указатель не используется, он будет на что-то указывать. Поэтому ошибки здесь нет. Однако строки 82 первоначально не существовало. Почему её не было? Я не могу ответить с уверенностью. Возможно, это какая-то оплошность. Может быть, я не задумывался, что это может что-то изменить, я действительно не знаю. Но именно отсутствие этой строки 82 привело к тому, что MetaTrader 5 предупредил о сбое.
Не знаю, осознаете ли вы важность такой банальной вещи, как простой тест для проверки того, занят указатель или нет. Возможно, проблема была даже не в ошибке с нашей стороны, поскольку процедура CreateCtrlSlider должна вызываться только в том случае, если ранее была выполнена процедура RemoveCtrlSlider. Однако есть одно место, где это не происходит. И это строка 212. Данная проблема заставляет оператор NEW повторно выполнять вызовы конструктора класса C_DrawImage, что выделяет больше памяти и дублирует объекты. Ранее выделенная память не освобождается, поэтому она накапливается и занимает всё больше и больше места в памяти, пока приложение не будет закрыто. В этот момент MetaTrader 5 сообщает, что произошел сбой приложения.
Заключение
Как мы уже говорили, в некоторых языках есть механизм, который не позволяет уже используемому указателю указывать на что-то другое. Однако, насколько нам удалось выяснить, в MQL5 это не так. Показанное здесь - это не ошибка в MQL5, а то, что вы должны знать и уметь делать при программировании. Хотя многие утверждают, что в MQL5 нет указателей, вы можете убедиться, что это не совсем так. И да, если не принять правильных мер предосторожности, можно столкнуться с серьезными проблемами даже в простых и, казалось бы, незамысловатых программах реализации.
В течение многих лет я сталкивался с подобными ошибками, когда указатель просто становился неуправляемым. И, даже несмотря на весь накопленный опыт, я продолжаю сталкиваться с ошибками, связанными с указателями. Это происходит из-за того, что не была выполнена проверка в строке 82, которая гарантирует, что уже занятый указатель не будет использован. Также я не обратил внимания на вызов строки 212, которая выполняется при каждом движении мыши, если позволяют условия, и находится внутри события мыши.
Искренне надеюсь, что эти знания будут вам полезны и послужат предостережением. Никогда не недооценивайте указатели. Это очень полезные инструменты, но они также могут стать серьезной головной болью. На следующем видео можно увидеть, как выглядит система без сбоев.
Демонстрационное видео
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12250





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