Скрытые поля C# недоступны за пределами класса – ясно как дважды два. Поэтому следующий код не должен работать.

public class Example
{
 private string _someValue;
 public void DoSomething(Example otherObject)
 {
  _someValue = otherObject._someValue; // Это еще что такое? Но ведь доступ к скрытой переменной нельзя получить из другого объекта!
 }
}

Но, как ни странно, он работает.

Оказывается, к переменной 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#, чтобы получить доступ к скрытым переменным однотипных классов.
Поделиться с друзьями
-->

Комментарии (1)


  1. lair
    25.08.2016 12:59

    Поэтому следующий код не должен работать.

    А где в этом коде мы покинули пределы класса-то?


    Ну и в чем "разоблачение", спрашивается?


    PS Отдельный зачет, конечно, в том, что тесты есть, а реализации — нет. Ну и как же "скрытые поля" помогают нам создавать объекты-значения?