Чотири способи отримання даних з прихованих полів C#

Добрий день. Не так давно на хабре проскакувала стаття, в якій показувалася можливість звернення до закритих полів об'єкта з іншого екземпляра того ж класу.

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 класами.

Всім добра, і нехай ваш код буде ясним і чистим.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.