Скрытые поля 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)


  1. MonkAlex
    23.06.2016 16:01
    +1

    Где связь первой и второй половиной? ЯННП Т_Т


    1. whitedemon9
      23.06.2016 18:01

      Тоже сначала не понял о чем идет речь, пока не перечитал самое начало:

      Оказывается, к переменной Private можно получить доступ из другого объекта ТОГО ЖЕ типа.


  1. Oxoron
    23.06.2016 16:42

    Если не ошибаюсь, похожий пример доступа к private полям видел у Троелсена.
    При этом есть немало других способов доступа к приватным полям: GetMemberInfo(), nested classes, ExpressionTrees, плюс совсем уж экзотика, завязанная на выравнивание полей в структурах.


  1. lair
    23.06.2016 16:47
    +2

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


    Странно, серьезно? А что странного в том, что вы получили доступ к private-филду, находясь внутри класса, его объявившего?


    1. impwx
      23.06.2016 17:02

      В некоторых языках (например Scala) есть ограничение видимости конкретным объектом, а не классом. Хотя согласен, что классовое ограничение встречается гораздо чаще.


      1. lair
        23.06.2016 17:16

        … но даже там оно нотируется иначе.