В .NET достаточно развитые средства рефлексии, и как результат этим активно пользуются. При этом вариантов и способов использования достаточно много. Эти способы отличаются в зависимости от того что надо: по типу действий (создать объект, вызвать его метод, получить/изменить значение поля), по информации об объекте (мы заранее знаем сигнатуру или не знаем), по типу доступа (публичные/приватные методы), по наличию параметров.
Я как-то делал доклад по этим технологиям на YaNA, но с тех пор прошло много времени, обнился Framework, и в моей жизни количество кода с рефлексией значительно возросло, поэтому я решил обновить знания, провести тесты и поделиться результатами.
Сегодня я рассмотрю достаточно простой способ: вызов конкструктора без параметров и с известными параметрами. Это достаточно часто встречающаяся задача, например, она может использоваться для создания плагинов и модулей. При этом мы знаем, что объект имеет определённый тип (отнаследован от интерфейса, например), так что в дальнейшем мы можем вызывать методы напрямую. Но тем не менее, мы не можем напрямую создать объект, т.к. у нас есть только информация о типе (Type), но изначально мы просто не можем написать new MyObject() — данные придут позднее.
Соответственно есть несколько путей решений:
Я как-то делал доклад по этим технологиям на YaNA, но с тех пор прошло много времени, обнился Framework, и в моей жизни количество кода с рефлексией значительно возросло, поэтому я решил обновить знания, провести тесты и поделиться результатами.
Сегодня я рассмотрю достаточно простой способ: вызов конкструктора без параметров и с известными параметрами. Это достаточно часто встречающаяся задача, например, она может использоваться для создания плагинов и модулей. При этом мы знаем, что объект имеет определённый тип (отнаследован от интерфейса, например), так что в дальнейшем мы можем вызывать методы напрямую. Но тем не менее, мы не можем напрямую создать объект, т.к. у нас есть только информация о типе (Type), но изначально мы просто не можем написать new MyObject() — данные придут позднее.
Соответственно есть несколько путей решений:
Activator.CreateInstance
Самый простой и удобный метод создания объекта:
Activator.CreateInstance(myObjectType)
Также можно передать параметры для конструктора. Всё просто и очевидно.
ConstructorInfo.Invoke
"Классический рефлекшен".
var constructorInfo = myObjectType.GetConstructor(new Type[0]);
constructorInfo.Invoke(new object[0]);
Т.е. мы получаем объект ConstructorInfo и через рефлексию создаём объект. В данном примере нет параметров, но их легко можно передать массивом объектов
Compiled Expression
Ещё один сравнительно новый вариант создания объекта, который заключается в построении Expression'а и дальнейшей компиляции его в делегат (анонимную функцию). Способ создания без параметров выглядит так:
var d = Expression.Lambda<Func<object>>(Expression.New(constructorInfo)).Compile();
В результате у нас будет делегат, возвращающий object (можно специфицировать точнее), который мы можем вызывать как обычный метод.
Если конструктор имеет параметры и нам точно известны, какие они должны быть, мы можем использовать подобный код:
var p1 = Expression.Parameter(typeof(int));
var p2 = Expression.Parameter(typeof(string));
var newExpression = Expression.New(constructorInfo, p1, p2);
var d = Expression.Lambda<Func<int, string, object>>(newExpression, true, p1, p2).Compile();
В данном случае, мы точно знаем, что у нас есть конкструктор с двумя параметрами типа int и string, поэтому мы генерируем конкретный код, вызывающий конкструктор с нужными параметрами. В отличие от предыдущих методов, данный слегка читерский, ибо тут мы точно указываем параметры, а мы их можем и не знать, соответственно у этого метода должна быть фора.
Compiled Expression with Any Parameters
Чтобы избавиться от этой форы и рассмотреть случаи, когда мы заранее не можем знать параметры, надо сгенерировать что-то общее. Логично будет рассмотреть случай, когда у нас есть делегат, принимающий массив параметров типа object, при этом данный делегат внутри сам проводит преобразования для корректного вызова. Код будет выглядеть примерно так:
var argsExpr = Expression.Parameter(typeof(object[]));
var constructorParams = constructorInfo.GetParameters()
.Select((p, i) => Expression.Convert(Expression.ArrayAccess(argsExpr, Expression.Constant(i)), p.ParameterType));
var newExpr = Expression.New(constructorInfo, constructorParams);
var d = Expression.Lambda<Func<object[], object>>(newExpr, true, argsExpr).Compile();
var constructorParams = constructorInfo.GetParameters()
.Select((p, i) => Expression.Convert(Expression.ArrayAccess(argsExpr, Expression.Constant(i)), p.ParameterType));
var newExpr = Expression.New(constructorInfo, constructorParams);
var d = Expression.Lambda<Func<object[], object>>(newExpr, true, argsExpr).Compile();
Т.е. мы передаём массив, в котором находятся аргументы, потом из этого массива достаём параметры для конструктора, конвертируем к нужному типу и вызываем конструктор.
Dynamic Method
Самый сложный метод, который заключается в использовании DynamicMethod и ручной генерации MSIL'а. Поскольку решение в отличие от остальных не совсем тривиальное, детальное описание реализации оставлю для следующей статьи (будет ссылка, когда статья будет написана). Мы его также как и Expression можем сделать специфичным, или обобщённым. У обобщённого основная идея та же, что и в предыдущем методе: мы генерируем делегат, принимающий массив object'ов, внутри приводим аргументы к нужному типу и вызываем конструктор. Только делаем всё это нативно, генерируя MSIL и компилируя его.
Результаты производительности
Мерять тут можно много чего. Но я остановился на следующих условиях:
- Только публичные конкструкторы (приватные значительно дольше, а необходимость в их использовании весьма специфичная)
- Время подготовки нужного объекта не замеряется. Подразумевается, что если уж есть цель оптимизации, то этим временем можно пренебречь. Хотя оно может и вносить ощутимые задержки на первый вызов. Если для вас это будет существенно, наверное стоит провести замеры на ваших условиях и железе.
- Объекты очень простые но не совершенно пустые (сложность этих объектов может сильно повлиять на расстановку сил, нивелировав или максимально усилив различия между методами
- Проверяется Debug и Release режим, чтобы выяснить ускорение, получаемое кодом от оптимизации
- Конкретные цифры я не покажу, ибо это сильно зависит от железа. Только отношение
- Также для сравнения добавлен прямой вызов конструктора
Вызов конструктора без параметров
По графику можно сделать простой вывод: если скорость не очень критична, используйте Activator, а если объектов нужно создавать очень много, то Expression. И да, Release режим в некоторых случаях рулит. Рефлекшен через ConstructorInfo самый медленный. Если у вас есть желание, можете использовать DynamicMethod, он самый быстрый.
Вызов конструктора с параметрами
Я взял конструктор с двумя параметрами int и string, чтобы было заметно влияние боксинга. Результаты получились следующие:
В данном случае Activator показал дичайшие тормоза, что очень удивительно, относительно предыдущего варианта. Обобщённый экспрешшен в 2 раза медленнее специализированного (хоть на графике это плохо видно) и в 4-6 раз медленнее прямого вызова (для сравнения скорости). Т.е. вывод тут такой — используйте скомпилированные экспрешшены, по-возможности с явными типами, если нет, то с обобщёнными, потери не очень сильные. При этом обобщённый DynamicMethod быстрее обобщённого Expresssion'а, но медленнее типизированного. Т.е. если хочется максимальной скорости, придётся относительно много писать, чтобы сделать максимально типизированный и производительный DynamicMethod.