Monday, July 21, 2014

Магия рефлексии в C#


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