Непрактичное использование рефлексии
Рефлексия является мощным API, что почти искушает вас использовать ее либерально. В разделе 2-4 показано эффективное использование рефлексии для решения некоторых сложных проблем, но мы чувствуем, что также необходимо показать некоторые из отрицательных сторон рефлексии. Знание потенциальных проблем рефлексии поможет вам избежать распространенных проблем с этим API. После прочтения этого раздела, вы поймете, почему рефлексию не следует использовать слишком часто и много в вашем приложении.
Проблемы с производительностью при использовании рефлексии
Первая проблема, с производительностью, имеет отношение к работе, которую должен выполнить Reflection API. Например, вызов метода, как Next()
для объекта Random
напрямую:
var value = new Random().Next();
Компилятор будет знать маркеры для ссылки типа Random
и ссылки метода Next()
, и этот прямой путь самый быстрый. Конечно, изрядное количество информации идет в определение .NET объекта во время выполнения, но получить случайное значение является довольно простым процессом. Но если бы все, что вы знаете, заключалось в том, что есть тип, который называется System.Random
, и метод Next
благодаря их именам, вы должны были бы использовать рефлексию, чтобы найти тех членов и в конечном итоге вызвать метод, что занимает много времени. Сколько времени? Следующий листинг является упрощенным стресс тестом вызова Next()
для нового объекта Random
500000 раз.
Листинг 2-3: Стресс тестирование прямого вызова метода
var stopwatch = Stopwatch.StartNew();
for(var x = 0; x < 500000; x++)
{
var random = new Random().Next();
}
stopwatch.Stop();
Console.Out.WriteLine(stopwatch.Elapsed.ToString());
Это не то, что бы вы хотели сделать для получения полумиллиона новых случайных чисел; вы бы использовали тот же объект Random
для каждого вызова. Но дело заключается в том, чтобы сравнить создание объекта и вызов метода, когда используется рефлексия. Следующий листинг такой же, как и листинг 2-3, за исключением использования рефлексии.
Листинг 2-4: Стресс тестирование вызова метода через рефлексию
var stopwatch = Stopwatch.StartNew();
for(var x = 0; x < 500000; x++)
{
var randomType = Type.GetType("System.Random");
var nextMethod = randomType.GetMethod("Next", Type.EmptyTypes);
var random = nextMethod.Invoke(Activator.CreateInstance(randomType), null);
}
stopwatch.Stop();
Console.Out.WriteLine(stopwatch.Elapsed.ToString());
В среднем, общее время для прямого подхода составляет около 2 секунд, в то время как подход с рефлексией занял 7 секунд. Как и в любом тесте производительности, число может измениться, и в зависимости от того, что делает все приложение, некоторые заботы с рефлексией могут быть приемлемы по сравнению с наличием некоторой гибкости. Вы также можете повысить производительность путем перемещения вызова Type.GetType()
за пределы цикла for
. Для нас это сократило общее время для подхода с рефлексией до 3.5 секунд. Но в целом, вы всегда будете сталкиваться с медленным временем выполнения при использовании подхода с рефлексией.
Хрупкость и рефлексия
Другой проблемой является хрупкость. Если вы пишете такой код:
var value = new Randon().Next();
компилятор будет информировать вас о неверном имени типа (мы предполагаем, что у вас нет типа "Randon"). Ошибка перехвачена сразу при компиляции кода. Или, если этот код работал раньше, но новая версия Random
меняет Next()
на NextValuе()
, вы это узнаете достаточно скоро. Но если вы пишете код
var value = Type.GetType("System.Randon");
вы не будете иметь ни малейшего представления, что у вас есть ошибка в коде при запуске компилятора. Как мы подчеркнули ранее, иметь хорошие модульные тесты для любого кода имеет решающее значение, и это особенно верно, в частности, для динамического, основанного на рефлексии кода. Вы должны убедиться, что строки, которые вы передаете в вызовы, как GetMethod()
и GetType()
, являются правильными и относятся к нужным членам.
Кроме того, вы видели ранее, что рефлексия дает вам доступ к не открытым элементам. Использование этих элементов в коде настоятельно не рекомендуется, потому что нет никакой гарантии, что имена останутся теми же или что члены не исчезнут полностью при переходе от версии к версии. Например, в версии .NET 4.0. класс Thread
содержит закрытое поле DONT_USE_InternalThread
типа IntPtr
. Это имя само по себе должно быть предупреждением для вас не связываться с ним вообще, но, скажем, вы это сделали по какой-то странной причине. Что произойдет в будущей версии .NET? Лучше иметь много тестов вокруг кода, чтобы убедиться, что ничего не сломается, потому что простое изменение имени сможет в будущем принести много проблем.
Использование не открытых членов
Есть всегда исключения из правил, и это относится и к вопросу использования к не открытых членов. В одной статье (Jason Bock (blog), “Adding Session State to a Mock HttpContext Object,” September 2005, http://mng.bz/V9r3) рассказывается, как использовать mock-технологию для объекта
HttpContext
при помощи рефлексии. В другой статье (Oleg Sych (posted by), “Simplifying WCF: Using Exceptions as Faults, July 2008,” http://mng.bz/GC7R) показано, как можно вернуть информацию об исключении в WCF. Третья статья (Jason Bock (blog), “Being Evil with a DynamicMethod, Class Internals, and Unsafe Code,” July 2008, http://mng.bz/2i0t) иллюстрирует, как вы можете изменить строку благодаря манипуляциям с внутренними членами. В некоторых случаях (например, сHttpContext
) изменение скрытой информации является единственным способом для достижения желаемого решения. В других случаях (например, реверсия строки), это чисто академический (и очень опасный!) способ. Если вы думаете, у вас нет другого выбора, кроме как использовать эти закрытые члены, убедитесь, что вы понимаете потенциальную опасности и охраняете ваш код соответствующим образом (с сильными модульными тестами, безопасными резервными путями в коде, когда члены не могут быть найдены и прочее).