Dlaczego instrukcja „kontynuuj” nie może znajdować się wewnątrz bloku „na koniec”?


107

Nie mam problemu; Jestem po prostu ciekawy. Wyobraź sobie następujący scenariusz:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

To się nie skompiluje, ponieważ powoduje błąd kompilatora CS0157 :

Kontrola nie może opuścić treści klauzuli końcowej

Czemu?


7
Więc. Po prostu ciekawy. Jeśli ma to dla ciebie całkowity sens, dlaczego się nie kompiluje, dlaczego chcesz, aby ktoś wyjaśnił, co już ma sens? =)
J. Steen

14
dlaczego miałbyś potrzebować continue;w finallybloku? czy to nie to samo co continue;po try -- catchbloku?
bansi

6
@ J.Steen Nie, WIEM, że finally/continueto ograniczenie kompilatora C # :-) Też jestem ciekawy przyczyny tego ograniczenia.
xanatos

5
@xanatos - technicznie rzecz biorąc, jest to ograniczenie bazowego CIL. Ze specyfikacji języka : „Transfer sterowania nigdy nie może wprowadzać procedury obsługi catch lub klauzuli na końcu, chyba że za pośrednictwem mechanizmu obsługi wyjątków”. i „Przeniesienie sterowania z obszaru chronionego jest dozwolone tylko na podstawie instrukcji wyjątku (Leave, end.filter, end.catch lub end.finally)”. brRodzina instrukcji latorośl nie może tego dokonać.
Niepodpisany

1
@Unsigned: to też dobre ograniczenie, pozwalające na to, że byłoby dziwne :)
user7116

Odpowiedzi:


150

finallybloki są uruchamiane niezależnie od tego, czy został zgłoszony wyjątek, czy nie. Co by się stało, gdyby został wyrzucony wyjątek continue? Nie można kontynuować wykonywania pętli, ponieważ nieprzechwycony wyjątek przeniesie kontrolę do innej funkcji.

Nawet jeśli nie zostanie zgłoszony żaden wyjątek, finallyzostanie uruchomiony, gdy inne instrukcje transferu sterującego w przebiegu bloku try / catch, takie jak returnna przykład a, powodują ten sam problem.

Krótko mówiąc, z jego semantyką finallynie ma sensu zezwalanie na przenoszenie kontroli z wnętrza finallybloku na zewnątrz.

Wspieranie tego za pomocą alternatywnej semantyki byłoby bardziej zagmatwane niż pomocne, ponieważ istnieją proste obejścia, które sprawiają, że zamierzone zachowanie jest wyraźniejsze. Otrzymujesz więc błąd i jesteś zmuszony do prawidłowego przemyślenia swojego problemu. Jest to ogólna idea „rzucenia się w dół sukcesu”, która pojawia się w C #.

C #, ty i koniec, jeśli sukces

Jeśli chcesz zignorować wyjątki (najczęściej jest to zły pomysł) i kontynuować wykonywanie pętli, użyj bloku catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Jeśli chcesz continuetylko wtedy, gdy nie są wyrzucane żadne nieprzechwycone wyjątki, po prostu umieść continuepoza blokiem try.


12
Przyjmę to jako odpowiedź, ponieważ rozumiesz powody, dla których Microsoft prawdopodobnie zdecydował się nie zaakceptować kontynuacji w ostatecznym. Prawdopodobnie te obrazy też mnie przekonały :)
lpaloub

20
czy potrafisz rozwinąć ideę „rzucić się w dół sukcesu”? Nie rozumiem :-D
Ant


13
Obraz wykonał Jon Skeet, przy okazji. Dostałem stąd: msmvps.com/blogs/jon_skeet/archive/2010/09/02/…
R. Martinho Fernandes

1
Oczywiście znak a continuew finallybloku będzie OK, jeśli będzie kontynuował tylko lokalną pętlę zdefiniowaną wewnątrz finallybloku. W pytaniu stara się jednak kontynuować „zewnętrzną” pętlę. Podobne oświadczenia, że nie można mieć wewnątrz finallybloku są return, break(gdy wyrwanie się z bloku) i goto(idąc do etykiety spoza finallybloku). Aby zapoznać się z pokrewną dyskusją na temat języka Java, zobacz Powrót z końcowego bloku w języku Java .
Jeppe Stig Nielsen

32

Oto wiarygodne źródło:

Instrukcja continue nie może wyjść z bloku final (sekcja 8.10). Gdy instrukcja continue występuje w bloku final, cel instrukcji continue musi znajdować się w tym samym bloku final; w przeciwnym razie wystąpi błąd w czasie kompilacji.

Pochodzi z MSDN, 8.9.2 Instrukcja continue .

Dokumentacja mówi, że:

Instrukcje bloku last są zawsze wykonywane, gdy sterowanie opuszcza instrukcję try. Dzieje się tak niezależnie od tego, czy przekazanie kontroli następuje w wyniku normalnego wykonania, w wyniku wykonania instrukcji break, continue, goto lub return, czy też w wyniku propagowania wyjątku z instrukcji try. Jeśli wyjątek zostanie zgłoszony podczas wykonywania ostatniego bloku, wyjątek jest propagowany do następnej otaczającej instrukcji try. Jeśli propagowano inny wyjątek, zostanie on utracony. Proces propagowania wyjątku omówiono dalej w opisie instrukcji throw (sekcja 8.9.5).

Jest stąd 8.10 Instrukcja try .


31

Możesz myśleć, że to ma sens, ale w rzeczywistości nie ma to sensu .

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

Co zamierzasz zrobić przerwę lub kontynuować, gdy zostanie zgłoszony wyjątek? Zespół kompilatora C # nie chce samodzielnie podejmować decyzji, zakładając breaklub continue. Zamiast tego postanowili poskarżyć się, że sytuacja dewelopera będzie niejednoznaczna do przekazania kontroli finally block.

Zatem zadaniem programisty jest jasne określenie, co zamierza zrobić, zamiast zakładania przez kompilator czegoś innego.

Mam nadzieję, że rozumiesz, dlaczego to się nie kompiluje!


Z drugiej strony, nawet gdyby nie było żadnego „catch”, na ścieżkę wykonania po finallyinstrukcji wpłynęłoby to, czy catchwyjście zostało zakończone przez wyjątek, i nie ma mechanizmu, który mógłby powiedzieć, jak to powinno współdziałać z plikiem continue. Podoba mi się jednak twój przykład, ponieważ pokazuje on jeszcze większy problem.
supercat

@supercat Zgadzam się z tobą, moja odpowiedź pokazuje przykład niejednoznacznej sytuacji dla kompilatora wciąż jest tak wiele problemów z tym podejściem.
Sriram Sakthivel

16

Jak stwierdzili inni, ale skupili się na wyjątkach, tak naprawdę chodzi o niejednoznaczne traktowanie przekazywania kontroli.

Myślisz prawdopodobnie o takim scenariuszu:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Teoretycznie można prześledzić przepływ sterowania i powiedzieć, że tak, to jest „ok”. Żaden wyjątek nie jest zgłaszany, nie jest przekazywana żadna kontrola. Ale projektanci języka C # mieli na myśli inne problemy.

Wyrzucony wyjątek

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

Dreaded Goto

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

Powrót

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

Zerwanie

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Tak na zakończenie, tak, natomiast nie jest niewielkie możliwość użycia continuew sytuacjach, gdy kontrola nie jest przekazywana, ale sporo (większość?) Przypadków dotyczyć wyjątki lub returnbloków. Projektanci języka uznali, że byłoby to zbyt niejednoznaczne i (prawdopodobnie) niemożliwe do zapewnienia w czasie kompilacji, że Twój continuejest używany tylko w przypadkach, gdy przepływ sterowania nie jest przenoszony.


Mam taką samą sytuację w Javie, gdy program Proguard ulegał awarii podczas zaciemniania. Twoje wyjaśnienie jest całkiem dobre. C # podaje błąd czasu kompilacji - ma to sens.
Dev_Vikram,

11

Generalnie continuenie ma sensu, gdy jest używany w finallybloku. Spójrz na to:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}

„Ogólnie” wiele rzeczy nie ma sensu. I nie oznacza to, że nie powinno działać „w szczególności”. IE: continuenie ma sensu poza instrukcją pętli, ale nie oznacza to, że nie jest obsługiwana.
zerkms

Myślę, że to ma jakiś sens. itemmoże być plik, odczyt nieudany -> finallyzamyka plik. continuezapobiega pozostałej obróbce.
jnovacho

5

„To się nie skompiluje i myślę, że ma to sens”

Myślę, że tak nie jest.

Kiedy dosłownie masz catch(Exception), nie potrzebujesz wreszcie (i prawdopodobnie nawet nie continue).

Jeśli masz bardziej realistyczne catch(SomeException)podejście, co powinno się stać, gdy wyjątek nie zostanie przechwycony? Twoje continuechce jechać w jedną stronę, wyjątek obsługi drugiego.


2
Myślę, że możesz potrzebować, finallykiedy masz catch. Jest powszechnie używany do niezawodnego zamykania zasobów.
jnovacho

Ale to nie tylko wyjątki. Można łatwo mieć returnoświadczenie w swoim trylub catchbloków. Co teraz zrobimy? Wrócić czy kontynuować pętlę? (a jeśli będziemy kontynuować, co jeśli jest to ostatnia pozycja do iteracji? Wtedy po prostu kontynuujemy poza pętlą, foreacha powrót nigdy się nie dzieje?) EDYCJA: Lub nawet gotodo etykiety poza pętlą, prawie każda akcja przenosząca kontrolę poza foreachpętlę .
Chris Sinclair

2
Nie zgadzam się z „Kiedy dosłownie masz złapanie (Wyjątek), to ostatecznie nie potrzebujesz (i prawdopodobnie nawet nie kontynuujesz)”. A jeśli muszę wykonać operację, niezależnie od tego, czy wyjątek wywołał, czy nie?
lpaloub

OK, w końcu zaspokoją dodatkowe potrzeby returnwewnątrz bloku. Ale zwykle wykonanie jest kontynuowane po złapaniu wszystkiego.
Henk Holterman

„wreszcie” nie jest „chwytem”. Powinien być używany do czyszczenia kodu. Ostatecznie blok jest uruchamiany niezależnie od tego, czy został zgłoszony wyjątek, czy nie . Rzeczy takie jak zamykanie plików lub zwalnianie pamięci (jeśli używasz niezarządzanego kodu) itp. To rzeczy, które umieszczasz w ostatnim bloku
Robotnik

3

Nie możesz opuścić ciała ostatecznego bloku. Obejmuje to łamanie, powrót, aw Twoim przypadku kontynuowanie słów kluczowych.


3

finallyBlok może być wykonany z wyjątkiem czeka na rethrown. Naprawdę nie miałoby sensu opuszczanie bloku (przez a continuelub cokolwiek innego) bez ponownego zgłaszania wyjątku.

Jeśli chcesz kontynuować pętlę cokolwiek się stanie, nie potrzebujesz instrukcji final: po prostu złap wyjątek i nie wyrzucaj ponownie.


1

finallydziała niezależnie od tego, czy zostanie zgłoszony nieprzechwycony wyjątek. Inni już wyjaśnili, dlaczego jest to continuenielogiczne, ale tutaj jest alternatywa, która jest zgodna z duchem tego, o co wydaje się prosić ten kod. Zasadniczo finally { continue; }mówi:

  1. W przypadku wykrycia wyjątków kontynuuj
  2. Jeśli są nieprzechwycone wyjątki, pozwól im zostać wyrzucone, ale kontynuuj

(1) można zadowalać umieszczając continuena końcu każdego z nich catch, a (2) zadowalać przechowywanie nieprzechwyconych wyjątków do późniejszego wyrzucenia. Możesz to napisać tak:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

W rzeczywistości finallyzostałby stracony również w trzecim przypadku, w którym nie było żadnych wyjątków, złapanych lub nie złapanych. Jeśli było to pożądane, możesz oczywiście po prostu umieścić pojedynczy znak continuepo bloku try-catch zamiast wewnątrz każdego catch.


1

Technicznie rzecz biorąc, jest to ograniczenie podstawowego CIL. Ze specyfikacji językowej :

Transfer kontroli nigdy nie jest dozwolony do wprowadzenia procedury obsługi catch lub klauzuli last, chyba że za pośrednictwem mechanizmu obsługi wyjątków.

i

Przekazanie sterowania z chronionego obszaru jest dozwolona wyłącznie przez instrukcji wyjątków ( leave, end.filter, end.catch, a end.finally)

Na stronie z brinstrukcją :

Ta instrukcja nie pozwala na wykonywanie transferów kontrolnych do bloków try, catch, filter, i na końcu.

To ostatnie odnosi się do wszystkich instrukcji branżowych, w tym beq, brfalseitp


-1

Projektanci języka po prostu nie chcieli (lub nie mogli) rozumować semantyki ostatecznego bloku zakończonego transferem sterującym.

Jednym z problemów, a może kluczowym, jest to, że finallyblok jest wykonywany jako część nielokalnego transferu sterowania (przetwarzanie wyjątków). Celem tego transferu kontroli nie jest otaczająca pętla; przetwarzanie wyjątku przerywa pętlę i kontynuuje rozwijanie.

Jeśli mamy transfer kontroli z finallybloku porządkowego, to pierwotny transfer kontroli jest „przechwytywany”. Zostaje anulowane, a kontrola idzie gdzie indziej.

Można wypracować semantykę. Mają to w innych językach.

Projektanci C # zdecydowali się po prostu nie zezwalać na statyczne przenoszenie elementów sterujących typu „goto”, co nieco uprościło pewne czynności.

Jednak nawet jeśli to zrobisz, nie rozwiąże to pytania, co się stanie, jeśli transfer dynamiczny zostanie zainicjowany z a finally: a co, jeśli ostatni blok wywoła funkcję, a ta funkcja wyrzuca? Oryginalne przetwarzanie wyjątków jest następnie „przechwytywane”.

Jeśli poznasz semantykę tej drugiej formy przechwytywania, nie ma powodu, aby odrzucać pierwszy typ. W rzeczywistości są tym samym: przekazanie kontroli jest przekazem kontroli, niezależnie od tego, czy ma ten sam zakres leksykalny, czy nie.


Różnica polega na tym, że finallyz definicji natychmiast po tym musi nastąpić kontynuacja transferu, który go spowodował (niezłapany wyjątek returnitp.). Byłoby łatwo zezwolić na transfery typu „goto” w ramach finally(nawiasem mówiąc, które języki to robią?), Ale oznaczałoby to, że try { return } finally { ... } może to nie wrócić , co jest całkowicie nieoczekiwane. Jeśli finallywywołuje funkcję, która rzuca, to tak naprawdę nie to samo, ponieważ już spodziewamy się, że wyjątki mogą wystąpić w dowolnym momencie i przerwać normalny przepływ.
nmclean

@nmclean: Właściwie wyjątek, który ucieka, finallyjest mniej więcej tym samym, ponieważ może spowodować nieoczekiwany i nielogiczny przepływ programu. Jedyną różnicą jest to, że projektanci języka, nie będąc w stanie odrzucić wszystkich programów, w których ostatecznie blok mógłby zgłosić nieobsługiwany wyjątek, zamiast tego pozwalają takim programom na kompilację i mają nadzieję, że program może zaakceptować wszelkie konsekwencje, które mogą nastąpić po porzuceniu wcześniejszego wycofania wyjątku sekwencja.
supercat

@supercat Zgadzam się, że przerywa on oczekiwany przepływ, ale chodzi mi o to, że tak jest zawsze w przypadku nieobsłużonych wyjątków, niezależnie od tego, czy bieżący przepływ jest a, finallyczy nie. Zgodnie z logiką ostatniego akapitu Kaz, funkcje powinny również mieć swobodę wpływania na przepływ w zakresie wywołującego za pomocą continueitp., Ponieważ mogą to robić w drodze wyjątków. Ale to oczywiście nie jest dozwolone; wyjątki są jedynym wyjątkiem od tej reguły, stąd nazwa „wyjątek” (lub „przerwanie”).
nmclean

@nmclean: Niezagnieżdżone wyjątki reprezentują przepływ sterowania, który różni się od normalnego wykonywania, ale nadal ma strukturę. Instrukcja następująca po bloku powinna być wykonywana tylko wtedy, gdy wszystkie wyjątki, które wystąpiły w tym bloku, zostały w nim przechwycone. Może się to wydawać rozsądnym oczekiwaniem, ale wyjątek, który występuje w obrębie finallybloku, może je naruszać. Naprawdę paskudna sytuacja, którą IMHO powinno zostać rozwiązane przez co najmniej finallypowiadomienie bloków o wszelkich oczekujących wyjątkach, aby mogły budować złożone obiekty wyjątków.
supercat

@mmclean Przepraszamy, nie zgadzam się, że nazwa „wyjątek” pochodzi od jakiegoś „wyjątku od reguły” (dotyczącego kontroli) specyficznego dla C #, LOL. Aspekt wyjątku zmieniający przepływ sterowania to po prostu nielokalny dynamiczny transfer sterowania. Regularne przekazanie kontroli w tym samym zakresie leksykalnym (bez rozwijania) można uznać za szczególny przypadek.
Kaz
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.