Если Вам знаком термин "рефлексия" и Вы пишите программы, которые используют этот механизм - то, возможно, Вас заинтересует как обстоят дела с этим "явлением" в других языках программирования. Если же этот термин Вам ни о чем не говорит, то ниже Вы найдете краткое пояснение, что же такое рефлексия (или как её еще называют - отражение) и несколько простых примеров с рефлексией на языках C++, Java и Python.

Несмотря на то, что рефлексия - это мощный и эффективный механизм, он в то же время достаточно сложный. Рефлексия интересует в основном разработчиков инструментальных средств, тогда как программисты, пишущие прикладные программы, обычно ею не пользуются.

И всё-таки иметь представление о том, что такое рефлексия должен каждый программист, так как многие задачи без неё просто не решаются или решение оказывается менее эффективным.

Что же такое рефлексия? Вот определение, которое дает Википедия:

"Термин отражение или рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения...".

Это определение не совсем точное, оно справедливо для языков Python и Java. Но в С++ кроме времени выполнения, у программы есть еще один важный этап - компиляция. И, несмотря на то, что в С++ рефлексия времени выполнения фактически отсутствует, рефлексия времени компиляции (или как её ещё называют - статическая рефлексия) очень скоро будет иметь широкую поддержку (стандарт С++17).

В Java имеется специальная библиотека рефлексии (java.lang.reflect), которая предоставляет большой набор инструментальных средств для манипуляции кодом в динамическом режиме (во время выполнения).

В языке Python всё является объектами: переменные, функции, классы, экземпляры классов, модули. Внутреннее устройство любого объекта всегда доступно во время выполнения программы. Интерпретатор никогда не скрывает внутреннее устройство объектов, поэтому рефлекия в этом языке наиболее естественный процесс.

Поддержка рефлексии есть во многих других языках программирования, таких как: С#, Objective-C, JavaScript - и всегда рефлексия позволяет программе анализировать собственную структуру. Чаще анализ производится на этапе выполнения - это свойственно тем языкам, в которых код исполняется виртуальной машиной или интерпретатором. Но анализ может проводиться и в момент компиляции, как в случае с С++.

Рассмотрим на примере возможности рефлексии в разных языках программирования. В качестве типовой задачи для рефлексии попытаемся: по имени класса создать экземпляр данного класса, "найти" у него определенный метод (также по имени) и вызвать его.. При этом нужно будет определить количество аргуметов этого метода и их тип, чтобы передать правильные значения.

Исходные данные: имя класса "Foo", имя метода "bar". Обязательным условием является то, что данный класс присутствует в нашей программе, другими словами, можно создать экземпляр класса Foo.

* - Если Вы не знаете Python, Java или C++, то всё равно посмотрите примеры для этих языков, так как они примитивны и полностью понятны!

Начнем с Python.

Python и рефлексия

Допустим класс Foo определен в текущем модуле, хотя это совершенно не обязательно. Мы смогли бы отыскать Foo и в любом другом модуле. Но для этого потребовалось бы написать чуть больше кода. Поэтому немного упростим задачу и "поместим" Foo в тот модуль, где и будем его искать. В Python нет перегрузки функций (методов), поэтому метод bar, будет только один (в Java нам придется учитывать еще и перегрузку). 

Эта коротка программа выведет примерно следующее:

{ ..., 'Foo': <class __main__.Foo at 0x1005e2738>, ... }

Это словарь атрибутов текущего модуля. В Python мы можем получить словарь атрибутов любого загруженного модуля (а не только текущего), используя переменную sys.modules.

Как видим в словаре присутствует ключ Foo с объектом class Foo в качестве значения (в Python даже классы являются объектами, и их можно, например, передавать в функции или хранить в списке). Можно обратиться по ключу и вызвать конструктор класса Foo.

Обратите внимание, мы работаем только с именем класса, вместо того чтобы явно написать: 

Ведь в этом весь смысл рефлексии - нам нужно проанализировать текущий модуль, найти класс по его имени и создать экземпляр этого класса. Всё это уже сделано. Теперь нужно найти и изучить метод bar. 

Чтобы обратиться к методу bar, мы можем воспользоваться встроенным атрибутом __dict__ или встроенной функцией getattr, которая позволяет получать доступ к атрибутам по строкам с их именами.

Доступ к методу получен, изучим метод bar. Функции являются обычными объектами, поэтому мы можем оперировать ими с помощью привычных инструментов, можем получить базовый доступ к атрибутам функции. Механизмы рефлексии позволяют нам также исследовать детали реализации – каждая функция, например, имеет присоединенный к ней объект с программным кодом, из которого можно получить такие сведения, как список локальных переменных и аргументов: 

Теперь нам доступна вся необходимая информация и можно осуществить вызов метода bar экземпляра класса Foo, передав точное количество аргументов. Тип аргументов в Python не так важен.

Задача решена. Как видете в Python механизм рефлексии является довольно простым и естественным инструментом. Мы можем легко исследовать состав любого модуля или класса, анализировать функции, узнавать об их аргументах и еще много всего. Интерпретатор не скрывает от нас ничего - во время исполнения программы доступны любые знания о её структуре. Это и есть рефлексия - программа обрабатывает собственный код. Такие программы называют рефлективными.

На очереди Java и её библиотека java.lang.reflect.

Java и рефлексия

Задача та же самая. Имеем имя класса и имя метода класса - "Foo" и "bar". Нужно сделать так:

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

Задача также усложняется еще и тем, что в языке Java разрешены перегрузки, поэтому методов с именем bar в классе может быть несколько. Нам нужно найти тот, который принимает 3 аргумента с типом char. Как будет видно дальше - рефлексия в Java позволяет провести даже самый сложный анализ. Итак, у нас есть класс Foo:

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

Получить доступ к этой информации можно, используя специальный класс, который так и называется: Class (возможно не лучшее название, но все привыкли). Вызывая статический метод forName(), можно получить объект типа Class соответствующий имени класса в строковом представлении:

Это еще не экземпляр класса Foo, это скорее источник метаинформации о классе Foo. А вот создать экземпляр класса Foo поможет метод newInstance():

Теперь нужно проанализировать функциональные возможности класса Foo с помощью рефлексии. Ниже приводится краткий обзор наиболее важных характеристик механизма рефлексии, позволяющих анализировать структуру класса.

Далее я просто приведу абзац из книги Хорстманна:

Три класса, Field, Method и Constructor, из пакета java.lang.reflect описывают соответственно поля, методы и конструкторы класса. У каждого класса есть свой набор методов: в состав Field входит метод getType(), который описывает тип поля; у классов Method и Constructor имеются методы, определяющие типы параметров, а класс Method позволяет также определить тип возвращаемого значения. На самом деле, в этих классах имеется еще много различных методов, которые позволяют получить такие данные как, например, модификатор доступа (public, private, protected).

Вернемся к классу Class. Как было сказано - это источник информации о классе. В классе Class есть методы getMethods(), getFields() и getConstructors(), возвращающие массивы открытых полей, методов и конструкторов, принадлежащих анализируемому классу. Вот как мы получим метод bar по его имени:

Всё хорошо, но есть одна проблема. Метод bar перегружен, и в блок if мы попадем два раза. А нам нужна конкретная версия с тремя аргументами типа char. Поэтому нужно "усилить" условие и применить дополнительный анализ параметров метода. Когда метод будет выбран точно, мы сможем вызвать его с помощью метода invoke класса Method.

Задача решена. Java обладает сильной библиотекой рефлексии, и её возможности очень обширны. Однако, если Вы в состоянии обойтись без применения рефлексии, то не стоит к ней прибегать. Программа, использующая механизм рефлексии работает медленнее, чем программа, непосредственно вызывающая эти методы.

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

На очереди последний язык в этом обзоре - С++. И с ним всё очень неоднозначно.

С++ и рефлексия

Если Вы не знакомы с С++, то многое в этом разделе будет Вам непонятно. Вы можете запомнить главное - в C++ с рефлексией времени выполнения всё очень плохо. Поддержка крайне слабая, фактически отсутствует. Всё что есть - это класс type_info, который содержит некоторые сведения о типе. Эти сведения доступны во время выполнения программы, но, например, создавать экземпляры классов type_info не умеет. Зато, с его помощью, можно получить символьную строку с именем типа (правда толку от неё почти нет, см. ниже почему).

Создать объект класса type_info напрямую нельзя. Единственный способ построения объекта type_info - использовать оператор typeid. Также невозможно копировать объекты класса type_info.

Стандарт С++11 дополнил класс type_info методом hash_code. Этот метод возвращает некоторое значение, которое будет идентично для объектов type_info, ссылающихся на один и того же тип. Других гарантий нет. Значение, которое возвращает hash_code, при следующем запуске программы, будет уже другим. 

В С++11 также добавили класс-обёртку type_index над type_info, который можно использовать, например, как индекс в ассоциативном контейнере. 

Но всего этого недостаточно, чтобы решить нашу задачу.

С type_info связано несколько печально известных проблем. Первая - функция-член name() (имя типа) класса type_info возвращает нестандартизованную символьную строку, а значит на неё нельзя полагаться, её даже нельзя достоверно знать. Всё будет зависеть от конкретной реализации. Например, на другой платформе, для одного и того же класса, это значение может уже быть другим. Отсюда следует, что имя класса "Foo" из нашего задания просто бессмысленно. 

* - я проверил, что возвращает метод name() с разными компиляторами. И всегда значение было одно и тоже. Однако, в исходных кодах gcc явно указан комментарий к методу name():

Returns an implementation-defined byte string; this is not portable between compilers!

Кроме того, строка, которую возвращает метод name(), не отмечена как constexpr. А это означает, что её нельзя использовать для вычислений на этапе компиляции (не подходит для метапрограммирования).

Еще одна проблема заключается в том, что type_name "ничего не знает" о typedef. Объявления typedef можно использовать для создания более коротких и значимых имен для типов - но класс type_name не способен возвращать соответствующие символьные имена.

Если бы только мы могли получить стандартизованное, портируемое, человекочитаемое символьное имя типа... но, увы. Такую возможность обещают в С++17.

И хотя, рефлексия времени выполнения в С++ уступает аналогичному механизму в Java и Python, всё не так плохо. В С++ рефлексия существует на стадии компиляции - это статическая рефлексия.  

Начиная С++11 расширенная версия type_traits, обеспечила широкий доступ к свойствам типа на этапе компиляции. Мы можем исследовать какой-либо тип, проанализировать его атрибуты, и в зависимости от этого "попросить" компилятор создать тот или иной код. Этот процесс по смыслу похож на то, что мы видели в Java или Python, но происходит он в другое время - не во время работы программы, а во время работы компилятора.

Спросите C++ программиста и Java программиста о том, что такое "метапрограммирование", и обратите внимание, что они будут рассказывать про разные этапы. Для программиста С++ метапрограммирование - это этап компиляции (compile-time). И рефлексия в этом языке имеет место быть именно на этой стадии.

Когда компилятор С++ обрабатывает единицу трансляции, у него есть доступ к большому количеству полезных метаданных. Но, к сожалению, программисту эти данные практически не доступны. Сейчас комитет по стандартизации С++ рассматривает предложения N4111, которое призывает расширить возможности рефлексии на этапе компиляции уже в следующем обновлении языка (С++17). А до той поры с решением нашей задачи на С++ лучше повременить.

Итог

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


Список литературы (в неформальном виде)

  1. Николай Джосаттис "Стандартная библиотека С++". 2-е издание.
  2. Бьерн Страуструп "Язык С++. Специальное издание".
  3. Кей Хорстманн, Гари Корнелл "Java. Библиотека профессионала. Том 1 и 2". 9-е издание.
  4. Марк Лутц "Изучаем Python". 4-е издание.
  5. Matu ́ˇs Chochl ́ık "A case for strong static reflection" N4452 и Static reflection (rev. 2) N4111

​​

Любые комментарии и замечания приветствуются. Надеюсь, статья была Вам интересна.