Главная страница   /   2.3. Непрактичное использование рефлексии (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

2.3. Непрактичное использование рефлексии

Рефлексия является мощным 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) изменение скрытой информации является единственным способом для достижения желаемого решения. В других случаях (например, реверсия строки), это чисто академический (и очень опасный!) способ. Если вы думаете, у вас нет другого выбора, кроме как использовать эти закрытые члены, убедитесь, что вы понимаете потенциальную опасности и охраняете ваш код соответствующим образом (с сильными модульными тестами, безопасными резервными путями в коде, когда члены не могут быть найдены и прочее).