Kurcze nie lubię słów inkrementacja/dekrementacja po polsku, zresztą chyba naprawdę nie istnieją w słowniku języka polskiego, jednak ich znaczenie chyba każdy z nas rozumie, chodzi oczywiście o operatory ++ i — dostępne w C# (i nie tylko).
Większość z nas zna ich działanie, czyli dla przykładu:
int x = 0; int y = 0; Console.WriteLine("x++:t{0}", x++); Console.WriteLine("x:t{0}", x); Console.WriteLine("++y:t{0}", ++y); Console.WriteLine("y:t{0}", y);
Wynik będzie następujący:
Analogicznie zachowa się oczywiście operator –, ale dla skrócenia zbędnego kodu nie będę tego „pokazywał”.
Jednakże pomimo wiedzy jakie rezultaty powinien kod zwrócić, nie zawsze dokładnie je rozumiemy, jak pokazał to przykład Ayende – choć szczerze mówiąc, nie ma co się dziwić, o pewnych rzeczach się po prostu zapomina, lub nie zwraca uwagi a potem nagle kupujemy zgrzewkę tigera i przez noc szukamy błędu bo rano klient chce zobaczyć działającą aplikację :)
Nie ma nic złego w tym, że czasami się pomylimy, gdyby to było złe to każdą linijkę w javascript bym rozpoczynał swojego czasu od średnika i każdego ifa od alertu ze względu na tego średnika co mógł mi skasować ifa :)
Ale nie o tym miało być :)
Operatory ++ i — w zależności od tego czy są stosowane pre czy post mają różne znaczenia i są różnie traktowane przez kompilator generujący kod IL. Różnica jednak dopiero jest widoczna kiedy chcemy od razu wykonać jakieś działanie na naszej zmiennej, to znaczy, że na przykład przy operacji x++ chcemy od razu ją wyświetlić Console.WriteLine(x++);, w przeciwnym wypadku, kod IL wygenerowany dla ++x i dla x++ niczym się nie różni i ma postać (w nawiasach informacje o stanie stosu po wykonaniu operacji):
static void noDiffPost() { int x = 0; x++; } static void noDiffPre() { int x = 0; ++x; }
- Załaduj wartość 0 na stos (stos: 0);
- Załaduj element na stosie do zmiennej x (stos: EMPTY);
- Załaduj wartość zmiennej x na stos (stos: 0);
- Załaduj wartość 1 na stos (stos++: 0, 1, stos–: 0, -1);
- Wykonaj operację dodania (przy ++) lub odejmowania (–) na dwóch elementach stosu i dodaj wynik działania na stos (stos++: 1, stos–:-1);
- Załaduj element na stosie do zmiennej x czyli w wypadku ++ to 1 a w wypadku — to -1 (stos: EMPTY).
Koniec, operacja jest dość prosta, zmiana jednak następuje kiedy chcemy od razu zmienną wykorzystać (na przykład wyświetlić Console.WriteLine(++x | x++);, w tym momencie, kod dla x++ ma następujące kroki:
static void withConsolePost() { int x = 0; Console.WriteLine(x++); }
- Załaduj wartość 0 na stos (stos: 0);
- Załaduj element na stosie do zmiennej x (stos: EMPTY);
- Załaduj wartość zmiennej x na stos (stos: 0);
- Zduplikuj wartość na stosie (stos: 0, 0);
- Załaduj wartość 1 na stos (stos: 0, 0, 1);
- Wykonaj operację add na dwóch elementach stosu i dodaj wynik działania na stos (stos: 0, 1);
- Załaduj element na stosie do zmiennej x (stos: 0, x: 1);
- Wypisz ostatni element na stosie czyli w tym wypadku 0.
Zaś dla ++x następujące:
static void withConsolePre() { int x = 0; Console.WriteLine(++x); }
- Załaduj wartość 0 na stos (stos: 0);
- Załaduj element na stosie do zmiennej x (stos: EMPTY);
- Załaduj wartość zmiennej x na stos (stos: 0);
- Załaduj wartość 1 na stos (stos: 0, 1);
- Wykonaj operację add na dwóch elementach stosu i dodaj wynik działania na stos (stos: 1);
- Zduplikuj wartość na stosie (stos: 1, 1);
- Załaduj element na stosie do zmiennej x (stos: 1, x: 1);
- Wypisz ostatni element na stosie czyli w tym wypadku 0.
Dużo łatwiej jest to sobie zobrazować na kartce papieru, dla każdego kroku dodając lub zdejmując elementy ze stosu. Ale widać jedną wyraźną różnicę – miejsce duplikacji wartości, jedno duplikuje wartość początkową, drugie wartość wyniku dodawania (dosłownie, komenda IL_0006 dup w pre, znajduje się na pozycji IL_0004 w post). Normalnie taka zasada działa, wyjątkiem jest jedno z równań (należących do tak zwanych zachować nieokreślonych przez specyfikację języka):
x = x++;
w którym punkt 8 opisu działania x++ brzmi tak:
Załaduj element na stosie do zmiennej x, czyli w tym wypadku 0.
static void Undefined() { int x = 0; x = x++; }
Tak jak poprzednio, tworzę zmienną lokalną, więc pierwsze linijki to jej inicjalizacja.
Dziwne ale IMO poprawne/niepoprawne (nie określone) zachowanie, zresztą opisane tutaj, zaś także do znalezienia u Ayende na blogu jako „zagadkę/interview question”.
Podobny przypadek występuje kiedy zwracamy x++ (tym razem IMO jest to określone zachowanie ;)):
static int NotSoUndefined() { int x = 0; return x++; }
Teraz skoro już wiemy jaki powinien być wynik i dlaczego tak jest :) to możemy przejść do pętli. Załóżmy przypadek dość prosty:
static void ForLoopPost() { for(int x = 0; x < 10; x++) { Console.WriteLine(x); } }
To chyba każdy się domyśli jaki będzie tego wynik – od 0 do 9 włącznie.
Teraz, co się stanie kiedy zmienimy sposób inkrementacji w pętli for (zasłońcie sobie powyższy kod by lepiej widzieć ten poniżej, serio)?
static void ForLoopPre() { for(int x = 0; x < 10; ++x) { Console.WriteLine(x); } }
Z godnie z tym co wiemy możemy przypuszczać iż wynik będzie inny, od 1 do 9 włącznie. Jeżeli zasłoniliście sobie kod powyżej to nawet mogliście przypuszczać iż tak naprawdę jest, jeżeli nie to może coś w was drgnęło :)
Oczywiście wynik jaki dostaniemy jest taki sam jak w poprzednim przypadku od 0 do 9. Łatwo jest się domyśleć kiedy ma się dwa źródła przed sobą, w końcu robimy i++ a następnie wykorzystujemy Console.WriteLine(i) i wartość i jest 0 a nie 1, choć zgodnie z tym co wiemy też powinna być 1, tak jak w przypadku z ++x :)
Dzieje się tak gdyż pętla for zamieniana jest tak naprawdę na pętle while i wygląda to mniej więcej tak:
- Utwórz zmienną x i przypisz do niej 0;
- Sprawdź czy warunek x < 10 jest prawdziwy, jeżeli tak to idź do punktu 3, w przeciwnym wypadku zakończ;
- Wykonaj Console.WriteLine(x);
- Zwiększ x o 1;
- Idź do punktu 2.
IL dla While Pre i Post:
static void WhileLoopPost() { int x = 0; while(x < 10) { Console.WriteLine(x); x++; } }
static void WhileLoopPre() { int x = 0; while(x < 10) { Console.WriteLine(x); ++x; } }
IL dla for pre i post (kod wklejony był już wcześniej, więc tylko IL):
Jedyną różnicą pomiędzy tymi kodami to wystąpienie komendy nop – w for, znajduje się ona na pozycji IL_000d zaś w while na pozycji IL_0011.
Więc nie ma znaczenia jaki rodzaj inkrementacji/dekrementacji zastosujemy w pętli for, możemy równie dobrze wykorzystać ++i (pre) jak i i++ (post).
Mam nadzieję, że to wyjaśniło wszelkie niejasności związane z pre i post inkrementacją/dekrementacją zarówno jako samoczynnej operacji jak i operacji zwiększania licznika pętli. Czy coś pominąłem? O czymś nie wspomniałem? Coś napisałem nie jasno? A może uważacie, że ten cały post to jest o d$%# potłuc bo każdy to w końcu wie? :)
PS.: do przeglądania kodu IL polecam ILDASM, .NET Reflector czasami ciutkę inaczej tłumaczy/wypisuje kod a tak to przynajmniej macie „pewność” iż tak jak jest wypisane tak to wygląda.
Nie wiem czy to ja ślepy, ale dla mnie obie pętle (ForLoopPre i ForLoopPost) mają w taki sam sposób inkrementację (w obu przypadkach w kodzie jest ++x – zasłaniałem jak autor przykazał :)).
Poza tym ciekawy post – lubię takie "niskopoziomowe" sprawy :)
Pozdrawiam,
Pawęł
laaa dzieki :) rzeczywiscie :) upsnelo mi sie. Ale nie ma roznicy, serio :) zaktualizje go wiec dzisiaj wieczorem ;) dzieki! :)
jest jeszcze jeden bug ale go juz poprawiac nie bede. screenshoty pokazuja deklaracje zmiennej i zas kod zmiennej x, jest to spowodowane tym ze najpierw robilem screenshoty a potem poprawialem kod do artu z i na x (jakos latwiej sie czytalo x++ niz i++ i mi word nie robil z i I :)). Sorki za to
dobra poprawione, zarowno ForLoopPost ;) jak i screenshoty by zawieraly X a nie I zmienna
I cały długi post o tak oczywistej sprawie ???
Ludziom to się nudzi :-)
czy jest jakiś język, gdzie ostatnia sekcja fora, for(;; o_ta!) jest wykonywana kiedy indziej jak nie już po wykonaniu ciała pętli i nie tuż przed sprawdzeniem warunku? Jest to osobna instrukcja, więc jakakolwiek kolejność jej (wewnętrznych operacji) by nie była, sama instrukcja jest niejako atomowa… więc czy ja nie widzę głębi tego problemu?
Comments are closed.