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 классами.
Всем добра, и пусть ваш код будет ясным и чистым.
Комментарии (27)
shai_hulud
03.07.2016 21:11+1Валидный путь раскрывать приватные поля без «хаков» это под-класс https://gist.github.com/deniszykov/556ddc0a1d335c96fb58b808ac66c894
Oxoron
03.07.2016 23:23Вы правы, nested классы видят поля внешних классов. При этом они довольно схожи с protected полями: также нужно создать дополнительный класс для доступа к секрету, также требуется изменить основной класс. Полагаю, область применения также совпадает.
Быстродействие, по идее, тоже одинаковое, но тут надо смотреть. Завтра замерю, выложу результаты.
В любом случае, спасибо за 5 способ.Oxoron
04.07.2016 20:02Неожиданно. Вложенные классы оказались шустрее наследников.
Inheritor: 5.4560ns
Nested: 4.6916ns
AxisPod
04.07.2016 07:57Хм, 3 метод отказыватся работать на .NET 4.5. Не хочет выдавать он приватные поля. Тогда как для public работает как и задумывалось.
Oxoron
04.07.2016 09:16Только что проверил на 4, 4.5, 4.6 — все ок. VS 2015, AnyCPU, Debug.
AxisPod
04.07.2016 09:20Хм, проверял на .FiddleNet
Oxoron
04.07.2016 09:43+1Как варианты — full trusted code, или проблемы с динамическими библиотеками.
Если работает с public полями — полагаю, дело в partial trust.
Dentty
04.07.2016 08:45+1Вам не кажется, что задача немного надуманная: реализовывать доступ к приватному полю класса, если есть возможность модифицировать этот самый класс? В этом случае, всё решается public свойством/методом. В случае, когда такого доступа нет и поле действительно private, то единственный способ — это рефлексия в той или иной вариации (примеры 2 и 3 в вашей статье).
ofmetal
11.11.2016 11:121. Перемещение информации из будущего в прошлое тождественно перемещению со скоростью выше скорости света.
2. Следствие из второго постулата СТО предусматривает существование предельной скорости, скорости света, быстрее которой информация не может быть передана.
=> 3. Передача информации в прошлое противоречит СТО.
Hydro
04.07.2016 09:04+21) Имхо, для тестов, способ 3 — самый годный. Плюс лямбда-геттер надо бы закэшировать.
Имхо, вложенный класс для раскрытия состояния (конкретно для тестовых случаев) — способ неправильный и нагружает класс лишним знанием о том, что кто-то должен будет к приватному члену обратиться. Лучше поправить упавшие тесты при рефакторинге приватного члена, на который тесты заточены, чем вносить лишнюю ответственность в класс.
2) Тестировать внутреннее состояние не совсем правильно, ибо по канонам ООП, класс должен обеспечивать корректную работу с внешней средой через публичный API. А как он это делает внутри — его личное дело.Oxoron
04.07.2016 09:271. Согласен. Единственно, про protected механику знает почти любой джун, а с nested классами и рефлексией все немного хуже, что в будущем может аукнуться при поддержке. Разве что, вынести акцессор в отдельный метод GetPrivate(object obj, string fieldName)…
Andrey_Koryakin
04.07.2016 12:24Вопрос новичка:
А что означает эта строка:
protected int SecretForInheritors => _secret;
Точнее, оператор => в ней?
Это ведь лямбда-оператор?vola
04.07.2016 12:37Сокращенный вариант readonly свойства:
protected int SecretForInheritors { get { return _secret; } }
Так же можно и функции обьявлять:
public int Mult(int x, int m) => x * m;
CheeseMaster
04.07.2016 12:24А можно ссылку (или примерное название) на статью, упомянутую в первом абзаце? Я так понимаю, это фишка C#? Думается мне, что совместно с методами расширения довольно интересно можно расстрелять себе ноги.
vola
04.07.2016 12:41Ноги расстрелять нельзя, т.к. методы расширения имеют доступ только к публичным членам
Oxoron
04.07.2016 12:44Не могу найти статью. Даже мой комментарий к ней пропал. Возможно, автор скрыл в черновики.
По поводу методов расширения — для того, чтобы метод видел эту переменную, он должен быть членом SecretKeeper (напрямую или в nested классе), так что для расстрельной ситуации придется постараться.
HotkeyM
04.07.2016 13:20public void DoSomething(Example example) { this.JustInt = example.JustInt; // Вполне валидная строка, некоторых удивляет }
Наверное, такое может удивить только программистов, не представляющих, что такое копирующий конструктор.
darkdaskin
04.07.2016 23:15Для красоты рефлексию можно завернуть в
DynamicObject
:
List<int> realList = new List<int>(); dynamic exposedList = ExposedObject.From(realList); // Read a private field - prints 0 Console.WriteLine(exposedList._size);
Подробности и ссылка на библиотеку.
Таким же образом можно завернуть и вариант с expression trees.
aquamakc
05.07.2016 20:08А можно вопрос? Для чего всё это? Скрытые поля ведь не просто так сделаны скрытыми. Если б разработчик хотел, чтоб к полю был доступ он бы сделал это либо через публичное свойство, или (если он старовер) через публичные get()-set() функции.
Я вижу 2 варианта:
1) Желание прострелить себе ногу модифицируя осознано скрытые разработчиком поля объектов, т.е. явно нарушить архитектуру приложения;
2) Изобрести костыль к кривому приложению, который, опять-же может оказаться выстрелом в ногу и сделать приложение ещё кривее.Oxoron
05.07.2016 21:13Последняя задача — кастомный сериализатор, который работает со скрытыми полями. В принципе, можно было обойти ту проблему иначе, но за счет пары дополнительных факторов было решено пилить свой сериализатор, в том числе с доступом к приватным полям.
По вашей градации — пункт 2. Разве что, багов после «костыля» стало меньше, и ноги пока целы.
WNeZRoS
Вы переусложнили GetSecretUsingExpressionTrees, в этом случае достаточно такого:
Oxoron
Спасибо, такой вариант действительно проще и быстрее. Внес его в статью.