Andriy Shyrokoryadov

.Net developer, data scientist

Рефлексия в языке C# - вопрос №29 на собеседование C# / .NET

Текст к видео "Рефлексия в языке C#" на канале YouTube

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

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

  1. добавляем ссылку на библиотеку в наш текущий проект;
  2. в код добавляем отсылку к определенному пространству имён в библиотеке;
  3. создаем объект интересующего нас класса, используя ключевое слово new;

Всё хорошо, всё работает. Всё работает до тех пор, пока у нас есть доступ к файлу библиотеки с интересующим нас типом. А теперь давайте представим ситуацию – всё то же самое, только у вас нет доступа к файлу библиотеки, но у вас есть информация, что файл библиотеки будет называться «А», в библиотеке будет класс, который будет называться «Б» и в классе будет задекларирован метод «В». Вашей задачей является вызов метода «В». Но, самое главное, у вас нет этого файла DLL и, соответственно, у вас нет возможности оценить как интересующий нас тип написан. А ваш собственный код писать надо, и никто не будет ждать пока вы получите доступ к файлу этой библиотеки. Тут может пригодиться рефлексия. С её помощью вы можете загрузить в память файл DLL и получить максимум информации о коде, который содержится в этом файле DLL. То есть если дать общее определение рефлексии, то можно сказать, что рефлексия – это способность кода C# читать другой код C# и его использовать, то есть создавать типы, записанные в файлах DLL, и выполнять логику из сторонних библиотек.

То есть, по сути, в нашем коде будет следующая логика:

  • прочитай файл DLL с кодом, файл называется «А.dll»;
  • получи список всех задекларированных типов в данной библиотеке;
  • если среди типов в полученном списке есть тип «Б», то создай объект этого типа;
  • если в типе «Б» есть метод «В», запусти его.

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

  • получи всё типы из файла DLL;
  • получи всё методы / свойства / поля / конструкторы для интересующего нас класса;
  • создай объект интересующего нас класса используя информацию о доступных конструкторах;
  • запусти полученный метод в объекте, который мы создали;

Более-менее, так работает рефлексия – читает файлы DLL и все что было прочитано, чужой код, может быть использовано в существующем коде. Наш код прочитал сторонний код и на его основании создал новый код. Так это работает. Добро пожаловать в мир рефлексии! Кстати, чтение атрибутов, о которых мы говорили в начале, происходит именно так. При помощи рефлексии можно получить всё атрибуты для типа и его членов, найти атрибут, который нас интересует и прочитать его значение, а если атрибут не указан, то ничего страшного – выполняем логику по умолчанию.

Возникает вопрос – почему рефлексия не используется столь часто. По моему скромному мнению, есть 2 причины. Первая – это то, что код рефлексии получается громоздким и его не очень удобно читать. Вторая причина – рефлексия использует достаточно большое количество ресурсов компьютера и приложение, которое интенсивно использует рефлексию, будет малопроизводительным и относительно медленным.

Давайте подумаем, как мы можем загрузить файлы DLL в код и какие источники таких файлов у нас есть. Прежде всего стоит сказать, что в языке C# определен тип Assembly и он является отображением всего того, что может содержаться в файле DLL. То есть после считывания библиотеки в объект типа Assembly, используя данный объект мы можем «вытянуть» всё информацию на тему данной библиотеки. Тип Assembly имеет несколько статических методов для загрузки файлов:

  • Load
  • LoadFile
  • LoadFrom
  • LoadModule
  • LoadWithPartialName

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

Начнем рассмотрение этих методов по порядку.

Метод Load имеет 4 перегрузки. Я бы хотел акцентировать ваше внимание на перегрузке Load(string). Может показаться, что в качестве аргумента можно предать путь к файлу библиотеки, например “C:\MyLibraries\Calculator.dll”. Но это не так. В данном случае ожидается что будет передано в качестве аргумента, не путь к файлу, а длинной или короткое имя библиотеки. Обычно рекомендуется использование длинного имени библиотеки. Что это такое? Длинное имя – это строка, которая содержит кроме названия библиотеки, еще несколько элементов. Этими элементами являются версия библиотеки, тип установок культуры, публичный токен. Также к вышеуказанное информации может иногда добавляться архитектура процессора, для которой была создана данная библиотека. Примером длинного имени может быть следующая строка: “system, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”. После загрузки файла библиотеки в код, эту строку можно получить из объекта Assembly по свойству FullName.

Чтобы загрузить файл библиотеки, необходимо использовать метод LoadFile. Однако одна из перегрузок метода LoadFrom имеет такую же сигнатуру. В чём разница между этими 2 методами. Разница между этими методами заключается в использование разных контекстов загрузки.

Метод LoadFile использует контекст загрузки по умолчанию – это значит, что если загружаемая библиотека имеет зависимости, то есть зависит от других библиотек, то такие библиотеки-зависимости должны находиться

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

Метод LoadFrom создает свой собственный контекст загрузки, так называемый load-from контекст. При использовании данного метода библиотеки-зависимости могут быть подгружены автоматически даже если они не находят контексте загрузки по умолчанию.

В предыдущем объяснении появилось одно новое определение – это глобальный кэш сборок (GACglobal assembly cache). Можно сказать, что это хранилище файлов DLL - библиотек .Net. Основной особенностью этого хранилища является то, что в нём могут храниться несколько версий одной библиотеки одновременно. То есть если у Вас есть 2 приложения, которые используют библиотеку «Х», но её разные версии, то можно поместить обе эти версии в глобальный кэш сборок и оба приложения будут в состоянии подгружать библиотеку «Х» в соответствующей версии. У этого технического решения есть и недостатки – возможно приложение, которое использует библиотеку «Х», не будет работать на другом компьютере, если в кэше сборок на другом компьютере не будет добавлена библиотек «Х». К библиотеке, которую планируется разместить в кэше сборок, также предъявляются определенные требования – то есть не каждая DLL-ка может попасть в глобальный кэш сборок. Хоть кэш называется глобальным, то он, по сути, имеет локальный характер – у каждого компьютера свой уникальный кэш сборок, а слово «глобальный» скорей всего обозначает то, что к кэшу можно получить доступ с уровня любого приложения на данном компьютере если есть такая необходимость.

В данный момент мы уже имеем некоторое представление, как подгрузить динамически библиотеку в наше приложение. В результате такой подгрузки мы получаем объект типа Assembly. Посредством этого объекта мы получим всю информацию, которая содержится в данной сборке или библиотеке. Стоить обратить внимание на все методы в классе Assembly, которые начинаются на слово Get. В частности, в практическом примере далее, нас будет интересовать метод GetTypes. Вызов данного метода возвращает список всех типов, определенных в данной сборке. Имея список типов, можно выбрать интересующий нас тип и получить всё информацию о данном типе. Типы определены как объекты класса Type и данный класс также имеет ряд методов, которые начинаются со слова Get и позволяют получить определенную информацию об определенных частях определенного типа. Например, метод GetProperties класса Type позволяет получить список всех свойств типа Type.

Результатом выполнения метода Get… названия определенной части, например GetMethods, GetFields, GetConstructors возвращает массив объектов определенного типа. Например MethodInfo, FieldInfo, ConstructorInfo. Каждый из этих типов содержит очень детальную информацию о методах, полях, конструкторах соответственно. Используя, например тип PropertyInfo, можно получить все данные о конкретном свойстве, а также установить или получить значение этого свойства для конкретного объекта используя механизмы рефлексии. Тоже самое с классом MethodInfo – получаем всю информацию о методе и можем вызвать этот метод на определенном объекте используя рефлексию. Описывать все свойства и методы объектов MethodInfo, FieldInfo, ConstructorInfo, а также им подобных объектов, не имеет смысла – они хорошо описаны в документации. Я рекомендую вам самостоятельно попробовать написать несколько примеров и получить информацию о типах, которые определены в .Net. Просто важно знать, что такие классы существуют и они являются частью общепринятого механизма рефлексии. В практических примерах к данному видео я покажу как использовать эти классы и какую информацию из них можно получить.

Репозиторий с кодом практического примера