Добрый день. Не так давно на хабре проскакивала статья, в которой показывалась возможность обращения к закрытым полям объекта из другого экземпляра того же класса.
Пусть у нас есть класс:
Добавим в него protected свойство:
И добавим класс наследник:
Проверяем код:
Иногда способ используется для тестирования: добавление protected поля не меняет публичный контракт класса, наследник создается в тестовом проекте. Помогает избегать заглушек (mocks\stubs) в тестовых методах. Модификацией этого метода можно считать использование internal полей и InternalVisibleTo атрибута в AssemblyInfo.
Недостатки: приходится создавать\поддерживать дополнительное поле, либо менять старое, для чего нужен как минимум доступ к классу. Для внешней библиотеки не применить. Если у класса есть наследники — для них изменится контракт класса, что увеличивает вероятность сделанной в будущем ошибки.
Снова используем тестовый класс:
Создадим статический класс с методом для извлечения секрета:
Протестировать можно кодом:
Способ годится в случаях, когда нет доступа к коду SecretKeeper, или нет желания менять контракт ��ласса. Иногда такой код можно увидеть в продакшне: разрабатывается новая версия библиотеки, потребовался доступ к private полю, менять текущий класс нельзя, ибо «работает — не трогай». Иногда применяется в тестировании, когда менять исходный класс нет времени. Если все-таки используете подобный вариант — помните про возможность закешировать FieldInfo (MemberInfo).
Недостатки: завязка на имя поля, что может аукнуться при рефакторинге. Кроме того, рефлексия — инструмент достаточно медленный.
Рефлексию вполне можно приготовить для шустрой работы. Снова рассмотрим тестовый класс:
И добавим в наш статический SecretFinder метод:
Протестировать можно кодом:
Лично я применял этот способ во время написания кастомного сериализатора. Полученные функции спокойно работают с приватными полями, кешируются, при этом производительность в два раза меньше аналогичного кода написанного в редакторе (и в 8 раз больше предыдущего примера).
Недостатки: достаточно сложен, даже для примера выше пришлось немного погуглить. В примере выше также наличествует завязка на имя свойства.
Способ основан на аналоге union структур из C.
В качестве примера рассмотрим структуру:
Создадим её копию, создав вместо private _secret публичное поле по тому же смещению:
Добавим структуру, содержащую как секрет, так и зеркало для его обнаружения:
В статический SecretFinder добавим метод:
Тестируется все кодом:
Область применения крайне ограничена: способ доступен только для структур, нуж��о быть предельно внимательным со смещениями, ограничены типы полей в структурах, требуются довольно специфические структуры, информация о выравниваниях. И хотя подход не лишен известной элегантности, я не могу представить себе ситуацию, в которой он оправдан.
В завершение хочу добавить: первые три подхода работают как с геттерами, так и сеттерами. Также можно работать со свойствами и методами. Метод с наследниками неприменим для статических классов (ибо они sealed), сложность рефлексивных методов слегка возрастет при работе с Generic классами.
Всем добра, и пусть ваш код будет ясным и чистым.
public class Example
{
  private int JustInt;
  // Some code here
  public void DoSomething(Example example)
  {
    this.JustInt = example.JustInt; // Вполне валидная строка, некоторых удивляет
  }
}
Способ 1, не совсем честный: используем protected поля и наследников
Пусть у нас есть класс:
public class SecretKeeper
{
    private int _secret; // Наше приватное поле
    // Для упрощения тестирования
    public int Secret{get { return _secret; } set { _secret = value; }}        
}
Добавим в него protected свойство:
    protected int SecretForInheritors => _secret; // Теперь наследники могут читать _secret
И добавим класс наследник:
public class SecretKeeperInheritor : SecretKeeper
{
  public int GetSecret()
  {
    return SecretForInheritors;
  }
}
Проверяем код:
var secret = new SecretKeeperInheritor {Secret = 42}.GetSecret();
Console.WriteLine
(
  secret == 42 ? "Inheritors test: passed" : "Inheritors test: failed"
);
 Иногда способ используется для тестирования: добавление protected поля не меняет публичный контракт класса, наследник создается в тестовом проекте. Помогает избегать заглушек (mocks\stubs) в тестовых методах. Модификацией этого метода можно считать использование internal полей и InternalVisibleTo атрибута в AssemblyInfo.
Недостатки: приходится создавать\поддерживать дополнительное поле, либо менять старое, для чего нужен как минимум доступ к классу. Для внешней библиотеки не применить. Если у класса есть наследники — для них изменится контракт класса, что увеличивает вероятность сделанной в будущем ошибки.
Способ 2, классический: рефлексия с GetMemberInfo
Снова используем тестовый класс:
public class SecretKeeper
{
    private int _secret;
    // Для упрощения тестирования
    public int Secret{get { return _secret; } set { _secret = value; }}
}
Создадим статический класс с методом для извлечения секрета:
public static class SecretFinder
{
    public static int GetSecretUsingFieldInfo(this SecretKeeper keeper)
    {
        FieldInfo fieldInfo = typeof (SecretKeeper).GetField("_secret", BindingFlags.Instance | BindingFlags.NonPublic);
        int result = (int)fieldInfo.GetValue(keeper);
        return result;
    }
}
Протестировать можно кодом:
SecretKeeper keeper = new SecretKeeper {Secret = 42}; // Создаем объект с секретом
int fieldInfoSecret = keeper.GetSecretUsingFieldInfo(); // Извлекаем секрет
Console.WriteLine
(
    fieldInfoSecret == 42 ? "FieldInfo test: passed" : "FieldInfo test: failed" // Немного форматируем вывод
);
Способ годится в случаях, когда нет доступа к коду SecretKeeper, или нет желания менять контракт ��ласса. Иногда такой код можно увидеть в продакшне: разрабатывается новая версия библиотеки, потребовался доступ к private полю, менять текущий класс нельзя, ибо «работает — не трогай». Иногда применяется в тестировании, когда менять исходный класс нет времени. Если все-таки используете подобный вариант — помните про возможность закешировать FieldInfo (MemberInfo).
Недостатки: завязка на имя поля, что может аукнуться при рефакторинге. Кроме того, рефлексия — инструмент достаточно медленный.
Способ 3, ускоренный классический: рефлексия с ExpressionTrees
Рефлексию вполне можно приготовить для шустрой работы. Снова рассмотрим тестовый класс:
public class SecretKeeper
{
    private int _secret;
    // Для упрощения тестирования
    public int Secret{get { return _secret; } set { _secret = value; }}
}
И добавим в наш статический SecretFinder метод:
public static int GetSecretUsingExpressionTrees(this SecretKeeper keeper)
{
    ParameterExpression keeperArg = Expression.Parameter(typeof(SecretKeeper), "keeper"); // SecretKeeper keeper argument
    Expression secretAccessor = Expression.Field(keeperArg, "_secret"); // keeper._secret
    var lambda = Expression.Lambda<Func<SecretKeeper, int>>(secretAccessor, keeperArg);
    var func = lambda.Compile(); // Получается функция return result = keeper._secret;
    return func(keeper);
}
Протестировать можно кодом:
SecretKeeper keeper = new SecretKeeper {Secret = 42}; // Создаем объект с секретом
int fieldInfoSecret = keeper.GetSecretUsingExpressionTrees(); // Извлекаем секрет
Console.WriteLine
(
    fieldInfoSecret == 42 ? "ExpressionTrees test: passed" : "ExpressionTrees test: failed" // Форматируем вывод
);
Лично я применял этот способ во время написания кастомного сериализатора. Полученные функции спокойно работают с приватными полями, кешируются, при этом производительность в два раза меньше аналогичного кода написанного в редакторе (и в 8 раз больше предыдущего примера).
Недостатки: достаточно сложен, даже для примера выше пришлось немного погуглить. В примере выше также наличествует завязка на имя свойства.
Способ 4, для тех, кто не ищет легких путей
Способ основан на аналоге union структур из C.
В качестве примера рассмотрим структуру:
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct StructWithSecret
{
    [FieldOffset(0)] private int _secret;
    public StructWithSecret(int secret)
    {
        _secret = secret;
    }
}
 Создадим её копию, создав вместо private _secret публичное поле по тому же смещению:
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct Mirror
{
    [FieldOffset(0)] public int Secret;
}
 Добавим структуру, содержащую как секрет, так и зеркало для его обнаружения:
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct Holmes
{
    [FieldOffset(0)] public StructWithSecret HereIsSecret; // Тут хранится секрет
    [FieldOffset(0)] public Mirror LetsLookAtTheMirror; // По тому же смещению стоит зеркало
}
В статический SecretFinder добавим метод:
public static int GetSecretFromStruct(this StructWithSecret structWithSecret)
{
    Holmes holmes = new Holmes {HereIsSecret = structWithSecret}; // Передаем Холмсу структуру с секретом
    return holmes.LetsLookAtTheMirror.Secret; // Холмс смотрит в зеркальце (а оно у него рядом с секретом) и секрет раскрыт
}
Тестируется все кодом:
var alreadyNotSecret = new StructWithSecret(42).GetSecretFromStruct();
Console.WriteLine
    (
        alreadyNotSecret == 42 ? "Structs test: passed" : "Structs test: failed"
    );
Область применения крайне ограничена: способ доступен только для структур, нуж��о быть предельно внимательным со смещениями, ограничены типы полей в структурах, требуются довольно специфические структуры, информация о выравниваниях. И хотя подход не лишен известной элегантности, я не могу представить себе ситуацию, в которой он оправдан.
В завершение хочу добавить: первые три подхода работают как с геттерами, так и сеттерами. Также можно работать со свойствами и методами. Метод с наследниками неприменим для статических классов (ибо они sealed), сложность рефлексивных методов слегка возрастет при работе с Generic классами.
Всем добра, и пусть ваш код будет ясным и чистым.
