Kompiluje brakującą instrukcję return w metodzie innej niż void

189

Napotkałem sytuację, w której w metodzie innej niż void brakuje instrukcji return , a kod nadal się kompiluje. Wiem, że instrukcje po pętli while są nieosiągalne (martwy kod) i nigdy nie zostaną wykonane. Ale dlaczego kompilator nawet nie ostrzega o zwrocie czegoś? Albo dlaczego język miałby pozwolić nam na zastosowanie metody nie-void mającej nieskończoną pętlę i niczego nie zwracającej?

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

Jeśli dodam instrukcję break (nawet warunkową) w pętli while, kompilator skarży się na niesławne błędy: Method does not return a valuew Eclipse i Not all code paths return a valueVisual Studio.

public int doNotReturnAnything() {
    while(true) {
        if(mustReturn) break;
        //do something
    }
    //no return statement
}

Dotyczy to zarówno Java, jak i C #.

cPu1
źródło
3
Fajne pytanie. Byłbym zainteresowany tego powodem.
Erik Schierboom
18
Zgadnij: jest to nieskończona pętla, więc przepływ kontroli powrotu jest nieistotny?
Lews Therin
5
„dlaczego język miałby pozwalać na stosowanie metody nie-void mającej nieskończoną pętlę i niczego nie zwracającej?” <- choć pozornie głupie, pytanie można również odwrócić: dlaczego nie byłoby to dozwolone? Nawiasem mówiąc, czy to jest rzeczywisty kod?
fge
3
W przypadku Javy można znaleźć dobre wyjaśnienie w tej odpowiedzi .
Keppil
4
Jak wyjaśnili inni, dzieje się tak dlatego, że kompilator jest wystarczająco inteligentny, aby wiedzieć, że pętla jest nieskończona. Zauważ jednak, że kompilator nie tylko pozwala na brak powrotu, ale wymusza go, ponieważ wie wszystko, gdy pętla jest nieosiągalna. Przynajmniej w Netbeans będzie dosłownie narzekać, unreachable statementjeśli coś jest po pętli.
Supr

Odpowiedzi:

240

Dlaczego język miałby pozwalać na stosowanie metody nie-void mającej nieskończoną pętlę i niczego nie zwracającej?

Reguła dla metod nie będących nieważnymi jest taka, że każda zwracana ścieżka kodu musi zwracać wartość , a ta reguła jest spełniona w twoim programie: zero z zerowych ścieżek kodu, które zwracają, zwraca wartość. Reguła nie brzmi: „każda metoda nie void musi mieć ścieżkę kodu, która zwraca”.

Umożliwia to pisanie skrótowych metod takich jak:

IEnumerator IEnumerable.GetEnumerator() 
{ 
    throw new NotImplementedException(); 
}

To metoda nieważna. To musi być non-void metoda w celu zaspokojenia interfejs. Ale niemądre wydaje się uczynienie tej implementacji nielegalną, ponieważ nic nie zwraca.

To, że twoja metoda ma nieosiągalny punkt końcowy z powodu goto(pamiętaj, że while(true)to po prostu przyjemniejszy sposób pisania goto) zamiast throw(co jest inną formą goto) nie ma znaczenia.

Dlaczego kompilator nawet nie ostrzega o zwrocie czegoś?

Ponieważ kompilator nie ma dobrych dowodów na to, że kod jest nieprawidłowy. Ktoś napisał while(true)i wydaje się prawdopodobne, że osoba, która to zrobiła, wiedziała, co robi.

Gdzie mogę przeczytać więcej o analizie osiągalności w C #?

Zobacz moje artykuły na ten temat tutaj:

ATBG: de facto i de jure osiągalność

I możesz również rozważyć przeczytanie specyfikacji C #.

Eric Lippert
źródło
Dlaczego więc ten kod podaje błąd czasu kompilacji: public int doNotReturnAnything() { boolean flag = true; while (flag) { //Do something } //no return } Ten kod również nie ma punktu przerwania. Więc teraz, skąd kompilator wie, że kod jest zły.
Sandeep Poonia
@ SandeepPoonia: Możesz przeczytać inne odpowiedzi tutaj ... Kompilator może wykryć tylko niektóre warunki.
Daniel Hilgarth
9
@ SandePPoonia: flagę logiczną można zmienić w czasie wykonywania, ponieważ nie jest ona stała, dlatego pętla niekoniecznie musi być nieskończona.
Schmuli
9
@SandeepPoonia: W C # specyfikacja mówi, że if, while, for, switchitd, rozgałęziające konstrukty, które działają na stałe traktowane są jako bezwarunkowych oddziałów przez kompilator. Dokładna definicja stałego wyrażenia znajduje się w specyfikacji. Odpowiedzi na twoje pytania znajdują się w specyfikacji; moja rada jest taka, że ​​ją czytasz.
Eric Lippert
1
Every code path that returns must return a valueNajlepsza odpowiedź. Jasno odnosi się do obu pytań. Dzięki
cPu1
38

Kompilator Java jest wystarczająco inteligentny, aby znaleźć nieosiągalny kod (kod po whilepętli)

a ponieważ jest nieosiągalny , nie ma sensu dodawać tam returninstrukcji (po whilezakończeniu)

to samo dotyczy warunkowego if

public int get() {
   if(someBoolean) {   
     return 10;
   }
   else {
     return 5;
   }
   // there is no need of say, return 11 here;
}

ponieważ warunek logiczny someBooleanmoże tylko ocenić albo, truealbo falsenie ma potrzeby podawania return jednoznacznie później if-else, ponieważ ten kod jest nieosiągalny , a Java nie narzeka na to.

sanbhat
źródło
4
Nie sądzę, żeby to naprawdę dotyczyło pytania. To wyjaśnia, dlaczego nie potrzebujesz returninstrukcji w nieosiągalnym kodzie, ale nie ma to nic wspólnego z tym, dlaczego nie potrzebujesz żadnej return instrukcji w kodzie OP.
Bobson
Jeśli kompilator Java jest wystarczająco inteligentny, aby znaleźć nieosiągalny kod (kod po pętli while), to dlaczego te poniższe kody się kompilują, oba mają nieosiągalny kod, ale metoda z if wymaga instrukcji return, ale metoda z chwilą nie. public int doNotReturnAnything() { if(true){ System.exit(1); } return 11; } public int doNotReturnAnything() { while(true){ System.exit(1); } return 11;// Compiler error: unreachable code }
Sandeep Poonia
@SandeepPoonia: Ponieważ kompilator nie wie, że System.exit(1)to zabije program. Może wykrywać tylko niektóre typy nieosiągalnego kodu.
Daniel Hilgarth
@Daniel Hilgarth: Ok kompilator nie wie, System.exit(1)że zabije program, możemy użyć dowolnego return statement, teraz kompilator rozpoznaje return statements. I zachowanie jest takie samo, zwrot wymaga za, if conditionale nie za while.
Sandeep Poonia
1
Dobra demonstracja nieosiągalnego odcinka bez specjalnego przypadku, takiego jakwhile(true)
Mateusz
17

Kompilator wie, że whilepętla nigdy nie przestanie działać, dlatego metoda nigdy się nie skończy, dlatego returninstrukcja nie jest konieczna.

Daniel Hilgarth
źródło
13

Biorąc pod uwagę, że twoja pętla działa w sposób ciągły - kompilator wie, że jest to pętla nieskończona - co oznacza, że ​​metoda i tak nigdy nie może wrócić.

Jeśli użyjesz zmiennej - kompilator wymusi regułę:

To się nie skompiluje:

// Define other methods and classes here
public int doNotReturnAnything() {
    var x = true;

    while(x == true) {
        //do something
    }
    //no return statement - won't compile
}
Dave Bish
źródło
Ale co jeśli „zrób coś” nie wiąże się w żaden sposób z modyfikacją x… kompilator nie jest taki inteligentny, aby to rozgryźć? Bum :(
Lews Therin
Nie - nie wygląda na to.
Dave Bish
@Lews, jeśli masz metodę oznaczoną jako zwracającą int, ale w rzeczywistości nie zwraca, powinieneś być zadowolony, że kompilator to oznaczy, abyś mógł oznaczyć metodę jako nieważną lub naprawić ją, jeśli nie masz zamiaru tego robić zachowanie.
MikeFHay
@MikeFHay Tak, nie kwestionując tego.
Lews Therin
1
Mogę się mylić, ale niektóre debugery pozwalają na modyfikację zmiennych. Tutaj, podczas gdy x nie jest modyfikowany przez kod i będzie zoptymalizowany przez JIT, można zmodyfikować x na false, a metoda powinna coś zwrócić (jeśli taka możliwość jest dozwolona przez debugger C #).
Maciej Piechotka
11

Specyfikacja Java definiuje pojęcie o nazwie Unreachable statements. W twoim kodzie nie możesz mieć nieosiągalnej instrukcji (jest to błąd czasu kompilacji). Po pewnym czasie nie wolno ci nawet mieć instrukcji return (prawda); instrukcja w Javie. while(true);Zestawienie sprawia, że następujące stwierdzenia nieosiągalny z definicji, zatem nie trzeba się returnoświadczenie.

Zauważ, że chociaż problem zatrzymania jest nierozstrzygalny w przypadku ogólnym, definicja nieosiągalnego stwierdzenia jest bardziej rygorystyczna niż zwykłe zatrzymanie. Decyduje o bardzo konkretnych przypadkach, w których program na pewno się nie zatrzymuje. Kompilator teoretycznie nie jest w stanie wykryć wszystkich nieskończonych pętli i nieosiągalnych instrukcji, ale musi wykryć określone przypadki zdefiniowane w specyfikacji (na przykład while(true)przypadek)

Konstantin Yovkov
źródło
7

Kompilator jest wystarczająco inteligentny, aby dowiedzieć się, że twoja whilepętla jest nieskończona.

Tak więc kompilator nie może myśleć za Ciebie. Nie może zgadnąć, dlaczego napisałeś ten kod. To samo oznacza zwracane wartości metod. Java nie będzie narzekać, jeśli nie zrobisz nic z wartościami zwracanymi przez metodę.

Tak więc, aby odpowiedzieć na twoje pytanie:

Kompilator analizuje kod i po stwierdzeniu, że żadna ścieżka wykonania nie prowadzi do odpadnięcia końca funkcji, kończy się na OK.

Mogą istnieć uzasadnione powody nieskończonej pętli. Na przykład wiele aplikacji korzysta z nieskończonej pętli głównej. Innym przykładem jest serwer WWW, który może bez końca czekać na żądania.

Adam Arold
źródło
7

W teorii typów istnieje coś, co nazywa się typem dolnym który jest podklasą każdego innego typu (!) I służy między innymi do wskazania braku zakończenia. (Wyjątki mogą być liczone jako rodzaj nieterminacji - nie kończysz normalną ścieżką).

Tak więc z teoretycznego punktu widzenia, te nie kończące się instrukcje można uznać za zwracające coś typu Bottom, który jest podtypem int, więc otrzymujesz (w pewnym sensie) wartość zwrotną mimo wszystko z perspektywy typu. I jest całkowicie w porządku, że nie ma żadnego sensu, że jeden typ może być podklasą wszystkiego innego, w tym int, ponieważ tak naprawdę nigdy go nie zwraca.

W każdym razie, poprzez teorię typu jawnego lub nie, kompilatory (autorzy kompilatorów) rozpoznają, że proszenie o wartość zwracaną po instrukcji nie kończącej jest głupie: nie ma możliwego przypadku, w którym mogłabyś potrzebować tej wartości. (Może być fajnie, gdy twój kompilator ostrzega cię, gdy wie, że coś się nie skończy, ale wygląda na to, że chcesz coś zwrócić. Ale lepiej to zostawić sprawdzającym styl, a może potrzebujesz podpisu, który: z jakiegoś innego powodu (np. podklasy), ale tak naprawdę chcesz nieterminacji.)

Rex Kerr
źródło
6

Nie ma sytuacji, w której funkcja może osiągnąć swój koniec bez zwracania odpowiedniej wartości. Dlatego kompilator nie ma na co narzekać.

Graham Borland
źródło
5

Visual Studio ma inteligentny silnik wykrywający, czy wpisałeś typ zwracany, a następnie powinien mieć instrukcję return w funkcji / metodzie.

Jak w PHP Twój typ zwrotu jest prawdziwy, jeśli nic nie zwróciłeś. kompilator dostaje 1, jeśli nic nie zostało zwrócone.

Od tego momentu

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

Kompilator wie, że chociaż sama instrukcja ma nieskończony charakter, więc nie należy jej brać pod uwagę. a kompilator php automatycznie się spełni, jeśli napiszesz warunek w wyrażeniu while.

Ale nie w przypadku VS zwróci ci błąd na stosie.

Tarun Gupta
źródło
4

Twoja pętla while będzie działać wiecznie, a zatem nie wyjdzie na zewnątrz; będzie nadal działać. Dlatego zewnętrzna część while {} jest nieosiągalna i nie ma sensu pisać na piśmie, czy nie. Kompilator jest wystarczająco inteligentny, aby dowiedzieć się, która część jest osiągalna, a która nie.

Przykład:

public int xyz(){
    boolean x=true;

    while(x==true){
        // do something  
    }

    // no return statement
}

Powyższy kod nie zostanie skompilowany, ponieważ może się zdarzyć, że wartość zmiennej x zostanie zmodyfikowana w treści pętli while. Dzięki temu można uzyskać dostęp do zewnętrznej części pętli while! Stąd kompilator wygeneruje błąd „nie znaleziono instrukcji return”.

Kompilator nie jest wystarczająco inteligentny (lub raczej leniwy;)), aby dowiedzieć się, czy wartość x zostanie zmodyfikowana, czy nie. Mam nadzieję, że to wszystko wyczyści.

kunal18
źródło
7
W rzeczywistości w tym przypadku nie chodzi o to, że kompilator jest wystarczająco inteligentny. W C # 2.0 kompilator był wystarczająco inteligentny, aby wiedzieć, że int x = 1; while(x * 0 == 0) { ... }była to nieskończona pętla, ale specyfikacja mówi, że kompilator powinien dokonywać dedukcji przepływu sterowania tylko wtedy, gdy wyrażenie pętli jest stałe , a specyfikacja definiuje wyrażenie stałe jako niezawierające zmiennych . Kompilator był zatem zbyt inteligentny . W C # 3 dostosowałem kompilator do specyfikacji i od tego czasu jest on celowo mniej inteligentny w zakresie tego rodzaju wyrażeń.
Eric Lippert
4

„Dlaczego kompilator nawet nie ostrzega o zwróceniu czegoś? Albo dlaczego język miałby pozwalać na zastosowanie metody innej niż void posiadającej nieskończoną pętlę i niczego nie zwracającej?”.

Ten kod jest ważny również we wszystkich innych językach (prawdopodobnie oprócz Haskell!). Ponieważ pierwsze założenie jest takie, że „celowo” piszemy jakiś kod.

Są też sytuacje, w których ten kod może być całkowicie poprawny, np. Jeśli zamierzasz go używać jako wątku; lub jeśli zwraca a Task<int>, możesz wykonać sprawdzanie błędów na podstawie zwróconej wartości int - której nie należy zwracać.

Kaveh Shahbazian
źródło
3

Mogę się mylić, ale niektóre debugery pozwalają na modyfikację zmiennych. Tutaj, podczas gdy x nie jest modyfikowany przez kod i będzie zoptymalizowany przez JIT, można zmodyfikować x na false, a metoda powinna coś zwrócić (jeśli taka możliwość jest dozwolona przez debugger C #).


źródło
1

Specyfika tego przypadku Java (które prawdopodobnie są bardzo podobne do przypadku C #) dotyczy sposobu, w jaki kompilator Java określa, czy metoda jest w stanie zwrócić.

W szczególności reguły są takie, że metoda z typem zwracanym nie może być w stanie zakończyć się normalnie i zamiast tego musi zawsze zakończyć się nagle (tutaj nagle, wskazując za pomocą instrukcji return lub wyjątku) zgodnie z JLS 8.4.7 .

Jeśli metoda ma zadeklarowany typ zwracany, wówczas błąd kompilacji występuje, jeśli treść metody może się normalnie zakończyć. Innymi słowy, metoda z typem zwracanym musi zwrócić tylko za pomocą instrukcji return, która zapewnia zwrot wartości; nie wolno „zrzucić końca jego ciała” .

Kompilator sprawdza, czy możliwe jest normalne zakończenie na podstawie reguł określonych w JLS 14.21 Nieosiągalne instrukcje ponieważ definiuje również reguły normalnego zakończenia.

W szczególności zasady dotyczące instrukcji nieosiągalnych stanowią szczególny przypadek tylko dla pętli, które mają zdefiniowane truestałe wyrażenie:

Instrukcja while może zakończyć się normalnie, jeśli jest spełniony przynajmniej jeden z następujących warunków:

  • Instrukcja while jest osiągalna, a wyrażenie warunkowe nie jest wyrażeniem stałym (§ 15.28) o wartości true.

  • Istnieje osiągalna instrukcja break, która wychodzi z instrukcji while.

Jeśli więc whileinstrukcja może się normalnie wypełnić , to konieczne jest podanie poniższej instrukcji return, ponieważ kod jest uznawany za osiągalny, a każda whilepętla bez osiągalnej instrukcji break lub stałejtrue wyrażenia jest uważana za zdolną do ukończenia normalnie.

Reguły te oznaczają, że twoja whileinstrukcja ze stałym prawdziwym wyrażeniem i bez a nigdy niebreak jest uważana za zakończoną normalnie , więc żaden kod poniżej niej nigdy nie jest uważany za osiągalny . Koniec metody znajduje się poniżej pętli, a ponieważ wszystko poniżej pętli jest nieosiągalne, tak samo jest koniec metody, a zatem metoda nie może zakończyć się normalnie (tego szuka kompilator).

if instrukcje, z drugiej strony, nie mają specjalnego wyłączenia dotyczącego stałych wyrażeń, które są zapewnione pętlom.

Porównać:

// I have a compiler error!
public boolean testReturn()
{
    final boolean condition = true;

    if (condition) return true;
}

Z:

// I compile just fine!
public boolean testReturn()
{
    final boolean condition = true;

    while (condition)
    {
        return true;
    }
}

Powód tego rozróżnienia jest dość interesujący i wynika z chęci uwzględnienia flag kompilacji warunkowej, które nie powodują błędów kompilatora (z JLS):

Można oczekiwać, że instrukcja if będzie przetwarzana w następujący sposób:

  • Instrukcja if-then może normalnie wypełnić, jeśli spełniony jest przynajmniej jeden z poniższych warunków:

    • Instrukcja if-then jest osiągalna, a wyrażenie warunkowe nie jest wyrażeniem stałym, którego wartość jest prawdziwa.

    • Oświadczenie wtedy może zostać wypełnione normalnie.

    Oświadczenie wtedy jest osiągalne, jeśli oświadczenie if-then jest osiągalne, a wyrażenie warunkowe nie jest wyrażeniem stałym, którego wartość jest fałszywa.

  • Instrukcja if-then-else może zostać wykonana normalnie, jeśli instrukcja następnie może zakończyć się normalnie lub instrukcja else może zakończyć się normalnie.

    • Oświadczenie then jest osiągalne, jeśli osiągalne jest wyrażenie if-then-else, a wyrażenie warunkowe nie jest wyrażeniem stałym, którego wartość jest fałszywa.

    • Instrukcja else jest osiągalna, jeśli jest osiągalna instrukcja if-then-else, a wyrażenie warunkowe nie jest wyrażeniem stałym, którego wartość jest prawdziwa.

Takie podejście byłoby spójne z traktowaniem innych struktur kontrolnych. Jednak aby umożliwić wygodne korzystanie z instrukcji if do celów „kompilacji warunkowej”, rzeczywiste reguły są różne.

Na przykład poniższa instrukcja powoduje błąd podczas kompilacji:

while (false) { x=3; }ponieważ oświadczenie x=3;nie jest osiągalne; ale pozornie podobny przypadek:

if (false) { x=3; }nie powoduje błędu czasu kompilacji. Kompilator optymalizujący może zdać sobie sprawę, że instrukcja x=3;nigdy nie zostanie wykonana i może pominąć kod tej instrukcji z wygenerowanego pliku klasy, ale instrukcjax=3; nie jest uważana za „nieosiągalną” w technicznie określonym tutaj znaczeniu.

Uzasadnieniem tego odmiennego traktowania jest umożliwienie programistom zdefiniowania „zmiennych zmiennych”, takich jak:

static final boolean DEBUG = false; a następnie napisz kod taki jak:

if (DEBUG) { x=3; } Chodzi o to, że powinna istnieć możliwość zmiany wartości DEBUG z false na true lub z true na false, a następnie poprawnie skompiluj kod bez żadnych innych zmian w tekście programu.

Dlaczego warunkowa instrukcja break powoduje błąd kompilatora?

Jak podano w regułach osiągalności pętli, pętla while może również zakończyć się normalnie, jeśli zawiera osiągalną instrukcję break. Ponieważ zasady dotyczące osiągalności ifoświadczenia następnie klauzuli nie biorą stan ifpod uwagę w ogóle, taka warunkowa ifoświadczenie jest następnie klauzula jest zawsze uważane za osiągalne.

Jeśli breakjest osiągalny, to kod po pętli jest ponownie uznawany za osiągalny. Ponieważ nie ma dostępnego kodu, który spowodowałby nagłe zakończenie po pętli, metoda jest następnie uważana za zdolną do ukończenia normalnie, a więc kompilator oznacza ją jako błąd.

Trevor Freeman
źródło