Не так просто, как кажется. Часть 2

Бог ты мой, вы нашли множество дополнительных ситуаций, в которых рефакторинг «устранить переменную» может работать неправильно. Вот всего несколько ваших наблюдений: (опять-таки, в каждом случае устраняется переменная «x»).

Проблема возникает каждый раз, когда x рассматривается как переменная, а не как значение. Такие очевидные случаи, когда x находится слева от оператора присваивания, ей присваивается результат оператора ++, или x используется в качестве “ref” или “out” параметра, являются весьма простыми. Однако бывают случаи, когда не столь очевидно, что x выступает в них в качестве переменной. Давайте предположим, что S – это изменяемая структура с полем F:

 S x = new S();
x.F = 123;

В этом случае мы не можем изменить этот код на (new S()).F = 123, поскольку new S() не является переменной. Изменяемое поле требует наличия некоторого хранилища, которое может быть изменено, однако в этом случае у нас его нет.

Ряд людей обратили внимание на то, что правила языка C# об использовании простых имен, могут приводить к проблемам:

 int y = 2;
void M()
{
  Action x = () => { Console.WriteLine(y); };  // refers to this.y
  {
    string y = "hello";
    x();
  }

Оба использования простого имени y в данном случае вполне корректны, поскольку области объявлений, в которых эти имена впервые используются, не пересекаются. Однако применение нашего рефакторинга приводит к их пересечению; этот код должен быть преобразован в

 void M()
{
  {
    string y = "hello";
    ((Action)(()=>{Console.WriteLine(this.y);})))();
  }

И ситуация становится еще хуже, если простые имена не могут быть полностью квалифицированы:

 Action x = () => {int z = 123;};
{
  string z = "hello";
  x();
}

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

У вас также могут возникнуть проблемы и с анонимными типами:

 int x = 42;
var a = new { x };

Этот код должен быть преобразован в

 var a = new { x = 42 };

Аналогично,

 int x = k.y( );
var a = new { x };

не может быть преобразован в

 var a = new { k.y() };

поскольку это приведет к изменению имени свойства анонимного метода.

Перемещение выражения может также привести к нарушению правил определенного присваивания (definite assignment rules); следующий код не может быть изменен с помощью нашего рефакторинга:

 void Foo(out int b)
{
   int x = b = R();
   if (Q()) return;
   doSomething(x);
}

Поскольку он будет преобразован следующим образом

 void Foo(out int b)
{
   if (Q()) return;
   doSomething(b = R());
}

И мы получим метод, который возвращает управление без присвоения значения выходному (out) параметру.

Я уже упоминал о том, что перемещение выражения может привести к изменению семантики программы, поскольку может измениться порядок наблюдаемых побочных эффектов. Было найдено множество жутких случаев, когда методы зависят от побочных эффектов; наиболее коварным из которых является следующий:

 int x = LocksFoo();
lock (Bar) { return M(x); }

Рефакторинг изменяет порядок захвата блокировок Foo и Bar, что может привести к взаимной блокировке, если другой код, зависимый от Foo, всегда захватывал блокировку до захвата блокировки Bar.

Выражения инициализаторов массивов (array initializers) корректны только в небольшом количестве ситуаций; и данный рефакторинг должен это учитывать:

 int[] x = {0,1};
Console.WriteLine(x);

Данной код должен быть преобразован в

 Console.WriteLine(new int[] {0, 1});

И наконец, перемещение выражений может переместить его из проверяемого (checked) контекста в непроверяемый (unchecked), и наоборот:

 int zero = 0;
int one = 1;
int x = int.MaxValue + one;
Console.WriteLine(checked(x + zero));

Простая реализация данного рефакторина изменит нормально работающую программу, в программу, которая завершается неудачно во время выполнения:

 Console.WriteLine(checked(int.MaxValue + one + zero));

Для того, чтобы программа оставалась корректной, рефакторинг должен вводить блок unchecked перед первым добавлением!

Всем спасибо, это было очень познавательно.

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