Udostępnij za pośrednictwem


Простые имена не так уж просты

В C# есть много правил, спроектированных для предотвращения некоторых обычных источников ошибок и поощрения хороших практик программирования. Так много, на самом деле, что частенько достаточно сложно разобраться, какое конкретно правило было нарушено. Я решил, что мог бы потратить некоторое время на обсуждение различных правил. Мы закончим головоломкой.

Для начала, жизненно важно понимать разницу между областью видимости и пространством деклараций. Чтобы осежить вашу память о моей давней статье: область видимости сущности – это регион текста, в котором к сущности можно обращаться по её неквалифицированному имени. Пространство деклараций – это регион текста, в котором две вещи не могут иметь одинаковое имя (за исключением методов, различающихся сигнатурами). «Пространство деклараций локальных переменных» является конкретным вариантом пространств деклараций, используемым для объявления локальных переменных; пространства деклараций локальных переменных имеют особые правила по определению того, когда они перекрываются.

Следующая вещь, которую вам надо понять, чтобы извлечь из этого какой-то смысл, это что такое «простое имя». Простое имя это всегда либо обычный идентификатор, как «x», или, в некоторых случаях, обычный идентификатор, за которым следует список типов-аргументов, как «Frob<int, string>».

Компилятор трактует множество вещей как «простые имена»: объявления локальных переменных, параметры лямбд, и так далее, всегда имеют первую форму простого имени в своих объявлениях. Когда вы пишете «Console.WriteLine(x);», простыми именами являются «Console» и «x», но не «WriteLine». К общему замешательству, есть текстовые сущности, которые имеют форму простых имён, но не трактуются как простые имена компилятором. Возможно, мы поговорим о некоторых из этих ситуаций в будущих невероятных приключениях.

Так что, без дальнейших церемоний, вот несколько относящихся к теме правил, которые часто путают. Особенно затруднительными люди находят правила 3 и 4.

1) Нельзя ссылаться на локальную переменную до её объявления. (Надеюсь, это выглядит разумно)

2) Нельзя иметь две локальных переменных с одним именем в одном пространстве декларации локальных переменных или во вложенных пространствах декларации локальных переменных

3) Область видимости локальных переменных простирается на весь блок, где размещено объявление. Это отличается от C++, где область видимости локальной переменной начинается только после места объявления.

4) Для каждого вхождения простого имени, будь то в объявлении или как части выражения, все использования этого простого имени внутри непосредственно включающего пространства декларации локальных переменных должны ссылаться на одну и ту же сущность.

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

Рассмотрим мир, в котором у нас нет правил 3 и 4. В том мире, этот код будет разрешён:

class C
{
    int x;
    void M()
    {
        // 100 строк кода
        x = 20; // означает «this.x»;
        Console.WriteLine(x); // означает «this.x»
        // 100 строк кода
        int x = 10;
        Console.WriteLine(x); // означает «локальный x»
    }
}

Это затрудняет жизнь читателю кода, у которого есть разумное предположение, что обе строчки «Console.WriteLine(x)» на деле печатают содержимое одной и той же переменной. Но это особенно неприятно по отношению к программисту поддержки, который хочет применить обоснованный стандарт кодирования к этому фрагменту. «Локальные переменные объявляются в начале блока, в котором используются» - вполне обоснованный стандарт кодирования, принятый во многих компаниях. Но замена кода на

class C
{
    int x;
    void M()
    {
        int x;
        // 100 строк кода
        x = 20; // уже не означает «this.x»;
        Console.WriteLine(x); // уже не означает «this.x»
        // 100 строк кода
        x = 10;
        Console.WriteLine(x); // означает «local x»
    }
}

меняет смысл кода! Мы хотим порицать написание многосотстрочных методов, но затруднение и внесение ошибок в процесс их рефакторинга во что-то более понятное – не лучший способ достичь этой цели.

Заметьте, что в оригинальной версии программы, правило 3 означает, что программа нарушает правило 1 – первое использование «x» трактуется как ссылка на локальную переменную до её объявления. Тот факт, что она нарушает правило 1 из-за правила 3 как раз и есть в точности то, что не даёт ей нарушить правило 4! Значение «x» непротиворечиво во всём блоке; он везде означает локальную переменную, которая, таким образом, иногда используется до объявления. Если бы мы вычеркнули правило 3, то это было бы нарушением правила 4, потому что тогда бы мы имели два противоречивых значения простого имени «x» в пределах одного блока.

Теперь, эти правила не означают, что вы можете рефакторить тяп-ляп. Мы всё ещё можем сконструировать ситуации, в которых похожие рефакторинги ломаются. Например;

class C
{
    int x;
    void M()
    {
        {
            // 100 строк кода
            x = 20; // означает "this.x";
            Console.WriteLine(x); // означает "this.x"
        }
        {
            // 100 строк кода
            int x = 10;
            Console.WriteLine(x); // означает "local x"
        }
    }
}

Это полностью легально. У нас одно и то же простое имя используется двумя разными способами в двух разных блоках, но непосредственно включающие блоки каждого использования не перекрываются. Локальная переменная видима во всём своём непосредственно включающем блоке, но блок не перекрывается с блоком выше. В этом случае, безопасно переносить объявление переменной в начало её блока, но небезопасно переносить его в начало внешнего блока; это изменит смысл «x» в первом блоке. Перемещение объявления вверх почти всегда безопасно; перемещение наружу не обязательно безопасно.

Теперь, когда вы всё это знаете, вот головоломка для вас, головоломка, которую я понял полностью неправильно, когда впервые увидел:

using System.Linq;
class Program
{
     static void Main()
    {
        int[] data = { 1, 2, 3, 1, 2, 1 };
        foreach (var m in from m in data orderby m select m)
          System.Console.Write(m);
    }
}

Имя «m» тут явно используется несколько раз в разных смыслах. Легальна ли эта программа? Если да, то почему правила по запрету повторного использования простых имён не срабатывают? Если нет, то какое именно правило здесь нарушено?

[Эрик в отпуске, пост был предварительно записан]