Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В предыдущей статье я начал свой рассказ динамические методы.
Мы узнали, как создать простейший динамический метод, в общих чертах обсудили, как работает стековая машина и рассмотрели некоторые простейшие операции Common Intermediate Language, которые можно использовать при генерации методов в runtime, такие как работу с константами, математические и битовые операции, а также работу с аргументами методов и локальными переменными.
В этой статье я хочу рассмотреть всё, что так или иначе связано с вызовами методов. Мы рассмотрим как из динамического метода вызывать статические и экземплярные методы, а также разберёмся, чем отличается виртуальные и невиртуальные вызовы экземплярных методов.
Кроме этого поговорим про динамические методы, привязанные к объектам, и рассмотрим возможность вызова одних динамических методов из других.
Операции вызова методов
Скорее всего, при создании динамического метода, вам потребуется вызывать из него другие методы. В Common Intermediate Language (далее просто IL) для вызова методов имеется три операции:
call - используется для прямого вызова метода по его дескриптору; может использоваться для вызова как статических методов, так и методов экземпляра;
calli - (если я ничего не путаю) используется обычно для вызова unmanaged кода и не будет рассматриваться в данной статье;
callvirt - используется для вызова метода через таблицу виртуальных методов.
Рассмотрим операции call
и callvirt
более подробно.
Вызов статических методов
С вызовом статических методов всё достаточно просто. Предположим, что у нас есть статический класс Greeter
со статическим методом SayHello
, который выводит приветствие в зависимости от двух параметров: имени и языка:
Если мы хотим вызвать этот статический метод из нашего динамического метода, мы должны последовательно поместить на стек значения обоих параметров, после чего вызвать операцию call
, передав ей в качестве аргумента необходимый метод.
Предположим, мы хотим написать функцию, которая будет создавать динамический метод с "зашитым" в неё константным языком, но с возможностью передавать имя в качестве параметра:
Теперь мы можем создать два метода для приветствия на русском и английском языках и вызвать их:
В результате получим:
Вызов методов экземпляров
С вызовом методов экземпляров всё чуточку интереснее, т.к. для этого можно использовать как операцию call
, так и операцию callvirt
, и результат будет зависеть от этого выбора.
Предположим, что у нас есть два класса: класс GreeterA
с виртуальным методом SayHello
и класс GreeterB
, переопределяющий этот виртуальный метод.
Если мы хотим вызвать метод экземпляра из нашего динамического метода, мы должны сначала поместить на стек сам экземпляр, на котором хотим вызвать метод, а затем последовательно поместить на стек значения всех его параметров (в этом примере, для простоты, параметров нет), после чего вызвать операцию call
или callvirt
передав ей в качестве аргумента необходимый метод.
Предположим, мы хотим написать функцию, которая будет создавать динамический метод для вызова метода SayHello
:
Обратите внимание, что тут мы всегда ищем метод SayHello
именно в базовом классе GreeterA
, чтобы иметь возможность вызывать его как на экземпляре самого класса GreeterA
, так и на экземпляре производного класса GreeterB
.
Теперь, если мы создадим два динамических метода и вызовем их с экземплярами классов GreeterA
и GreeterB
:
Мы получим следующий результат:
При вызове метода через операцию call
происходит невиртуальный вызов метода (т.е. вызов по указателю на метод). При этом всегда вызывается метод того класса, в котором мы этот метод искали (через GetMethod
). Напомню, что мы искали метод SayHello
именно в базовом классе, поэтому результат не зависит от реального типа экземпляра.
При вызове же метода через операцию callvirt
происходит виртуальный вызов метода, который использует таблицу виртуальных методов (т.е. указатель на метод сначала ищется в таблице, которая уникальна для каждого класса, а только потом происходит вызов). При этом вызываться будет метод из того класса, экземпляр которого используется, поэтому для каждого экземпляра был вызван метод из его класса.
Фактически невиртуальный вызов ничем не отличается от вызова статического метода, где первым параметром просто передаётся экземпляр, на котором этот метод вызывается. И отсюда есть одна особенность: с помощью операции call
можно вызвать метод экземпляра на пустой ссылке, т.е. this
в методе будет равен null
!
Например, если попробовать сделать следующее:
Мы получим следующий результат:
Как видно, первый метод сработал, т.к. он не использует ссылку this
и ничто не мешает ему выполняться.
Второй же метод выбросил исключение, т.к. для поиска вызываемого метода нужна таблица виртуальных методов, ссылка на которую находится в экземпляре класса, которого в данном случае нет.
Динамические методы, привязанные к объекту
Все примеры динамических методов, которые я приводил выше в этой и предыдущей статьях были, можно сказать, статическими. Они могли принимать параметры, содержали некоторую логику, однако не имели никакого состояния.
Но это состояние можно легко добавить, указав при создании динамического метода целевой объект, к которому этот метод будет привязан. Это сделает его похожим на экземплярный метод, а целевой объект станет его неявным первым параметром в котором можно сохранять состояние. При этом целевым объектом может быть экземпляр абсолютно любого класса.
Предположим, мы хотим создать динамический метод, который для каждого строкового ключа будет при каждом вызове возвращать целочисленное значение, увеличенное на единицу.
На C# подобный код мог бы выглядеть следующим образом:
Вот как этот же код можно реализовать в виде динамического метода:
Кода получилось уже значительно больше, дам некоторые пояснения:
При создании экземпляра
DynamicMethod
мы указываем два параметра, где в первом параметре передаём тип целевого объекта (или попросту типthis
).При создании делегата в самом конце мы напротив не указываем тип целевого объекта, а передаём сам объект (
getNextValueTarget
) в методCreateDelegate
в качестве параметра.При вызове метода
TryGetValue
последний параметр имеет модификаторout
, поэтому мы передаём в него не значение, а ссылку на значение. Получить её можно через локальную переменную операциейldloca
.После вызова метода
TryGetValue
на стек помещается результат выполнения этого метода. Поскольку он нам не нужен, мы должны удалить его из стека операциейpop
, в конце концов стек остался пустым.Другой полезной операцией может быть операция
dup
, которая напротив дублирует последнее значение, находящееся на стеке.Для того, чтобы сохранить значение обратно в словарь мы должны обратиться к индексатору. Но индексаторы в .NET - это просто свойства. А свойства в .NET - это просто пары методов с префиксами
get_
иset_
, поэтому вызвать индексатор можно как обычный метод.
В результате выполнения такого кода мы получим следующее:
Вызов одного динамического метода из другого
Ещё одной полезной особенностью динамических методов является то, что одни динамические методы можно вызывать из других. Для этого достаточно использовать операцию call
, передав ей в качестве аргумента ссылку на динамический метод (не на делегат).
Данный трюк может быть полезен, например, когда вы генерируете мапперы или фабрики для сложных объектов с несколькими уровнями вложенности. При этом вложенные объекты могут создаваться отдельным динамическим методом, который можно использовать повторно.
Рассмотрим пример. Предположим, что мы хотим создать динамический метод, который будет логировать (выводить на консоль) объекты некоторого класса, используя указанную строку формата и текущую дату.
При этом сами объекты мы хотим логировать не банальным ToString
, а выводить конкатенацию всех свойств с суффиксом Name
(далее, назовём это полным именем объекта). При этом динамический метод получения полного имени объекта мы хотели бы потом использовать ещё где-нибудь.
Например, если у нас есть вот такой класс, содержащий два свойства для хранения имени и фамилии:
То метод получения полного имени объекта на C# мог бы выглядеть следующим образом:
(Я специально не использую StringBuilder, т.к. в динамическом методе мы будем генерировать точно такой же код.)
Напишем метод, который для указанного класса будет возвращать нам динамический метод для получения полного имени объекта этого класса:
Мы могли бы использовать этот метод, чтобы создать делегат и вызвать его следующим образом:
Но вместо этого используем его в другим динамическом методе, который на C# будет отдалённо напоминать следующий код:
Всего одна строчка! Но в виде динамического метода кода будет сильно больше:
Теперь мы можем сначала создать общий динамический метод для получения полного имени объекта, а затем два разных метода логирования, которые будут его использовать:
Данный код выведет следующее:
Заключение
В данной статье мы поговорили про вызовы методов в динамических методах: рассмотрели вызов статических и экземплярных методов, обсудили привязку динамических методов к экземпляру объекта для хранения состояния, а также посмотрели на возможность вызова одних динамических методов из других.
В следующей (и скорее всего заключительной статье) я планирую рассмотреть оставшиеся, но важные операции IL, которыми частенько приходится пользоваться: создание объектов и массивов, условные переходы и циклы.
Немного саморекламы
В качестве пет‑проекта я делаю telegram‑канал, куда из разных мест собираются интересные статьи, связанные с.NET тематикой.
Канал ведётся автоматически скриптами на GitHub Actions. Там нет и никогда не будет рекламы. Буду рад если зайдёте: https://t.me/amazing_dotnet