Aby stwierdzić problem, Dangling Else Problem to dwuznaczność w specyfikacji składni kodu, w której może być niejasny, w przypadku następnych ifs i els, do których jeszcze należy.
Najprostszy i klasyczny przykład:
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
Jest to niejasne dla tych, którzy nie znają na pamięć specyfiki języka, który if
otrzymuje else
(a ten konkretny fragment kodu jest poprawny w pół tuzina języków, ale może działać inaczej w każdym z nich).
Konstrukcja Dangling Else stanowi potencjalny problem dla implementacji analizatora składni bez skanera, ponieważ strategia polega na zarzucaniu strumienia plików po jednym znaku na raz, dopóki analizator składni nie zobaczy, że ma on wystarczająco dużo do tokenizacji (trawienie w asemblerze lub języku pośrednim, który kompiluje) . Umożliwia to parserowi utrzymanie stanu minimalnego; gdy tylko uzna, że ma wystarczającą ilość informacji, aby zapisać tokeny, które jest parsowane do pliku, zrobi to. To jest końcowy cel parsera bez skanera; szybka, prosta, lekka kompilacja.
Zakładając, że znaki nowej linii i białe znaki przed lub po interpunkcji są bez znaczenia (jak w większości języków w stylu C), to stwierdzenie wydaje się kompilatorowi jako:
if(conditionA)if(conditionB)doFoo();else doBar;
Doskonale parsowalny do komputera, więc zobaczmy. Dostaję jedną postać na raz, dopóki nie będę:
if(conditionA)
Och, wiem, co to oznacza (w języku C #), oznacza to „ push
warunekA na stosie ewaluacji, a następnie wywołanie, brfalse
aby przejść do instrukcji po następnym średniku, jeśli nie jest to prawda”. W tej chwili nie widzę średnika, więc na razie ustawię przesunięcie skoku do następnej spacji po tej instrukcji i zwiększę to przesunięcie, gdy wstawię więcej instrukcji, aż zobaczę średnik. Kontynuowanie analizy ...
if(conditionB)
OK, to analizuje podobną parę operacji IL i następuje natychmiast po instrukcji, którą właśnie przeanalizowałem. Nie widzę średnika, więc zwiększę przesunięcie skoku mojej poprzedniej instrukcji o długość moich dwóch poleceń (jednego dla push i jeden dla breaka) i nadal szukam.
doFoo();
Ok, to proste. To jest „ call
doFoo”. I czy to jest średnik, który widzę? To wspaniale, to koniec linii. Zwiększę przesunięcia obu bloków o długość tych dwóch poleceń i zapomnę, że kiedykolwiek mnie to obchodziło. OK, kontynuuję ...
else
... O o. To nie jest tak proste, jak się wydawało. OK, zapomniałem, co właśnie robiłem, ale else
oznacza to, że gdzieś już widziałem warunkowe polecenie przerwania, więc pozwól mi spojrzeć wstecz ... tak, brfalse
oto jest , zaraz po tym, jak włączyłem „warunek B” stos, cokolwiek to było. OK, teraz potrzebuję bezwarunkowego break
jako następnego oświadczenia. Stwierdzenie, które nastąpi później, jest teraz zdecydowanie celem mojego warunkowego przerwania, więc upewnię się, że mam rację, i zwiększę bezwarunkową przerwę, którą wprowadziłem. Przechodząc ...
doBar();
To łatwe. „ call
doBar”. I jest średnik i nigdy nie widziałem żadnych aparatów ortodontycznych. Zatem bezwarunkowy break
powinien przejść do następnego stwierdzenia, cokolwiek to jest, i mogę zapomnieć, że kiedykolwiek mnie to obchodziło.
A więc, co mamy ... (uwaga: jest 22:00 i nie mam ochoty konwertować offsetów bitowych na szesnastkowy lub wypełniać pełnej powłoki IL funkcji za pomocą tych poleceń, więc to tylko pseudo-IL przy użyciu numerów linii, w których zwykle byłyby przesunięcia bajtów):
ldarg.1 //conditionA
brfalse <line 6> //jumps to "break"
ldarg.2 //conditionB
brfalse <line 7> //jumps to "call doBar"
call doFoo
break <line 8> //jumps beyond statement in scope
call doBar
<line 8 is here>
Cóż, to faktycznie działa poprawnie, JEŻELI reguła (jak w większości języków w stylu C) jest taka, że else
idzie z najbliższym if
. Wciśnięty, aby śledzić zagnieżdżanie wykonania, działałby w ten sposób, w przypadku gdy warunek A jest fałszywy, cała pozostała część fragmentu kodu jest pomijana:
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
... ale robi to przypadkowo, ponieważ przerwa związana z instrukcją zewnętrzną if
przeskakuje do break
instrukcji na końcu instrukcji wewnętrznej if
, co powoduje, że wskaźnik wykonania wykracza poza całą instrukcję. Jest to dodatkowy niepotrzebny skok, a jeśli ten przykład byłby bardziej złożony, mógłby przestać działać, jeśli zostałby parsowany i tokenizowany w ten sposób.
A co, jeśli specyfikacja języka mówi, że zwisanie else
należy do pierwszego if
, a jeśli warunek A jest fałszywy, to wykonywany jest doBar, a jeśli warunek A jest prawdziwy, ale nie warunek B, to nic się nie dzieje?
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
Analizator składni zapomniał o istnieniu pierwszego if
, a zatem ten prosty algorytm analizatora składni nie wygenerowałby poprawnego kodu, nie mówiąc już o wydajnym kodzie.
Teraz parser może być wystarczająco inteligentny, aby zapamiętać if
s i else
s przez dłuższy czas, ale jeśli specyfikacja języka mówi, że pojedynczy else
po dwóch if
s pasuje do pierwszego if
, to powoduje problem z dwoma if
s z dopasowaniem else
s:
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
else
doBaz();
Parser zobaczy pierwszy else
, dopasuje do pierwszego if
, a następnie zobaczy drugi i przejdzie w panikę w trybie „co do diabła robiłem ponownie”. W tym momencie parser ma dość dużo kodu w stanie umożliwiającym modyfikację, który wolałby już wypchnąć do wyjściowego strumienia plików.
Istnieją rozwiązania wszystkich tych problemów i co-jeśli. Ale albo kod musi być taki, że smart zwiększa złożoność algorytmu analizatora składni, lub specyfikacja języka pozwalająca analizatorowi na tak niemądry zwiększa szczegółowość kodu źródłowego języka, na przykład poprzez wymaganie zakończenia instrukcji typu end if
lub nawiasów wskazujących zagnieżdżenie blokuje, jeśli if
instrukcja ma else
(oba są powszechnie widoczne w innych stylach językowych).
To tylko jeden, prosty przykład kilku if
instrukcji, i spójrz na wszystkie decyzje, które musiał podjąć kompilator, i gdzie i tak mógł się łatwo pomylić. Taki jest szczegół tego niewinnego oświadczenia Wikipedii w twoim pytaniu.