Jaka jest różnica między i ++ a ++ i?

204

Widziałem je zarówno wykorzystywane w wielu kawałków kodu C # i chciałbym wiedzieć kiedy użyć i++lub ++i( ibędąc liczba zmienna jak int, float, doubleitp). Czy ktoś to wie?

Dlaor
źródło
13
Z wyjątkiem przypadków, gdy nie ma to żadnej różnicy, nigdy nie powinieneś używać żadnego z nich, ponieważ spowoduje to, że ludzie zadadzą to samo pytanie, które zadajesz teraz. „Nie każ mi myśleć” dotyczy zarówno kodu, jak i projektu.
Instance Hunter
2
@Dlaor: Czy przeczytałeś nawet linki w moim komentarzu? Pierwszy dotyczy C #, a drugi jest niezależny od języka z akceptowaną odpowiedzią, która koncentruje się na C #.
gnovice
7
@gnovice, pierwszy pyta o różnicę wydajności, podczas gdy ja pytam o rzeczywistą różnicę kodową, drugi dotyczy różnicy w pętli, podczas gdy ja pytam o różnicę w ogóle, a trzeci dotyczy C ++.
Dlaor
1
Uważam, że nie jest to duplikat różnicy między i ++ i ++ i w pętli? - jak Dloar mówi w swoim komentarzu powyżej, inne pytanie dotyczy konkretnie użycia w pętli.
chue x

Odpowiedzi:

201

Co dziwne, wygląda na to, że dwie pozostałe odpowiedzi tego nie przeliterowały i zdecydowanie warto powiedzieć:


i++oznacza „powiedz mi wartość i, a następnie przyrost”

++ioznacza „przyrost i, a następnie powiedz mi wartość”


Są to operatorzy działający przed inkrementacją, po inkrementacji. W obu przypadkach zmienna jest zwiększana , ale jeśli weźmiesz wartość obu wyrażeń w dokładnie tych samych przypadkach, wynik będzie inny.

Kieren Johnstone
źródło
11
Wydaje się to sprzeczne z tym, co mówi Eric
Evan Carroll
65
Żadne z tych stwierdzeń nie jest poprawne. Rozważ swoje pierwsze zdanie. i ++ w rzeczywistości oznacza „zapisz wartość, zwiększ ją, zapisz w i, a następnie powiedz mi oryginalną zapisaną wartość”. Oznacza to, że opowiadanie odbywa się po inkrementacji, a nie wcześniej, jak to powiedziałeś. Rozważ drugie zdanie. i ++ faktycznie oznacza „zapisz wartość, zwiększ ją, zapisz w i, i powiedz mi wartość przyrostową”. Sposób, w jaki to powiedziałeś, sprawia, że ​​nie jest jasne, czy wartość jest wartością i, czy wartością, która została przypisana do i; mogą być inni .
Eric Lippert,
8
@Eric, wydaje mi się, że nawet z twoją odpowiedzią, drugie stwierdzenie jest kategorycznie poprawne. Chociaż wyklucza kilka kroków.
Evan Carroll
20
Oczywiście, przez większość czasu niechlujne, niepoprawne sposoby opisu semantyki operacyjnej dają takie same wyniki jak dokładny i poprawny opis. Po pierwsze, nie widzę żadnej istotnej wartości w uzyskiwaniu właściwych odpowiedzi przez nieprawidłowe rozumowanie, a po drugie, tak, widziałem kod produkcyjny, który źle robi tego rodzaju rzeczy. Dostaję prawdopodobnie pół tuzina pytań od prawdziwych programistów co roku, dlaczego niektóre wyrażenia pełne przyrostów i dekrecji oraz dereferencji tablicowych nie działają tak, jak zakładali.
Eric Lippert
12
@supercat: Dyskusja dotyczy C #, a nie C.
Thanatos
428

Typowa odpowiedź na to pytanie, niestety zamieszczone już tutaj, jest taka, że ​​jeden wykonuje przyrost „przed” pozostałymi operacjami, a drugi wykonuje przyrost „po” pozostałych operacjach. Choć intuicyjnie prowadzi to do pomysłu, to stwierdzenie jest całkowicie błędne . Sekwencja wydarzeń w czasie jest bardzo dobrze zdefiniowane w C #, a to zdecydowanie nie przypadek, że przedrostek (++ var) i postfix (var ++) wersje ++ robić rzeczy w innej kolejności w stosunku do innych operacji.

Nic dziwnego, że zobaczysz wiele błędnych odpowiedzi na to pytanie. Wiele książek „nauczysz się języka C #” również się myli. Ponadto sposób, w jaki C # robi, jest inny niż w C. Wiele osób uważa, że ​​C # i C są tym samym językiem; oni nie są. Projekt operatorów inkrementacji i dekrementacji w C # moim zdaniem pozwala uniknąć wad projektowych tych operatorów w C.

Istnieją dwa pytania, na które należy odpowiedzieć, aby ustalić, jakie dokładnie działanie przedrostka i Postfiksa ++ znajdują się w języku C #. Pierwsze pytanie brzmi: jaki jest wynik? a drugie pytanie dotyczy tego, kiedy ma miejsce efekt uboczny przyrostu?

Nie jest oczywiste, jaka jest odpowiedź na którekolwiek z tych pytań, ale w rzeczywistości jest to dość proste, gdy je zobaczysz. Pozwól, że wyjaśnię ci dokładnie, co x ++ i ++ x robią dla zmiennej x.

W przypadku formularza prefiksu (++ x):

  1. x jest obliczane w celu utworzenia zmiennej
  2. Wartość zmiennej jest kopiowana do lokalizacji tymczasowej
  3. Wartość tymczasowa jest zwiększana, aby utworzyć nową wartość (nie zastępując wartości tymczasowej!)
  4. Nowa wartość jest przechowywana w zmiennej
  5. Wynikiem operacji jest nowa wartość (tj. Wartość przyrostowa wartości tymczasowej)

W przypadku formularza Postfiks (x ++):

  1. x jest obliczane w celu utworzenia zmiennej
  2. Wartość zmiennej jest kopiowana do lokalizacji tymczasowej
  3. Wartość tymczasowa jest zwiększana, aby utworzyć nową wartość (nie zastępując wartości tymczasowej!)
  4. Nowa wartość jest przechowywana w zmiennej
  5. Wynikiem operacji jest wartość tymczasowa

Niektóre rzeczy do zauważenia:

Po pierwsze, kolejność zdarzeń w czasie jest dokładnie taka sama w obu przypadkach . Ponownie absolutnie nie jest tak, że kolejność zdarzeń w czasie zmienia się między prefiksem i postfiksem. Całkowicie fałszywe jest twierdzenie, że ocena odbywa się przed innymi ocenami lub po innych ocenach. Oceny odbywają się w dokładnie takiej samej kolejności w obu przypadkach, co widać w krokach od 1 do 4 jako identyczne. Tylko różnicą jest to ostatni krok - czy wynik jest wartość tymczasowa lub nowym, zwiększonym wartości.

Możesz to łatwo zademonstrować za pomocą prostej aplikacji na konsolę C #:

public class Application
{
    public static int currentValue = 0;

    public static void Main()
    {
        Console.WriteLine("Test 1: ++x");
        (++currentValue).TestMethod();

        Console.WriteLine("\nTest 2: x++");
        (currentValue++).TestMethod();

        Console.WriteLine("\nTest 3: ++x");
        (++currentValue).TestMethod();

        Console.ReadKey();
    }
}

public static class ExtensionMethods 
{
    public static void TestMethod(this int passedInValue) 
    {
        Console.WriteLine("Current:{0} Passed-in:{1}",
            Application.currentValue,
            passedInValue);
    }
}

Oto wyniki...

Test 1: ++x
Current:1 Passed-in:1

Test 2: x++
Current:2 Passed-in:1

Test 3: ++x
Current:3 Passed-in:3

W pierwszym teście możesz zobaczyć, że zarówno to, jak currentValuei to, co zostało przekazane do TestMethod()rozszerzenia, pokazuje tę samą wartość, zgodnie z oczekiwaniami.

Jednak w drugim przypadku ludzie będą próbować powiedzieć, że wzrost currentValuenastępuje po połączeniu z TestMethod(), ale jak widać z wyników, dzieje się to przed połączeniem, jak wskazuje wynik „Bieżący: 2”.

W takim przypadku najpierw wartość currentValuejest przechowywana jako tymczasowa. Następnie przyrostowa wersja tej wartości jest przechowywana z powrotem, currentValueale bez dotykania wartości tymczasowej, która nadal przechowuje oryginalną wartość. W końcu ten tymczasowy jest przekazywany do TestMethod(). Jeśli przyrost nastąpił po wywołaniu do TestMethod(), zapisałby dwukrotnie tę samą niekrementowaną wartość, ale tak nie jest.

Ważne jest, aby pamiętać, że wartość zwracana zarówno z operacji, jak currentValue++i ++currentValueopiera się na wartości tymczasowej, a nie rzeczywistej przechowywanej w zmiennej w momencie zakończenia którejkolwiek operacji.

Przywołaj w powyższej kolejności operacji, dwa pierwsze kroki kopiują bieżącą wartość zmiennej do wartości tymczasowej. Właśnie tego używa się do obliczenia wartości zwracanej; w przypadku wersji przedrostkowej jest to wartość tymczasowa zwiększana, podczas gdy w przypadku wersji przedrostkowej jest to wartość bezpośrednio / nie zwiększana. Sama zmienna nie jest odczytywana ponownie po początkowym zapisaniu w pamięci tymczasowej.

Mówiąc prościej, wersja postfiksowa zwraca wartość odczytaną ze zmiennej (tj. Wartość tymczasową), podczas gdy wersja prefiksowa zwraca wartość zapisaną z powrotem do zmiennej (tj. Wartość przyrostową wartości tymczasowej). Ani nie zwracaj wartości zmiennej.

Jest to ważne, aby zrozumieć, ponieważ sama zmienna może być niestabilna i uległa zmianie w innym wątku, co oznacza, że ​​wartość zwrotna tych operacji może różnić się od bieżącej wartości przechowywanej w zmiennej.

Zaskakujące jest, że ludzie często są zdezorientowani co do pierwszeństwa, asocjatywności i kolejności wykonywania działań niepożądanych, podejrzewam, że głównie dlatego, że jest tak mylący w C. C # został starannie zaprojektowany, aby był mniej mylący pod każdym względem. Aby uzyskać dodatkową analizę tych problemów, w tym moją dalszą demonstrację fałszywości idei, że operacje na prefiksach i postfiksach „przenoszą rzeczy w czasie”, patrz:

https://ericlippert.com/2009/08/10/precedence-vs-order-redux/

, które doprowadziły do ​​tego pytania SO:

int [] arr = {0}; int wartość = arr [arr [0] ++]; Wartość = 1?

Być może zainteresują Cię moje poprzednie artykuły na ten temat:

https://ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order/

i

https://ericlippert.com/2007/08/14/c-and-the-pit-of-despair/

oraz ciekawy przypadek, w którym C utrudnia uzasadnienie poprawności:

https://docs.microsoft.com/archive/blogs/ericlippert/bad-recursion-revisited

Ponadto napotykamy podobne subtelne problemy, gdy rozważamy inne operacje, które mają skutki uboczne, takie jak powiązane proste zadania:

https://docs.microsoft.com/archive/blogs/ericlippert/chaining-simple-assignments-is-not-so-simple

A oto ciekawy post na temat tego, dlaczego operatory przyrostowe dają wartości w C # zamiast w zmiennych :

Dlaczego nie mogę zrobić ++ i ++ w językach podobnych do C?

Eric Lippert
źródło
4
+1: Czy dla naprawdę ciekawych mógłbyś podać odniesienie do tego, co rozumiesz przez „wady projektowe tych operacji w C”?
Justin Ardini
21
@Justin: Dodałem kilka linków. Ale w gruncie rzeczy: w C nie masz żadnych gwarancji co do kolejności, w jakiej rzeczy dzieją się na czas. Kompilator zgodny może zrobić wszystko, co mu się podoba, gdy w tym samym punkcie sekwencji występują dwie mutacje i nigdy nie trzeba mówić, że robisz coś, co jest zachowaniem zdefiniowanym w implementacji. Prowadzi to ludzi do pisania niebezpiecznie nieeksportowalnego kodu, który działa na niektórych kompilatorach, a na innych robi coś zupełnie innego.
Eric Lippert,
14
Muszę powiedzieć, że dla naprawdę ciekawych jest to dobra znajomość, ale w przypadku przeciętnej aplikacji w języku C # różnica między sformułowaniami w innych odpowiedziach a rzeczywistymi sprawami jest o wiele poniżej poziomu abstrakcji języka, który naprawdę sprawia, że bez różnicy. C # nie jest asemblerem, a spośród 99,9% razy i++lub ++isą one używane w kodzie, rzeczy działające w tle są właśnie takie; w tle . Piszę C #, aby wspiąć się na poziomy abstrakcji powyżej tego, co dzieje się na tym poziomie, więc jeśli to naprawdę ma znaczenie dla twojego kodu C #, możesz już być w złym języku.
Tomas Aschan
24
@Tomas: Po pierwsze, martwię się o wszystkie aplikacje C #, a nie tylko o masę przeciętnych aplikacji. Liczba nietypowych aplikacji jest duża. Po drugie, nie robi różnicy, z wyjątkiem przypadków, w których robi różnicę . Dostaję pytania o te rzeczy oparte na prawdziwych błędach w prawdziwym kodzie, prawdopodobnie pół tuzina razy w roku. Po trzecie, zgadzam się z twoim większym punktem; cała idea ++ jest pomysłem bardzo niskiego poziomu, który wygląda bardzo osobliwie we współczesnym kodzie. Jest tak naprawdę tylko dlatego, że takie operatora ma idiomatyczne języki podobne do C.
Eric Lippert,
11
+1, ale muszę powiedzieć prawdę ... To lata, w których jedyne użycie operatorów postfiksów, które robię, nie jest samotne z rzędu (więc tak nie i++;jest) w for (int i = 0; i < x; i++)... I jestem bardzo, bardzo szczęśliwy z tego! (i nigdy nie używam operatora prefiksu). Jeśli będę musiał napisać coś, co będzie wymagało starszego programisty 2 minuty na rozszyfrowanie ... Cóż ... Lepiej napisać jeszcze jedną linię kodu lub wprowadzić tymczasową zmienną :-) Myślę, że twój „artykuł” (wygrałem nazywam to „odpowiedzią”) potwierdza mój wybór :-)
Xanatos
23

Jeśli masz:

int i = 10;
int x = ++i;

wtedy xbędzie 11.

Ale jeśli masz:

int i = 10;
int x = i++;

wtedy xbędzie 10.

Uwaga: jak wskazuje Eric, przyrost występuje w obu przypadkach jednocześnie, ale to, jaka wartość jest podana jako wynik, różni się (dzięki Eric!).

Ogólnie lubię używać, ++ichyba że istnieje dobry powód, aby tego nie robić. Na przykład, pisząc pętlę, lubię używać:

for (int i = 0; i < 10; ++i) {
}

Lub, jeśli muszę tylko zwiększyć zmienną, lubię używać:

++x;

Zwykle jeden lub drugi sposób nie ma większego znaczenia i sprowadza się do stylu kodowania, ale jeśli używasz operatorów wewnątrz innych zadań (jak w moich oryginalnych przykładach), ważne jest, aby zdawać sobie sprawę z potencjalnych skutków ubocznych.

dcp
źródło
2
Prawdopodobnie możesz wyjaśnić swoje przykłady, porządkując linie kodu - to znaczy powiedz „var x = 10; var y = ++ x;” i „var x = 10; var y = x ++;” zamiast.
Tomas Aschan
21
Ta odpowiedź jest niebezpiecznie błędna. Kiedy inkrement nastąpi, nie zmienia się w stosunku do pozostałych operacji, dlatego powiedzenie, że w jednym przypadku odbywa się to przed pozostałymi operacjami, a w drugim przypadku następuje po pozostałych operacjach, jest głęboko mylące. Przyrost jest wykonywany w obu przypadkach jednocześnie . Inną rzeczą jest to, jaką wartość podaje się jako wynik , a nie po wykonaniu przyrostu .
Eric Lippert
3
Zedytowałem to, aby użyć inazwy zmiennej, a nie varjako słowa kluczowego C #.
Jeff Yates
2
Więc bez przypisywania zmiennych i po prostu wpisywania „i ++;” lub „++ i;” da dokładnie takie same wyniki, czy popełniam błąd?
Dlaor
1
@Eric: Czy możesz wyjaśnić, dlaczego oryginalne sformułowanie jest „niebezpieczne”? Prowadzi to do prawidłowego użytkowania, nawet jeśli szczegóły są błędne.
Justin Ardini
7
int i = 0;
Console.WriteLine(i++); // Prints 0. Then value of "i" becomes 1.
Console.WriteLine(--i); // Value of "i" becomes 0. Then prints 0.

Czy to odpowiada na twoje pytanie?

thelost
źródło
4
Można sobie wyobrazić, że przyrost porostku i zmniejszenie przedrostka nie mają żadnego wpływu na zmienną: P
Andreas
5

Sposób działania operatora polega na tym, że jest on zwiększany w tym samym czasie, ale jeśli znajduje się przed zmienną, wyrażenie będzie oceniać za pomocą zmiennej zwiększającej / zmniejszającej:

int x = 0;   //x is 0
int y = ++x; //x is 1 and y is 1

Jeśli jest za zmienną, bieżąca instrukcja zostanie wykonana z oryginalną zmienną, tak jakby nie była jeszcze zwiększana / zmniejszana:

int x = 0;   //x is 0
int y = x++; //'y = x' is evaluated with x=0, but x is still incremented. So, x is 1, but y is 0

Zgadzam się z dcp w używaniu wstępnego zwiększania / zmniejszania (++ x), chyba że jest to konieczne. Naprawdę jedyny raz, kiedy używam przyrostowego / malejącego czasu, to pętle while lub pętle tego rodzaju. Te pętle są takie same:

while (x < 5)  //evaluates conditional statement
{
    //some code
    ++x;       //increments x
}

lub

while (x++ < 5) //evaluates conditional statement with x value before increment, and x is incremented
{
    //some code
}

Możesz to również zrobić podczas indeksowania tablic i takie:

int i = 0;
int[] MyArray = new int[2];
MyArray[i++] = 1234; //sets array at index 0 to '1234' and i is incremented
MyArray[i] = 5678;   //sets array at index 1 to '5678'
int temp = MyArray[--i]; //temp is 1234 (becasue of pre-decrement);

Itd itd...

Mike Webb
źródło
4

Dla przypomnienia, w C ++, jeśli możesz użyć albo (tj.), Nie przejmujesz się kolejnością operacji (chcesz tylko zwiększyć lub zmniejszyć i użyć go później), operator prefiksu jest bardziej wydajny, ponieważ nie trzeba utworzyć tymczasową kopię obiektu. Niestety większość ludzi używa posfiksu (var ++) zamiast przedrostka (++ var), tylko dlatego, że nauczyliśmy się tego na początku. (Zapytano mnie o to w wywiadzie). Nie jestem pewien, czy jest to prawda w języku C #, ale zakładam, że tak będzie.

DevinEllingson
źródło
2
Dlatego używam ++ i in c # do kiedy jest używany samodzielnie (tzn. Nie w większym wyrażeniu). Ilekroć widzę pętlę ac # z i ++, trochę płaczę, ale teraz, gdy wiem, że w języku c # jest to właściwie ten sam kod, czuję się lepiej. Osobiście nadal będę używać ++ i, ponieważ nawyki umierają ciężko.
Mikle