Составные операторы присваивания. Часть 1

Когда люди пытаются объяснить новичку составные операторы присваивания, такие как += –= *= /= %= <<= >>= &= |= ^=, они обычно говорят нечто подобное: «x += 10; - это всего лишь более короткая запись для x = x + 10;» И хотя это совершенно верно для локальной переменной x типа int, это не совсем так в общем случае. Существует множество тонкостей, связанных с составными операторами присваивания, которые не очевидны с первого взгляда.

Прежде всего, предположим, что выражение, которое находится слева, обладает побочными эффектами или стоимость его вызова очень высока. И вы хотите, чтобы оно выполнялось только один раз:

 class C 
{ 
  private int f;
  private int P { get; set; }
  private static C s = new C();
  private static C M() 
  {  
    Console.WriteLine("Hello");
    return s;
  }
  private struct Evil 
  {
      public int f; // Зло! Изменяемый значимый тип с открытым полем!
      public int P { get; set; }
  }
  private static Evil[] evil = new Evil[2000];
  private static Evil[] N()
  {
    Console.WriteLine("Badness");
    return evil;
  }

Если где-то внутри класса C у вас есть код: M().f += 10; тогда вы хотите, чтобы побочные эффекты, связанные с вызовом метода M произошли только один раз. Этот код не аналогичен M().f = M().f + 10;

А чему он аналогичен? Как насчет такого:

 C receiver = M();
receiver.f = receiver.f + 10;

Правильно? Кажется да, но давайте рассмотрим более сложный пример. Предположим, у нас есть следующий код: N()[123].f += 10;. Тогда следующий код:

 Evil receiver = N()[123];
receiver.f = receiver.f + 10;

явно не правильный. Мы делаем копию содержимого переменной N()[123], и затем изменяем переменную, содержащую копию, но нам нужно изменить исходное значение.

Это еще одно доказательство того, почему изменяемые значимые типы являются полнейшим злом!

Для точного выражения семантики, нам нужна возможность, которой нет в языка C#, под названием, «ссылки на локальные переменные» («ref locals»). Язык C# поддерживает ссылки для параметров функций, но не поддерживает ссылки для локальных переменных. При создании ссылки для параметров функции вы говорите: «этот параметр является синонимом этой переменной»:

 void N(ref int x) { x = 10; }
    …
N(ref M().f);

Это означает: «Вычислите выражение, а затем сделайте, чтобы переменная x ссылалась на ту же самую область памяти». Предположим, у нас есть аналогичная возможность не только для параметров, но и для локальных переменных. Тогда мы можем создать локальную переменную, которая является синонимом для (возможно нелокальной) переменной. Тогда M().f += 10 будет эквивалентно следующему коду:

 ref int variable = ref M().f;
variable = variable + 10;

Теперь побочный эффект от вызова метода M произойдет только один раз. Аналогично, следующий код: N()[123].f += 10; который содержит массив изменяемых значимых типов будет преобразован так:

 ref int variable = ref N()[123].f;
variable = variable + 10;

Побочный эффект вызова метода N произойдет один раз и мы изменим поле правильной переменной.

Язык C# не поддерживает возможность создания ссылок на локальные переменные, хотя CLR поддерживает эту возможность. Мы можем реализовать эту возможность, хотя, мне кажется, что у нас есть более приоритетные задачи.

А что, если вместо переменной мы бы изменяли свойство?

 M().P += 10;

Вы опять можете подумать, что это всего лишь синтаксический сахар для:

 C receiver = M();
receiver.P = receiver.P + 10;

Что, конечно же, является синтаксическим сахаром для:

 C receiver = M();
receiver.set_P(receiver.get_P() + 10);

Опять таки, мы хотим, чтобы побочный эффект возник только один раз, хотя, конечно же, нам придется вызвать два разных метода для getter-а и setter-а; этого избежать нельзя.

Но, опять-таки, у нас будут проблемы, если receiver будет переменной значимого типа. Тогда, для кода: N()[123].P += 10; нам придется генерировать:

 ref Evil receiver = ref N()[123];
receiver.set_P(receiver.get_P() + 10);

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

Аналогично, если бы у нас был индексатор, объявленный в классе C:

 M()[X()] += 10;

Теперь нам пришлось бы отслеживать и receiver и индекс, чтобы они не вычислялись дважды, следующим образом:

 C receiver = M();
int index = X();
receiver[index] = receiver[index] + 10;

И, конечно же, как и в случае со свойствами, индексатор является всего лишь синтаксическим сахаром для вызова метода, и, опять-таки, нам нужно убедиться, что мы будем использовать правильные ссылки, если receiver является переменной изменяемого значимого типа.

То же самое касается операторов += и -= для событий, поскольку они являются синтаксическим сахаром для методов подписки и отписки от события.

В любом случае, я думаю, что хватит уже рассказывать о том, что побочные эффекты должны выполняться только один раз и что определить корректное место изменения состояния не так просто как кажется.

Еще одним интересным моментом встроенных составных операторов присваивания заключается в том, что в случае необходимости неявно добавляется «явное» преобразование типов. Например, если вы напишите:

 short s = 123;
s += 10;

То этот код не преобразуется в s = s + 10, поскольку результатом сложения short с int будет int, что делает присваивание некорректным. На самом деле этот код преобразуется так:

 s = (short)(s + 10);

Тогда, если произойдет переполнение типа short, произойдет автоматическое обрезание типа к нужному размеру.

Последний тонкий момент для встроенных составных операторов присваивания заключается в том, что если запрещено само присваивание, то составной оператор присваивания тоже запрещен. Если вы напишите:

 int i = 10;
short s = 123;
s += i;

Этот код некорректен, поскольку s = i некорректно.

Эти детали интересны сами по себе, а в следующий раз мы рассмотрим, как эти тонкости влияют на некоторые предложенные расширения языка C#.

Оригинал статьи