Dlaczego było stwierdzenie (j ++); zakazana?

169

Poniższy kod jest nieprawidłowy (zobacz go na ideone ):

public class Test
{
    public static void Main()
    {
        int j = 5;
        (j++);      // if we remove the "(" and ")" then this compiles fine.
    }
}

błąd CS0201: Tylko przypisanie, wywołanie, inkrementacja, dekrementacja, await i nowe wyrażenia obiektu mogą być używane jako instrukcja

  1. Dlaczego kod kompiluje się, gdy usuwamy nawiasy?
  2. Dlaczego nie kompiluje się z nawiasami?
  3. Dlaczego C # został zaprojektowany w ten sposób?
user10607
źródło
29
@Servy Wiele osób zaangażowanych w projektowanie języka C # jest na SO, dlatego zapytałem. Coś w tym nie tak?
user10607
34
Nie mogę sobie wyobrazić pytania bardziej na temat niż „dlaczego określony język programowania radzi sobie z tym konkretnym zachowaniem w ten sposób?”
Mage Xy
20
Teraz mamy odpowiedź Erica Lipperta. Może chcesz ponownie przemyśleć swoją decyzję dotyczącą zaakceptowanej odpowiedzi. Jest Boże Narodzenie, więc być może zbyt wcześnie zaakceptowałeś odpowiedź.
Thomas Weller
17
@Servy Czy sugerujesz, że decyzje projektowe nigdy nie są dokumentowane i są absolutnie nieznane wszystkim innym? On nie prosi użytkowników, tak aby odgadnąć, że prosi o odpowiedź - że nie oznacza on z prośbą o odgadnięcia. To, czy ludzie odpowiadają zgadywaniem, zależy od nich , a nie od niego. I jak OP zauważył, że ludzie na przepełnienie stosu, który robił faktycznie prace nad C # i wykonane te decyzje.
Rob
5
@Rob: Problem w tym, że tak, chyba że gdzieś uzasadnienie jest już udokumentowane, to musi, ale spekulacje są fajne, a kto nie chce się dzielić swoimi? Jeśli znajdziesz sposób, aby upewnić się, że takie czyste (lub „wyuczone”) domysły są rzetelnie usuwane, powiedz mi, a zbijemy fortunę.
Deduplicator

Odpowiedzi:

221

Docenione głębokie spostrzeżenia.

Zrobię co w mojej mocy.

Jak zauważyły ​​inne odpowiedzi, kompilator wykrywa, że wyrażenie jest używane jako instrukcja . W wielu językach - C, JavaScript i wielu innych - użycie wyrażenia jako instrukcji jest całkowicie legalne. 2 + 2;jest legalne w tych językach, mimo że jest to stwierdzenie, które nie ma żadnego skutku. Niektóre wyrażenia są przydatne tylko ze względu na ich wartości, niektóre wyrażenia są przydatne tylko ze względu na ich skutki uboczne (takie jak wywołanie metody zwracającej void), a niektóre wyrażenia niestety są przydatne w obu przypadkach. (Podobnie jak przyrost.)

Chodzi o to, że stwierdzenia, które składają się tylko z wyrażeń, są prawie na pewno błędami, chyba że te wyrażenia są zwykle uważane za bardziej przydatne ze względu na ich skutki uboczne niż ich wartości . Projektanci języka C # chcieli znaleźć kompromis, dopuszczając wyrażenia, które były ogólnie uważane za efekt uboczny, jednocześnie odrzucając te, które są również zwykle uważane za przydatne dla ich wartości. Zbiór wyrażeń, które zidentyfikowali w C # 1.0, to przyrosty, ubytki, wywołania metod, przypisania i nieco kontrowersyjne wywołania konstruktorów.


POZA STRONĄ: Zwykle uważa się, że konstrukcja obiektu jest używana ze względu na wartość, którą wytwarza, a nie jako efekt uboczny konstrukcji; moim zdaniem przyzwolenie new Foo();jest trochę błędem. W szczególności widziałem ten wzorzec w rzeczywistym kodzie, który spowodował usterkę bezpieczeństwa:

catch(FooException ex) { new BarException(ex); } 

Zaskakująco trudno jest wykryć tę wadę, jeśli kod jest skomplikowany.


Dlatego kompilator wykrywa wszystkie instrukcje zawierające wyrażenia, których nie ma na tej liście. W szczególności wyrażenia w nawiasach są identyfikowane właśnie jako takie - wyrażenia w nawiasach. Nie ma ich na liście „dozwolonych jako wyrażenia instrukcji”, więc są niedozwolone.

Wszystko to służy zasadzie projektowania języka C #. Jeśli wpisałeś (x++);, prawdopodobnie zrobiłeś coś nie tak . To prawdopodobnie literówka M(x++);lub po prostu coś. Pamiętaj, nastawienie zespołu kompilatora C # nie brzmi: „ czy możemy wymyślić jakiś sposób, aby to zadziałało? ”. Postawa zespołu kompilatora C # brzmi: „ jeśli wiarygodny kod wygląda na prawdopodobny błąd, poinformujmy o tym programistę ”. Deweloperzy C # lubią to podejście.

Teraz wszystko, co powiedział, jest rzeczywiście kilka dziwnych przypadków, w których specyfikacja C # ma sugerować lub państwowe wprost, że nawiasy są niedozwolone, ale kompilator C # pozwala je tak czy inaczej. W prawie wszystkich tych przypadkach niewielka rozbieżność między określonym zachowaniem a dozwolonym zachowaniem jest całkowicie nieszkodliwa, więc twórcy kompilatorów nigdy nie naprawili tych małych błędów. Możesz przeczytać o nich tutaj:

Czy istnieje różnica między return myVar a return (myVar)?

Eric Lippert
źródło
5
Odp .: "Zwykle myśli się o wywołaniu konstruktora jako używanym do wartości, którą wytwarza, a nie do efektu ubocznego konstrukcji; moim zdaniem jest to trochę niedopasowanie": Wyobrażam sobie, że jeden czynnik w tej decyzji było to, że jeśli kompilator zabronił, nie byłoby żadnego czysty fix w przypadkach, gdy nazywając ją za efekt uboczny. (Większość innych zabronionych wyrażeń zawiera zbędne części, które nie służą żadnemu celowi i można je usunąć, z wyjątkiem sytuacji ... ? ... : ..., w których poprawką jest użycie if/ elsezamiast.)
ruakh
1
@ruakh: Ponieważ jest to zachowanie, którego należy odradzać, czysta poprawka nie jest konieczna, o ile istnieje rozsądnie tania / prosta poprawka. Które tam jest; przypisz go do zmiennej i nie używaj tej zmiennej. Jeśli chcesz być rażący, że go nie używasz, nadaj tej linii kodu własny zakres. Chociaż technicznie można by argumentować, że zmienia to semantykę kodu (bezużyteczne przypisanie referencji jest nadal bezużytecznym przypisaniem referencji), w praktyce jest mało prawdopodobne, aby miało to znaczenie. Przyznaję, że generuje trochę inny IL ... ale tylko wtedy, gdy jest skompilowany bez optymalizacji.
Brian
Safari, Firefox i Chrome podają ReferenceError podczas pisania contineu;.
Brian McCutchon
2
@McBrainy: Masz rację. Źle pamiętam usterkę wynikającą z błędów ortograficznych; Sprawdzę swoje notatki. W międzyczasie usunąłem obraźliwe stwierdzenie, ponieważ nie ma ono związku z szerszą kwestią.
Eric Lippert
1
@Mike: Zgadzam się. Inną sytuacją, w której czasami widzimy instrukcję, są przypadki testowe, try { new Foo(null); } catch (ArgumentNullException)...ale oczywiście te sytuacje z definicji nie są kodem produkcyjnym. Wydaje się rozsądne, że taki kod można napisać w celu przypisania do zmiennej fikcyjnej.
Eric Lippert
46

W specyfikacji języka C #

Instrukcje wyrażeń służą do oceny wyrażeń. Wyrażenia, których można używać jako instrukcji, obejmują wywołania metod, alokacje obiektów przy użyciu operatora new, przypisania przy użyciu = i złożonych operatorów przypisania, operacje zwiększania i zmniejszania przy użyciu operatorów ++ i - oraz wyrażenia await.

Umieszczenie nawiasów wokół instrukcji tworzy nowe tak zwane wyrażenie w nawiasach. Ze specyfikacji:

Wyrażenie w nawiasach składa się z wyrażenia zawartego w nawiasach. ... Wyrażenie w nawiasach jest obliczane przez ocenę wyrażenia w nawiasach. Jeśli wyrażenie w nawiasach oznacza przestrzeń nazw lub typ, wystąpi błąd w czasie kompilacji. W przeciwnym razie wynik wyrażenia w nawiasach jest wynikiem oceny zawartego wyrażenia.

Ponieważ wyrażenia w nawiasach nie są wymienione jako prawidłowe wyrażenie wyrażenia, zgodnie ze specyfikacją nie są one prawidłowe. Nikt nie wie, dlaczego projektanci zdecydowali się to zrobić w ten sposób, ale zakładam, że nawiasy nie działają, jeśli całe stwierdzenie jest zawarte w nawiasach: stmti (stmt)są dokładnie takie same.

Frank Bryce
źródło
4
@Servy: Jeśli uważnie przeczytasz, było to nieco bardziej zniuansowane. Tak zwana „instrukcja wyrażenia” jest rzeczywiście prawidłową instrukcją, a specyfikacja zawiera listę typów wyrażeń, które mogą być prawidłowymi instrukcjami. Jednak powtarzam na podstawie specyfikacji, że typ wyrażenia zwany „wyrażeniem w nawiasach” NIE znajduje się na liście prawidłowych wyrażeń, które mogą być używane jako prawidłowe wyrażenia.
Frank Bryce
4
Nie OPpyta o projekt języka. Chce tylko wiedzieć, dlaczego to błąd. Odpowiedź brzmi: ponieważ nie jest to prawidłowe stwierdzenie .
Leandro
9
OK, moja wina. Odpowiedź brzmi: ponieważ zgodnie ze specyfikacją nie jest to prawidłowe stwierdzenie. Masz to: powód projektowy !
Leandro
3
Pytanie nie dotyczy głębokiego, opartego na projektowaniu powodu, dla którego język został zaprojektowany do obsługi tej składni w ten sposób - pyta, dlaczego jeden fragment kodu kompiluje się, podczas gdy inny fragment kodu (który dla początkujących wygląda na to, że powinien się zachowywać identycznie) nie można skompilować. Ten post odpowiada na to.
Mage Xy
4
@Servy JohnCarpenter nie mówi tylko „Nie możesz tego zrobić”. Mówi, że te dwie rzeczy, przez które byłeś zdezorientowany, to nie to samo. j ++ jest instrukcją wyrażenia, podczas gdy (j ++) jest wyrażeniem w nawiasach. Nagle OP zna teraz różnicę i czym one są. To dobra odpowiedź. Odpowiada na treść pytania, a nie na tytuł. Myślę, że wiele sporów budzi słowo „projekt” w tytule pytania, ale projektanci nie muszą na to odpowiadać, a jedynie zaczerpnięto ze specyfikacji.
thinklarge
19

ponieważ nawiasy wokół i++znaku tworzą / definiują wyrażenie… jak mówi komunikat o błędzie… proste wyrażenie nie może być użyte jako instrukcja.

dlaczego język został tak zaprojektowany? aby uniknąć błędów, wprowadzających w błąd wyrażeń jako instrukcji, które nie wywołują skutków ubocznych, takich jak posiadanie kodu

int j = 5;
j+1; 

druga linia nie ma żadnego efektu (ale być może nie zauważyłeś). Ale zamiast usuwać go przez kompilator (ponieważ kod nie jest potrzebny). Wyraźnie prosi o usunięcie go (więc będziesz świadomy błędu) LUB napraw to, jeśli zapomniałeś czegoś wpisać.

edytuj :

aby część dotycząca nawiasów była bardziej przejrzysta ... nawiasy w języku c # (oprócz innych zastosowań, takich jak rzutowanie i wywołanie funkcji), służą do grupowania wyrażeń i zwracania pojedynczego wyrażenia (tworzenia wyrażeń podrzędnych).

na tym poziomie kodu dozwolone są tylko stamenty ... tak

j++; is a valid statement because it produces side effects

ale używając nawiasu zamieniasz go w wyrażenie

myTempExpression = (j++)

i to

myTempExpression;

jest niepoprawne, ponieważ kompilator nie może zapewnić, że wyrażenie jest efektem ubocznym. (nie bez wystąpienia problemu zatrzymania).

CaldasGSM
źródło
Tak, na początku też tak myślałem. Ale to nie ma efektu ubocznego. Wykonuje postinkrementację jzmiennej, prawda?
user10607
1
j++;jest prawidłową instrukcją, ale używając nawiasów, o których mówisz let me take this stement and turn it into an expression... a wyrażenia nie są poprawne w tym miejscu w kodzie
CaldasGSM
Szkoda, że ​​nie ma formy instrukcji „oblicz i ignoruj”, ponieważ zdarzają się sytuacje, w których kod może chcieć stwierdzić, że wartość można obliczyć bez zgłaszania wyjątku, ale nie przejmuje się faktyczną wartością obliczoną w ten sposób. Instrukcja oblicz i ignoruj ​​nie byłaby używana zbyt często, ale w takich przypadkach wyjaśniałaby intencje programisty.
supercat