Что компилятор думает о выражении switch?

Вдохновленный от -5 вопрос снова!

Я прочитал [ этот комментарий] Quartermeister и был удивлен!

Так почему это компилируется

switch(1) {
    case 2:
}

но это не так.

int i;

switch(i=1) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

ни это

switch(2) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

Обновить:

-5 вопрос стал -3,

3 ответа

Решение

Ни один из них не должен компилироваться. Спецификация C# требует, чтобы в разделе switch было хотя бы одно утверждение. Парсер должен запретить это.

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

В вашем последнем примере секция коммутатора имеет достижимую конечную точку:

void M(int x) { switch(2) { case 2: ; } }

так что должно быть ошибка.

Если у тебя есть:

void M(int x) { switch(x) { case 2: ; } }

тогда компилятор не знает, будет ли x когда-либо равен 2. Он консервативно предполагает, что может, и говорит, что раздел имеет достижимую конечную точку, потому что метка регистра переключателя достижима.

Если у тебя есть

void M(int x) { switch(1) { case 2: ; } }

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

Если у тебя есть:

void M(int x) { switch(x = 1) { case 2: ; } }

или же

void M(int x) { x = 1; switch(x) { case 2: ; } }

Тогда вы знаете, и я знаю, что конечная точка недостижима, но компилятор этого не знает. Правило в спецификации заключается в том, что достижимость определяется только путем анализа константных выражений. Любое выражение, которое содержит переменную, даже если вы знаете ее значение каким-либо другим способом, не является константным выражением.

В прошлом у компилятора C# были ошибки, когда это было не так. Вы могли бы сказать такие вещи, как:

void M(int x) { switch(x * 0) { case 2: ; } }

и компилятор будет исходить из того, что x * 0 должно быть равно 0, поэтому метка регистра недоступна. Это была ошибка, которую я исправил в C# 3.0. В спецификации сказано, что для этого анализа используются только константы, и x это переменная, а не константа.

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

void M(int x) { if (x * 0 == 0) Y(); }

Затем компилятор может сгенерировать код, как если бы вы написали

void M(int x) { Y(); }

если захочет. Но он не может использовать тот факт, что x * 0 == 0 верно для целей определения достижимости заявления.

Наконец, если у вас есть

void M(int x) { if (false) switch(x) { case 2: ; } }

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

void M(int x) { if (x * 0 != 0) switch(x) { case 2: ; } }

не лечит x * 0 != 0 как falseТаким образом, конечная точка считается достижимой.

В Visual Studio 2012 причина первого очевидна. Компилятор определяет, что код недоступен:

switch (1)
{
    case 2:
}

Предупреждение. Обнаружен недоступный код.

В двух других случаях компилятор сообщает: "Элемент управления не может перейти от одной метки регистра (" case 2: ") к другой". Я не вижу слова "('case 1')" ни в одном из неудачных случаев.

Я думаю, что компилятор просто не агрессивен в отношении постоянной оценки. Например, следующее эквивалентно:

int i;
switch(i=1)
{
    case 2:
}

а также

int i = 1;
switch(i)
{
    case 2:
}

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

switch (1)
{
    case 2:
}

И определите, что код недоступен.

Я подозреваю, что ответ "почему этот компилятор" будет "потому что мы позволяем JIT-компилятору обрабатывать агрессивную оптимизацию".

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

static void withoutVar()
{
    Console.WriteLine("Before!");

    switch (1)
    {
        case 2:
    }

    Console.WriteLine("After!");
}

Который при декомпиляции с ILSpy показывает нам этот IL:

.method private hidebysig static 
    void withoutVar () cil managed 
{
    // Method begins at RVA 0x2053
    // Code size 26 (0x1a)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldstr "Before!"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: br.s IL_000e

    IL_000e: ldstr "After!"
    IL_0013: call void [mscorlib]System.Console::WriteLine(string)
    IL_0018: nop
    IL_0019: ret
} // end of method Program::withoutVar

Который нигде не помнит оператора switch. Я думаю, что причина, по которой он не оптимизирует и второй, может быть связана с перегрузкой операторов и тому подобным. Таким образом, вполне возможно, что у меня есть пользовательский тип, который при назначении 1превращается в 2, Тем не менее, я не совсем уверен, мне кажется, что отчет об ошибке должен быть представлен.

Другие вопросы по тегам