Что компилятор думает о выражении 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
, Тем не менее, я не совсем уверен, мне кажется, что отчет об ошибке должен быть представлен.