Скрытые поля C# недоступны за пределами класса – ясно как дважды два. Поэтому следующий код не должен работать.
public class Example
{
private string _someValue;
public void DoSomething(Example otherObject)
{
_someValue = otherObject._someValue; // What!? You can't access a private variable from another object! Can you?
}
}
Но, как ни странно, он работает.
Оказывается, к переменной Private можно получить доступ из другого объекта того же типа. И, если задуматься, это вполне логично. Зачем нужны переменные Private? Для инкапсуляции и управления состоянием объекта.
Представьте, что нам нужно создать контейнер с ограниченной емкостью – коробку конфет. Мы могли бы сделать это так:
public class Chocolate
{
public string Name { get; set; }
}
public class ChocolateBox
{
public List<Chocolate> Chocolates { get; set; }
public bool IsValid()
{
return Chocolates.Count > -1 && Chocolates.Count < 11;
}
}
Догадайтесь, в чем проблема
Несмотря на то, что у нас есть метод IsValid, в коробку можно добавить больше, чем максимальное количество конфет (хотя, думаю, заказчик был бы не против). Чтобы решить эту проблему, мы можем инкапсулировать коробку и добавить методы для соблюдения необходимых ограничений. К тому же это позволит нам избавиться от метода IsValid.
public class Chocolate
{
public string Name { get; set; }
}
public class ChocolateBox
{
private List<Chocolate> _chocolates;
public void AddChocolate(Chocolate chocolate)
{
if(_chocolates == null) _chocolates = new List<Chocolate>();
if(_chocolates.Count > 10) throw new Exception("No more space");
_chocolates.Add(chocolate);
}
// Other methods removed for brevity
}
Но здесь есть одно условие.
Разработчики, создающие другие части системы, могут не догадываться, как работает ваша «коробка». В таком случае на помощь приходит модульное или интеграционное тестирование. Чтобы избежать любых недоразумений с кодом, нужно просто сделать неявное явным.
Но если разработчик знаком с классом, ему можно доверить изменение внутреннего состояния – в том числе для других классов того же типа.
Уверен, что из этой особенности можно извлечь массу плюсов, но хочу выделить лишь один, который оказался для меня особенно полезным.
Доступ к скрытым переменным при создании объектов-значений
Объекты-значения – это один из шаблонов проблемно-ориентированного программирования, который также можно использовать за пределами этой модели. Отличительная черта таких объектов в том, что их равенство основывается не на идентификаторе, а на значении.
Простой пример – строка. Две строки с одними и теми же символами в одинаковом порядке считаются равнозначными. Аналогично можно сравнивать телефонные номера из системы CRM, если, конечно, вы не пишете ПО для оператора телефонной связи – в таком случае телефонный номер может выступать идентификатором клиента и, следовательно, не быть объектом-значением.
В большинстве случаев объекты-значения неизменяемы. В том числе это касается строк: «изменяя» строку, вы, по сути, создаете новый объект. Чем полезна такая неизменяемость? Мне приходит на ум 4 причины:
- Объекты намного легче тестировать.
- Объекты легче распараллелить.
- Можно быть уверенным, что они всегда в действительном состоянии.
- Не нужно отслеживать много идентификаторов в приложении (пожалуй, основная причина).
Пример объекта-значения
В качестве примера давайте смоделируем контейнер для жидкости или, проще говоря, чашку с максимальной емкостью в 1 литр. При этом мы можем перелить содержимое из одной чашки в другую (с точки зрения кода для этого понадобится еще один экземпляр чашки). Жидкость, превышающая максимальный объем, считается утерянной. Если же две чашки содержат одинаковый объём жидкости, они равны.
Начнем с тестирования. Чтобы чашка была неизменяемой, передадим текущий объем жидкости в конструктор. Для начала проверим, можем ли мы создать чашку, наполненную жидкостью.
[Test]
public void Constructor_ValidVolume_ObjectIsNotNull()
{
var cup = new Cup(0.5m);
Assert.NotNull(cup);
}
Теперь удостоверимся, что чашка не может содержать недопустимый объем – более одного или менее нуля литров.
[TestCase(-0.1)]
[TestCase(1.1)]
public void Constructor_InvalidVolumes_ThrowsException(decimal invalidVolume)
{
ArgumentOutOfRangeException expected = new ArgumentOutOfRangeException("volume");
ArgumentOutOfRangeException actual = null;
try
{
var cup = new Cup(invalidVolume);
}
catch (ArgumentOutOfRangeException ex)
{
actual = ex;
}
Assert.AreEqual(expected.ParamName, actual.ParamName);
}
А теперь самое интересное. Как сделать, чтобы чашки были одинаковыми? Для этого нам нужен доступ к скрытым переменным.
[Test]
public void Equals_TwoEqualCups_AreEqual()
{
var cup1 = new Cup(0.5m);
var cup2 = new Cup(0.5m);
Assert.AreEqual(cup1, cup2);
}
На досуге можете потренироваться с переливанием жидкости. Впрочем, возникает интересный вопрос: как перелить жидкость из чашки, если она неизменяема? :)
Вывод
Объекты-значения – полезный концепт проблемно-ориентированного программирования. При переопределении метода equals вы можете воспользоваться преимуществами этой особенности в C#, чтобы получить доступ к скрытым переменным однотипных классов.
Поделиться с друзьями
Комментарии (6)
Oxoron
23.06.2016 16:42Если не ошибаюсь, похожий пример доступа к private полям видел у Троелсена.
При этом есть немало других способов доступа к приватным полям: GetMemberInfo(), nested classes, ExpressionTrees, плюс совсем уж экзотика, завязанная на выравнивание полей в структурах.
lair
23.06.2016 16:47+2Но, как ни странно, он работает.
Странно, серьезно? А что странного в том, что вы получили доступ к private-филду, находясь внутри класса, его объявившего?impwx
23.06.2016 17:02В некоторых языках (например Scala) есть ограничение видимости конкретным объектом, а не классом. Хотя согласен, что классовое ограничение встречается гораздо чаще.
MonkAlex
Где связь первой и второй половиной? ЯННП Т_Т
whitedemon9
Тоже сначала не понял о чем идет речь, пока не перечитал самое начало:
Оказывается, к переменной Private можно получить доступ из другого объекта ТОГО ЖЕ типа.