Переменные, заканчивающиеся на "1", имеют "1", удаленные в ILSpy. Зачем?

В попытке выяснить, как компилятор С# оптимизирует код, я создал простое тестовое приложение. С каждым изменением теста я скомпилировал приложение, а затем открыл двоичный файл в ILSpy.

Я только что заметил что-то, что для меня странно. Очевидно, что это намеренно, однако, я не могу придумать, почему компилятор сделал это.

Рассмотрим следующий код:

static void Main(string[] args)
{
    int test_1 = 1;
    int test_2 = 0;
    int test_3 = 0;

    if (test_1 == 1) Console.Write(1);
    else if (test_2 == 1) Console.Write(1);
    else if (test_3 == 1) Console.Write(2);
    else Console.Write("x");
}

Беспредметный код, но я написал это, чтобы увидеть, как ILSpy интерпретирует операторы if.

Однако, когда я скомпилировал/декомпилировал этот код, я заметил что-то, что заставило меня почесывать голову. Моя первая переменная test_1 была оптимизирована до test_! Есть ли веская причина, почему компилятор С# будет делать это?

Для полной проверки это вывод Main(), который я вижу в ILSpy.

private static void Main(string[] args)
{
    int test_ = 1; //Where did the "1" go at the end of the variable name???
    int test_2 = 0;
    int test_3 = 0;
    if (test_ == 1)
    {
        Console.Write(1);
    }
    else
    {
        if (test_2 == 1)
        {
            Console.Write(1);
        }
        else
        {
            if (test_3 == 1)
            {
                Console.Write(2);
            }
            else
            {
                Console.Write("x");
            }
        }
    }
}

UPDATE

По-видимому, после проверки IL, это проблема с ILSpy, а не с компилятором С#. Евгений Подскаль дал хороший ответ на мои первоначальные комментарии и наблюдения. Тем не менее, мне интересно узнать, является ли это скорее ошибкой в ​​ILSpy или если это преднамеренная функциональность.

Ответы

Ответ 1

Ну, это ошибка. Не так много ошибок, маловероятно, чтобы кто-нибудь когда-либо подавал отчет об ошибке. Обратите внимание, что ответ Евгения очень вводит в заблуждение. ildasm.exe достаточно умен, чтобы знать, как найти файл PDB для сборки и получить информацию об отладке для сборки. Который включает имена локальных переменных.

Это обычно не роскошь, доступная для дизассемблера. Эти имена фактически не присутствуют в самой сборке, и они обязательно должны делать без PDB. Что-то, что вы можете увидеть в файле ildasm.exe, просто удалите файлы .pdb в каталогах obj\Release и bin\Release, и теперь он выглядит так:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       50 (0x32)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1,
           int32 V_2)
  IL_0000:  ldc.i4.1
  // etc...

Имена, подобные V_0, V_1 и т.д., конечно, невелики, дизассемблер обычно придумывает что-то лучшее. Что-то вроде "num".

Итак, как бы там ни было обнаружено сообщение об ошибке в ILSpy, он также считывает файл PDB, но искажает полученный символ. Вы можете указать ошибку с продавцом, но вряд ли они будут относиться к ней как к высокоприоритетной ошибке.

Ответ 2

Вероятно, это проблема с декомпилятором. Поскольку IL корректен для .NET 4.5 VS2013:

.entrypoint
  // Code size       79 (0x4f)
  .maxstack  2
  .locals init ([0] int32 test_1,
           [1] int32 test_2,
           [2] int32 test_3,
           [3] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0

edit: он использует данные из файла .pdb(см. этот ответ), чтобы получить правильные переменные имен. Без pdb он будет иметь переменные в форме V_0, V_1, V_2.

EDIT:

Изменяется имя в файле NameVariables.cs в методе:

public string GetAlternativeName(string oldVariableName)
{
    if (oldVariableName.Length == 1 && oldVariableName[0] >= 'i' && oldVariableName[0] <= maxLoopVariableName) {
        for (char c = 'i'; c <= maxLoopVariableName; c++) {
            if (!typeNames.ContainsKey(c.ToString())) {
                typeNames.Add(c.ToString(), 1);
                return c.ToString();
            }
        }
    }

    int number;
    string nameWithoutDigits = SplitName(oldVariableName, out number);

    if (!typeNames.ContainsKey(nameWithoutDigits)) {
        typeNames.Add(nameWithoutDigits, number - 1);
    }

    int count = ++typeNames[nameWithoutDigits];

    if (count != 1) {
        return nameWithoutDigits + count.ToString();
    } else {
        return nameWithoutDigits;
    }
}

NameVariables класс использует словарь this.typeNames для хранения имен переменных без конечного числа (такие переменные означают что-то особенное для ILSpy или, возможно, даже для IL, но я действительно сомневаюсь в этом), связанный с счетчиком их явлений в методе декомпилировать.

Это означает, что все переменные (test_1, test_2, test_3) завершатся в одном слоте ( "test_" ), а для первого count var будет одним, что приведет к выполнению:

else {
    return nameWithoutDigits;
}

где nameWithoutDigits есть test_

EDIT

Во-первых, спасибо @HansPassant и его ответ за указание на ошибку в этом сообщении.

Итак, источник проблемы:

ILSpy такой же умный, как и ildasm, потому что он также использует данные .pdb(или как еще он получает имена test_1, test_2 вообще). Но его внутренняя работа оптимизирована для использования с сборками без какой-либо информации, связанной с отладкой, поэтому ее оптимизация, связанная с работой с переменными V_0, V_1, V_2, работает непоследовательно с богатством метаданных из файла .pdb.

Как я понимаю, виновником является оптимизация для удаления _0 из одиночных переменных.

Фиксация, вероятно, потребует распространения факта использования данных .pdb в код поколения имен переменных.