Не так просто, как кажется. Часть 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 перед первым добавлением!
Всем спасибо, это было очень познавательно.