
От начального до среднего уровня: Шаблон и Typename (II)
Введение
В предыдущей статье, "От начального до среднего уровня: Шаблон и Typename (I)", мы начали разговор о довольно сложной, но очень увлекательной теме: создании шаблонов функций и процедур. Поскольку данная тема довольно сложна для рассмотрения и объяснения в нескольких статьях, мы разделим ее на большее число статей. Однако мы не будем слишком углубляться только в эту тему, прежде чем перейти к другим, не менее интересным, поскольку есть вещи, которые будут иметь смысл только в том случае, если будут затронуты и другие темы.
Но некоторое время мы будем работать только над данной темой, пока не создадим достаточно прочную и широкую базу, чтобы можно было перейти к другим, до возвращения к шаблонам. Ведь на самом деле эта тема очень обширная. В любом случае, необходимо прояснить несколько моментов, о которых мы не упомянули в предыдущей статье, именно для того, чтобы не усложнять вопрос. Я хочу, чтобы вы чувствовали себя комфортно, изучая каждую из этих статей, и чтобы они помогли вам начать программировать более правильно и безопасно, имея хотя бы хорошее представление о каждом инструменте, доступном в MQL5.
Хотя многое из описанного здесь, применимо и к другим языкам, необходимо уделить должное внимание применению этих концепций.
После этих слов, пришло время расслабиться, убрать возможные отвлекающие факторы и сосредоточиться на том, о чем пойдет речь в этой статье. Здесь мы поговорим немного подробнее о шаблонах.
Представленный здесь материал предназначен для дидактических целей. Ни в коем случае нельзя рассматривать это приложение как окончательное, цели которого будут иные, кроме изучения представленных концепций.
Шаблоны, больше шаблонов
Один из самых интересных моментов, связанных с шаблонами, заключается в том, что при правильном планировании они становятся незаменимым инструментом. Это происходит, потому что в итоге мы создаем то, что многие могут назвать моделью быстрого внедрения. Другими словами, нам больше не нужно программировать всё, а только часть необходимых деталей.
Скорее всего, вы даже не представляете, о чем я сейчас идет речь. Но по мере изучения и практики в программировании вы обнаружите, что многие вещи можно делать гораздо быстрее, если предпринять определенные шаги. Знание и понимание всех имеющихся в нашем распоряжении инструментов позволяет выбрать наилучший путь, НЕ ПРИВЯЗЫВАЙТЕСЬ К НАЗВАНИЯМ. Постарайтесь понять принятую концепцию, и со временем удастся взять правление в свои руки и выстроить свой собственный путь.
Давайте теперь начнем с очень простой реализации, основанной на том, что было показано в предыдущей статье. Она приведена ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(-10.0, 25.0)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> 11. T Sum(T arg1, T arg2) 12. { 13. Print(__FUNCTION__, "::", __LINE__); 14. return arg1 + arg2; 15. } 16. //+------------------------------------------------------------------+
Код 01
В коде 01, о котором говорилось в предыдущей статье, у нас есть возможность использовать одну и ту же функцию для работы с разными типами данных. Однако есть здесь кое-что, мягко говоря, раздражающее. Проблема заключается в том, что в строках 06 и 07 у нас есть данные разных типов. Как вы могли заметить, изучив содержание предыдущей статьи, компилятор прекрасно справляется с данной проблемой, создавая перегруженные функции, так что в итоге не нужно определять, какой тип данных используется. Но именно здесь находится неприятная часть: типы данных, используемые здесь, должны быть одинаковыми, то есть, если первый аргумент, передаваемый шаблону функции Sum, имеет тип float, то второй аргумент также ДОЛЖЕН иметь тип float. Иначе компилятор выдаст ошибку или, в лучшем случае, предупреждение о том, что между используемыми типами данных существует проблема.
Для лучшего понимания, давайте изменим одну из строк. Это может быть строка 06 или 07 кода 01, так что в функцию будут передаваться разные типы. Помните, что на самом деле функции еще не существует. Компилятор должен собрать ее на основе предоставленного шаблона. Таким образом, мы изменили код 01, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(T arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
Код 02
Прошу заметить, что изменили только те значения, которые передавались в функцию. В данном случае решили изменить строку 07, как можно видеть, сравнив код 01 с кодом 02. Однако, несмотря на это небольшое и невинное изменение в коде, посмотрите, что получилось, когда мы попытались его скомпилировать.
Рисунок 01
Данные сообщения, которые показываются на изображении 01, могут немного отличаться в зависимости от конкретного случая. Читатель может запутаться, заметив, что компилятору не удалось установить базу ссылок, поскольку единственным изменением были значения в строке 07 из кода. Это именно тот раздражитель, о котором мы говорили раньше. Поскольку наш пример носит чисто дидактический характер, можно подумать: «Почему компилятор не может создать код на основе предоставленного шаблона?» Причина в том, что, поскольку первый аргумент является целочисленным значением, мы не можем поместить во второй аргумент значение с плавающей точкой. Или наоборот, когда мы сначала помещаем значение с плавающей точкой, а затем целочисленное значение.
Поскольку шаблон функции Sum определяется в коде 02, компилятору не удается создать подходящую функцию для выполнения того, что ожидается от функции шаблона, то есть функции Sum. Многие новички в итоге сдаются и выбирают другой подход к решению проблемы, когда всё можно было бы решить очень просто. Однако, чтобы правильно решить данный вопрос, необходимо сначала понять, что ожидается от функции или процедуры, используемой в качестве шаблона, чтобы создать другие перегруженные процедуры или функции.
Поскольку наша цель - чисто дидактическая, используемая нами функция очень проста. Всё, что мы ожидаем от нее, - это сложить два значения и вернуть результат этой суммы. Если вы уже пытались прибавить значение с плавающей точкой к целочисленному значению, то помните, что в результате получится значение с плавающей точкой.
Подобные вещи известны как преобразование типов, или приведение типов, и мы говорили об этом, когда обсуждали переменные и константы. По сути, всё сводится к следующему:
Рисунок 02
Исходя из изображения 02, мы знаем, что тип double используется во всех случаях, когда математические операции выполняются с разными типами, как это делается в строке 07 из кода 02. Зная это и имея представление о том, что компилятор использует не перегруженную функцию, а шаблон, мы можем заставить компилятор понять, что мы в курсе происходящего. Таким образом, мы можем изменить код 02, чтобы он действительно работал, как показано в приведенной ниже реализации.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum((double) 5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(T arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
Код 03
Прошу заметить, что в коде 03 мы используем тот же код, что и в коде 02. Однако здесь мы заставляем компилятор понять, что мы знаем о том, что имеем дело с типом double, даже если значение является переменной целочисленного типа. Это явное преобразование типа, выполненное с помощью добавления термина в строку 07 из кода 03, отличает эту же строку 07 из кода 02 и позволяет компилятору использовать шаблонную функцию Sum таким образом, чтобы создать соответствующую перегрузку. Благодаря этому конечный результат будет таковым:
Рисунок 03
Дело в том, что представленное нами решение - не единственно возможное. Мы можем сделать что-то немного по-другому и получить тот же результат. Главное понять, что именно мы хотим реализовать и какова наша цель на данный момент. Поэтому вместо того, чтобы просто запоминать код, или, что еще хуже, использовать старую тактику CTRL+C и CTRL+V, нам нужно действительно понять используемые концепции. Только так мы сможем решить все проблемы, связанные с программированием, по мере их возникновения.
Другой способ решения - это ограничить один из аргументов определенным типом. Хотя многие не считают данный метод подходящим, он позволяет решать различные типы задач в очень специфических ситуациях, когда мы заранее знаем, что всегда будем использовать определенный тип информации в конкретном аргументе.
Итак, предполагая, что у первого аргумента ВСЕГДА будет тип integer, мы можем сделать что-то вроде этого:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(short arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
Код 04
Результат этого кода такой же, как и у кода 03, т.е. у изображения 03. Однако в коде 04 мы сообщаем компилятору, что один из аргументов шаблонной функции всегда должен быть типа integer. В данном случае мы используем 16-битный тип со знаком, но он может быть и любым другим. Обратите внимание, что в данной ситуации нам не нужно явно выполнять преобразование типов. Это связано с тем, что компилятор уже знает, как поступить в этой ситуации. Однако важно подчеркнуть, что второй аргумент будет определен компилятором на этапе создания исполняемого файла. Поэтому у нас будет одна функция для ответа на вызов в строке 06, где мы используем только целочисленные типы, и другая функция для ответа в строке 07, где мы используем целочисленный тип и плавающую точку.
А теперь я спрошу вас: разве не весело играть и практиковаться в таких сценариях? Прошу заметить, что мы указываем компилятору, как ему работать. И таким образом нам удается без особых усилий создать различные возможности построения одной и той же вещи.
Но не думайте, что всё кончено. Есть и другой способ решения этой проблемы. Однако в этом случае всё становится немного сложнее, и возникает проблема другого вида, чье решение будет показано в другой раз. Тем не менее, это действительно заслуживает внимания, поскольку подходит для различных ситуаций и может даже открыть двери в новый мир возможностей использования и способов работы с MQL5.
Однако, чтобы не запутаться, мы рассмотрим это в другой теме. Сначала изучите эти понятия и только потом попробуйте усвоить то, что будет дальше.
Один шаблон, несколько типов
В предыдущей теме мы рассмотрели, как справиться с довольно неприятной проблемой, которая иногда ограничивает нас в использовании шаблона. Однако изученный материал - лишь первая часть того, что мы действительно можем сделать. То, что мы рассмотрим здесь, не очень распространено, по крайней мере, в MQL5, поскольку пока я не помню, чтобы кто-то использовал подобную методологию. Многие могут подумать, что такой возможности не существует или она не может быть реализована, а значит, у них будут руки связаны, и они не смогут достичь своей главной цели.
Однако тот факт, что вы не видели или не слышали об использовании чего-либо, не означает, что этого не существует или что это не принято в языке программирования. Во многих случаях проблема заключается даже не в этом, а в других видах проблем, которые могут возникать именно из-за неправильного использования или (как это происходит в большинстве случаев) из-за непонимания некоторых концепций, связанных с инструментом программирования.
В предыдущей статье мы объяснили, что T, используемый в шаблоне, на самом деле является идентификатором, который будет использоваться компилятором для локальной идентификации определенного типа. Это необходимо для того, чтобы знать, как работать с поступающей информацией. Если мы понимаете концепцию идентификатора, то знаете, что если идентификатор объявлен правильно, то он будет вести себя как переменная или как константа.
В случае с идентификатором, которым является тот T, который мы видим в строке 10 из кода 04, он НЕ ЯВЛЯЕТСЯ ПЕРЕМЕННОЙ, поскольку не может быть изменен после определения. «Тогда, очевидно, что это константа». Однако это определенная компилятором константа, представляющая ожидаемый тип данных.
Прошу заметить: когда говорится о типе данных, имеется в виду содержимое из изображения 02. Поэтому важно понять концепцию, а не заучивать формулы или модели реализации. Поняв эту концепцию, которая, хотя и проста, но, как вы вскоре заметите, весьма мощна, мы можем создать почти что магическое решение, которое будет понимать, как шаблон должен использоваться для создания чего-либо, будь то функция или процедура. Для этого нужно будет добавить столько идентификаторов, сколько необходимо, чтобы охватить как можно больше разрешенных случаев. Это решение будет за вами, на этапе внедрения кода.
Затем мы можем изменить код 04 и найти что-то более похожее на код 02, но без проблемы, обнаруженной в коде 02, в которой можно было бы использовать только один тип данных. Звучит сложно, но на самом деле всё гораздо проще, чем можно себе представить. Давайте посмотрим, как будет выглядеть решение этой проблемы с применением новой концепции использования нескольких идентификаторов типов.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(5, 35.2)); 10. } 11. //+------------------------------------------------------------------+ 12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (T1)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+
Код 05
Вот тут-то всё и становится по-настоящему сложным, несмотря на то, что мы проявили осторожность и показывали всё постепенно. Однако проблема здесь кроется именно в том, что мы делаем в строке 12, где происходит объявление шаблона функции Sum.
После выполнения код 05 приведет к результату из изображения ниже.
Изображение 04
Мы выделяли один момент на этом изображении именно для того, чтобы привлечь внимание читателя. Прошу заметить, что результат операции неверен. Вернее, он НЕ СООТВЕТСТВУЕТ ожидаемому значению, так как ожидаемое значение здесь было бы таким же, как на изображении 03. Но почему? Можно предположить, что дело в том, как написана строка 12, поскольку это, очевидно, не имеет никакого смысла. Однако проблема НАХОДИТСЯ НЕ В СТРОКЕ 12, как вы могли бы предположить, а в строке 09 или в строке 16, в зависимости от того, как вы анализируете код или как он должен был быть реализован. Поэтому важно понимать принятые концепции и всегда думать, прежде, чем писать код как попало.
Вы можете мне не верить. Итак, МЫ НЕ БУДЕМ ТРОГАТЬ СТРОКУ 16, а строку 09 будем, изменив порядок объявления значений. Это приводит нас к коду, представленному ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(35.2, 5)); 10. } 11. //+------------------------------------------------------------------+ 12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (T1)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+
Код 06
Прошу заметить, что мы изменили только то, о чем рассказали, а результат выполнения кода можно увидеть ниже.
Изображение 05
«Что за безумный и бессмысленный поступок! Теперь я боюсь браться за программирование, пока лучше не разберусь в вопросе. Мне показалось, что всё это очень легко. Я даже считал себя программистом, ведь мне удавалось писать небольшие фрагменты кода, которые работали. Но, глядя на это, я понимаю, что по-прежнему не знаю абсолютно ничего. Я только начинаю узнавать, что значит быть настоящим программистом».
Успокойтесь, всё не так уж плохо. На самом деле не всегда всё также просто, как кажется на первый взгляд, особенно когда мы попадаем в зону комфорта и не выходим из нее. Но проблема в том, что многие считают себя хорошими программистами и, из-за этого, просто перестают учиться. Поэтому я подтверждаю:
Хороший программист НИКОГДА НЕ ПЕРЕСТАЕТ УЧИТЬСЯ. Он постоянно актуализирует знания и изучает новые концепции. ВСЕГДА.
Вещи, подобные тому, что вы только что увидели, фактически УНИЧТОЖАЮТ моральный дух программиста, тем более, когда он получает в руки код, который, в принципе, правильный. И, в сущности, он правильный. Однако он всегда возвращает неверные результаты без очевидных причин. Поэтому не обманывайте себя: для того, чтобы считаться программистом, недостаточно уметь писать код. Чтобы достичь такого уровня, нужно пройти через многое, и во многих случаях, только время научит вас. Я сам прошел через такое и скажу вам: мне потребовалось много времени, чтобы научиться справляться с такими вопросами. Я даже начал понимать, почему в один момент код может работать, а в другой - сходить с ума, выдавая бессмысленные, на первый взгляд, результаты.
Но давайте разберемся, что происходит и в коде 05, и в коде 06, ведь это одно и то же: единственное различие заключается в простом изменении последовательности объявления параметров в строке 09.
Когда компилятор встречает вызов шаблона, который мы объявляем в строке 12, он проверяет, какой тип данных используется в каждом аргументе, точно так же, как и раньше, создавая перегруженную функцию для обслуживания этого конкретного вызова, в случае если ни одна из ранее созданных функций не может обслуживать текущий шаблон.
Поскольку в строке 12 у нас есть два typename для обозначения двух разных типов, мы можем использовать два совершенно разных типа данных, что раньше было невозможно. Это прекрасно охватывает большое количество случаев, поскольку, по сути, значения могут быть как целочисленными, так и с плавающей точкой, и нам не нужно беспокоиться об этом в самом начале. Однако из-за специфических проблем такое моделирование охватывает не все случаи, но это уже выходит за рамки сегодняшней темы. Зная, что мы можем одновременно и без проблем использовать как целочисленные данные, так и данные с плавающей точкой, мы можем получить почти идеальный шаблон.
Таким образом, компилятор поместит в константу T1 тип данных, объявленный в первом аргументе, в то время как тип данных, используемый во втором аргументе, будет помещен в константу T2. Затем, выполнив преобразование типа в строке 16, чтобы он соответствовал возвращаемому типу функции Sum, мы можем получить результат, подобный тому, что показан на изображениях 04 или 05.
Поскольку, глядя на код шаблона, мы имеем лишь смутное представление о том, что на самом деле соберет компилятор, трудно понять, почему мы получили такие разные результаты всего лишь за счет простой модификации порядка объявления значений в строке 09.
Для лучшего понимания, давайте посмотрим, какой будет функция, написанная компилятором, в обоих случаях. Тогда, если предположить, что мы используем не шаблон, а традиционную кодировку, в качестве кода 05 мы получим следующее:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(5, 35.2)); 10. } 11. //+------------------------------------------------------------------+ 12. int Sum(int arg1, int arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (int)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+ 19. int Sum(int arg1, double arg2) 20. { 21. Print(__FUNCTION__, "::", __LINE__); 22. 23. return (int)(arg1 + arg2); 24. } 25. //+------------------------------------------------------------------+
Код 07
При выполнении кода 07 получится именно то, что показано на изображении 04. А код 06, если бы он был создан в традиционной модели программирования, имел бы в качестве внутреннего содержания то, что можно увидеть ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(35.2, 5)); 10. } 11. //+------------------------------------------------------------------+ 12. int Sum(int arg1, int arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (int)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+ 19. double Sum(double arg1, int arg2) 20. { 21. Print(__FUNCTION__, "::", __LINE__); 22. 23. return (double)(arg1 + arg2); 24. } 25. //+------------------------------------------------------------------+
Код 08
Точно так же, как код 07 привел бы к появлению изображения 04, код 08 привел бы к появлению изображения 05. Прошу заметить, что разница между одним кодом и другим очень тонкая, и во многих ситуациях она может остаться незамеченной, и мы даже не поймем, что что-то пошло не так. Однако, в отличие от кода, который использует шаблоны, в кодах 07 и 08 мы быстро определим причину проблемы. Посмотрев на результаты, можно заметить, что здесь присутствует явное приведение типов, которое заставляет код выдавать ошибочный ответ, тем самым исправляя проблему.
Однако при использовании шаблона это не так легко заметить. В этом случае вам будет тяжело и, возможно, вы даже откажетесь от продолжения этого пути. И только если вы будете достаточно упрямы, то в конце концов поймете, где ошибка. Однако прежде, чем вы подумаете: «Итак, всё, что нам нужно сделать, - это использовать явное приведение типа к типу double в строке 16 из кода 05 или кода 06. Это, безусловно, решило бы нашу проблему с ответом, полученным в терминале».
Давайте посмотрим, почему это на самом деле не решит проблему. В некоторых случаях это даже создаст и другие проблемы. И причина этого довольно проста. Проблема в том, что вы будете пытаться решить результат в строке 09 кода, забывая о том, что есть строка 08, где мы используем данные типа integer. Тогда, вместо того чтобы исправить проблему, вы можете еще больше запутать ситуацию, создав тем самым новую проблему в другом месте.
Это, безусловно, типичная ситуация, которая в итоге становится довольно неловкой. Поэтому редко можно встретить практический код, использующий шаблон с несколькими типами, по крайней мере в MQL5. Что касается языка C, в основном C++, то такие вещи очень распространены и происходят постоянно.
Поэтому то, что здесь представлено, скорее интересно, чем полезно для повседневного использования. Однако, когда возникнет необходимость в его применении, вы быстро вспомните, какие потенциальные проблемы он может создать. Так что не стесняйтесь придумывать решение для решения подобного конфликта. Насколько я знаю, нет простого способа решить данную проблему. Даже в C++, где такое случается, часто приходится выкручиваться, чтобы обойти проблему. И поверьте, это совсем не весело.
Заключительные идеи
В данной статье мы рассказали, как справиться с одной из самых раздражающих и сложных ситуаций в программировании, с которой можно столкнуться: использование разных типов в одной и той же функции или шаблоне процедуры. Хотя большую часть времени мы уделяли только функциям, всё, что мы здесь рассмотрели, полезно и может быть применено к процедурам, как при использовании передачи по значению, так и в тех случаях, когда мы используем передачу по ссылке. Однако, чтобы не делать материал утомительным, мы не покажем эти случаи в действии.
Поэтому давайте договоримся о том, что вы должны практиковаться и пытаться формулировать небольшие фрагменты кода для изучения тех случаев, о которых мы говорили некоторое время назад, таких как работа с пошаговыми ссылками и использование шаблонов процедур, потому что на этом раннем этапе мы не обязательно будем работать с этим.
В приложении вы найдете многие из представленных сегодня кодов. Коды, которые не будут доступны, являются простыми модификациями кодов, представленных в приложении. В любом случае, отработка того, что показано здесь, поможет лучше закрепить содержание. В следующей статье мы подробнее поговорим о шаблонах. Поэтому, до скорой встречи!
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/15668
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





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