В этой статье мы рассмотрим магию, которую нам позволяет делать рефлексия. Иногда бывают такие случаи, когда нужно
изменить какую-то внутреннюю переменную, которая скрыта от нас, и ее нельзя
изменить нормальным способом. Например, загрузить стороннюю библиотеку и выполнить
какую-то функцию из нее. Но давайте лучше рассмотрим на примере, как можно
использовать рефлексию, используя, по сути, хак для изменения логики поведения. Давайте
создадим простое консольное приложение ReflectionSample.
Затем в созданный
класс добавим новый класс A, который
реализуем следующим образом:
public class A
{
public A(int n)
{
_b.Add(new B { C = n });
}
public void
Print()
{
foreach (var value in _b)
{
Console.WriteLine(value.C);
}
}
private List<B> _b
= new List<B>();
private class B
{
public int C { get; set; }
}
}
Теперь
посмотрим, как этот класс используется на примере.
class Program
{
static void Main(string[]
args)
{
var a = new A(5);
a.Print();
}
}
В
результате если мы нажмем комбинацию клавиш в студии Ctrl+F5, то на экране будет выведен результат "5".
А что делать, чтобы после вызова метода Print() класса A у нас вывелось какое-то другое число. Или давайте усложним задачу и представим,
что мы не знаем реализацию класса A, но хотим подменить вывод метода Print(). Вероятность того, что мы не будем знать реализацию, очень большая,
поскольку свою реализацию мы можем легко изменить, а вот изменить реализацию
чужого метода бывает намного сложнее. В первую очередь, чтобы посмотреть на реализацию класса A, нам
нужно воспользоваться каким-то из существующих популярных
дизассемблеров, которые позволяют посмотреть не только IL-код написанной библиотеки, но зачастую и полный исходный
код этой библиотеки на языке C#. Я часто пользуюсь
бесплатным ILSpy, который в большинстве случаев
покрывает все мои нужды, либо платным аналогом .NET
Reflector, который, соответственно, более мощный, но и стоит определенных
денег. Для нашего примера вполне достаточно будет самого обычного ILSpy.
Как мы можем видеть
с рисунка выше, ILSpy показал нам полностью исходный код нашего класса A, на языке C#. Одним из
существенных недостатков языка C# является то, что он легко поддаётся взлому, и с целью предотвращения от
этого нужно пользоваться различными обфускаторами, чтобы каким-то образом скрыть
исходный код. Из увиденного кода мы видим, что метод Print с помощью конструкции foreach перебирает
элементы списка вложенного закрытого класса B. Поскольку
класс B у нас private, мы не можем
просто так взять и создать новый экземпляр этого класса. Также, если у нас вдруг получится создать этот класс, мы не можем сами
добавить его в
список _b класса А. Чтобы не
запутать вас еще больше,приведу ниже код, который я имел в виду.
private List<B> _b = new List<B>();
private class B
{
public int C { get; set; }
}
Как видите, просто так изменить свойство С класса B не выйдет. Но тут нам на помощь приходить рефлексия. Мы можем переписать наш код следующим образом:
class Program
{
static void Main(string[] args)
{
var a = new A(5);
IList items
= (IList)(typeof(A).GetField("_b", BindingFlags.NonPublic
| BindingFlags.Instance).GetValue(a));
var first
= items[0];
first.GetType().GetProperty("C", BindingFlags.Public
| BindingFlags.Instance).SetValue(first, 6);
a.Print();
}
}
Давайте
последовательно пройдемся по этому коду. Первая
строка создает экземпляр класса и устанавливает ему значение 5.
var a = new A(5);
Затем следующей
строкой мы говорим компилятору, что хотим получить поле _b, которое
принадлежит этому классу (instance type) и которое, к тому же, является не публичным. После этого приводим это
все к типу IList. Мы это можем
сделать по той причине, что класс List<T> наследуется
от интерфейса IList.
IList items = (IList)(typeof(A).GetField("_b", BindingFlags.NonPublic
| BindingFlags.Instance).GetValue(a));
После этого мы
получаем первый элемент коллекции items.
var first = items[0];
Теперь же нам
осталось найти открытое (public) свойство C, для которого
мы в той же строке проставим сразу новое значение.
first.GetType().GetProperty("C", BindingFlags.Public
| BindingFlags.Instance).SetValue(first, 6);
Если мы добавим
это все в Watch (Window), то сможем сами
убедиться в том, что это работает.
Если мы запустим
наше приложение, то сможем сами убедиться в том, что наш код выведет значение 6, которое мы
проставили на последнем этапе. Теперь немного усложним задачу.
Заменим ключевое слово class для
нашего класса B на struct. То есть, поменяем класс на структуру.
private struct B
{
public int C { get; set; }
}
Теперь
если мы запустим наше приложение, то увидим на экране старое значение 5.
А если запустим
в дебаг-режиме и посмотрим в Watch, что у нас изменилось, то увидим, что items[0] значение не изменилось.
Для старожилов, тех,
которые общаются с языком C# на "ты", в этом коде нет ничего необычного. Здесь происходит то, что в
терминах .NET Framework называется
boxing/unboxing, или, другими
словами, упаковка/распаковка. Если вы не знаете, что такое упаковка/распаковка
в языке C#, рекомендую посмотреть статью на хабрахабр "Тонкие моменты C#". Если не углубляться в детали,
то упаковка – это приведение значимого типа (value type) к ссылочному
типу (reference type), распаковка – это обратная операция по приведению ссылочного типа к значимому. Термин упаковка легко запомнить, чтобы не путать эти
два понятия, тем, что в управляемой куче выделяется память для хранения значимого
типа. Если грубо, то это некая коробка, в которую мы упаковываем наше значение. Дело
в том, что в строке
var first = items[0];
мы положили нашу
структуру B (value type) в
ссылочный тип object. Упаковка
произошла намного ранее, поскольку мы наш List<B> привели к
типу IList, который
работает со значимыми типами, и, соответственно, упаковал все наши значения.
Поскольку значимые типы при упаковке копируются, не изменяя исходные данные, мы
можем провести небольшой хак с нашим списком IList, поскольку IList является ссылочным типом и хранит ссылки на каждое значение. Поэтому
чтобы наш код заработал, просто нужно после изменения свойства С добавить следующую строку:
items[0] = first;
Полный
исходный код будет выглядеть так:
class Program
{
static void Main(string[]
args)
{
var a = new A(5);
IList items
= (IList)(typeof(A).GetField("_b", BindingFlags.NonPublic
| BindingFlags.Instance).GetValue(a));
var first
= items[0];
first.GetType().GetProperty("C", BindingFlags.Public
| BindingFlags.Instance).SetValue(first, 6);
items[0] = first;
a.Print();
}
}
Теперь
наш код выглядит следующим образом. Мы скопировали значение со items[0] в переменную first,
затем изменили в ней значение, а после этого присвоили переменную first первому элементу коллекции items[0]. Таким образом мы для items[0]
подменили значение. После этого можем убедиться, что у нас все заработало.
После
запуска проекта увидим ожидаемый результат.
Надеюсь, эта статья даст прояснит некоторые моменты в изучении возможностей, которые нам
предоставляет рефлексия. Статья получилась небольшой, водной и позволяет
разобраться на практике, как можно использовать рефлексию для сторонних
библиотек.
No comments:
Post a Comment