Вложенные типы, пространства имен и оператор контекста '::'

Классы, структуры и объединения могут быть описаны не только в глобальном контексте, но и внутри другого класса или структуры. И даже более того: определение можно сделать внутри функции. Это позволяет описывать все сущности, необходимые для работы какого-либо класса или структуры, внутри соответствующего контекста и тем самым избежать потенциального конфликта имен.

В частности, в программе рисования структура для хранения координат Pair была до сих пор определена глобально. Когда программа разрастется, вполне возможна ситуация, что появится необходимость в другой сущности с именем Pair (особенно учитывая довольно общее название). Поэтому описание структуры желательно перенести внутрь класса Shape (Shapes6.mq5).

class Shape
{
public:
   struct Pair
   {
      int xy;
      Pair(int aint b): x(a), y(b) { }
   };
   ...
};

Вложенные описания получают права доступа согласно указанным модификаторам секций. В данном случае мы сделали имя Pair публично доступным. Внутри класса Shape обращением с типом структуры Pair никак не меняется из-за переноса. Однако во внешнем коде необходимо указывать полностью квалифицированное имя, включающее имя внешнего класса (контекста), оператор выбора контекста '::' и непосредственно идентификатор внутренней сущности. Например, чтобы описать переменную с парой координат потребуется написать:

Shape::Pair coordinates(00);

Уровень вложенности при описании сущностей не ограничен, поэтому полностью квалифицированное имя может содержать идентификаторы множества уровней (контекстов), разделенные '::'. Например, мы могли бы все классы рисования заключить внутри внешнего класса Drawing, в секции public.

class Drawing
{
public:
   class Shape
   {
   public:
      struct Pair
      {
         ...
      };
   };
   class Rectangle : public Shape
   {
      ...
   };
   ...
};

Тогда полные имена типов (например, для использования в OnStart или других внешних функциях) удлинились бы:

Drawing::Shape::Rect coordinates(00);
Drawing::Rectangle rect(2001007050clrBlue);

С одной стороны это неудобно, но с другой является порой необходимостью в больших проектах с большим числом классов. В нашем маленьком проекте данный подход используется только для демонстрации технической возможности.

Для объединения логически связанных классов и структур в именованные группы MQL5 предоставляет более простой способ, чем включение их в "пустой" класс-обертку.

Пространство имен объявляется с помощью ключевого слова namespace, после которого идет имя и блок фигурных скобок, включающий все необходимые определения. Вот как выглядит та же программа рисования с использованием namespace:

namespace Drawing
{
   class Shape
   {
   public:
      struct Pair
      {
         ...
      };
   };
   class Rectangle : public Shape
   {
      ...
   };
   ...
}

Основных отличий два: внутреннее содержимое пространства всегда доступно публично (модификаторы доступа в нем не применимы) и после закрывающей фигурной скобки нет точки с запятой.

Добавим в класс Shape метод move, принимающий структуру Pair в качестве параметра:

class Shape
{
public:
   ...
   Shape *move(const Pair &pair)
   {
      coordinates.x += pair.x;
      coordinates.y += pair.y;
      return &this;
   }
};

Тогда в функции OnStart можно организовать сдвиг всех фигур на заданную величину с помощью вызова данной функции:

void OnStart()
{
   // рисуем случайный набор фигур
   for(int i = 0i < 10; ++i)
   {
      Drawing::Shape *shape = addRandomShape();
      // сдвигаем все фигуры
      shape.move(Drawing::Shape::Pair(100100));
      shape.draw();
      delete shape;
   }
}

Обратите внимание, что типы Shape и Pair приходится описывать полными именами: Drawing::Shape и Drawing::Shape::Pair соответственно.

Блоков с одним и тем же названием пространства может встретиться несколько: всё их содержимое попадет в один логически единый контекст с указанным именем.

Идентификаторы, определенные в глобальном контексте, в частности все встроенные функции MQL5 API, также доступны через оператор выбора контекста, перед которым ничего не ставится. Например, вот как может выглядеть вызов функции Print:

::Print("Done!");

Когда вызов делается из какой-либо функции, определенной в глобальном контексте, необходимости в такой записи нет.

Необходимость может проявиться внутри какого-либо класса или структуры, если в них определен одноименный элемент (функция, переменная или константа). Например, добавим метод Print в класс Shape:

   static void Print(string x)
   {
      // пусто
      // (вероятно, собираемся потом выводить в отдельный лог-файл)
   }

Поскольку тестовые реализации метода draw в производных классах вызывают Print, то теперь они перенаправлены на данный метод Print: из нескольких одинаковых идентификаторов компилятор выбирает тот, что определен в более приближенном контексте. В данном случае, определение в базовом классе находится ближе к фигурам, чем глобальный контекст. В результате вывод в журнал из классов фигур будет подавлен.

При этом вызов Print из функции OnStart по-прежнему работает (потому что он вне контекста класса Shape).

void OnStart()
{
   ...
   Print("Done!");
}

Чтобы "починить" отладочную печать в классах, нужно все вызовы Print предварить оператором выбора глобального контекста:

class Rectangle : public Shape
{
   ...
   void draw() override
   {
      ::Print("Drawing rectangle"); // вновь печатаем через глобальный Print(...)
   }
};