Jeśli mam pętlę for, która jest zagnieżdżona w innej, jak mogę efektywnie wyjść z obu pętli (wewnętrznej i zewnętrznej) w najszybszy możliwy sposób?
Nie chcę używać wartości logicznej, a następnie powiedzieć, że przejdź do innej metody, ale po prostu wykonuję pierwszy wiersz kodu po zewnętrznej pętli.
Jak szybko i przyjemnie to zrobić?
Myślałem, że wyjątki nie są tanie / powinny być wprowadzane tylko w naprawdę wyjątkowych warunkach itp. Dlatego nie sądzę, aby to rozwiązanie było dobre z punktu widzenia wydajności.
Nie sądzę, że warto korzystać z nowszych funkcji .NET (metody anonowe), aby robić coś, co jest dość fundamentalne.
c#
for-loop
nested-loops
GurdeepS
źródło
źródło
Odpowiedzi:
Ale
goto
to brzydkie i nie zawsze możliwe. Możesz również umieścić pętle w metodzie (lub metodzie anonowej) i użyć,return
aby wyjść z powrotem do głównego kodu.vs:
Zauważ, że w C # 7 powinniśmy uzyskać „funkcje lokalne”, co (składnia tbd itp.) Oznacza, że powinno działać coś takiego:
źródło
goto
jest szkodliwe per se , podczas gdy jest to po prostu właściwy instrument do robienia pewnych rzeczy i podobnie jak wiele instrumentów może być niewłaściwie używanych. Ta niechęć religijnagoto
jest szczerze głupia i zdecydowanie nienaukowa.Dostosowanie C # podejścia często używanego w C - ustawianie wartości zmiennej zewnętrznej pętli poza warunkami pętli (tj. Dla pętli używającej zmiennej int
INT_MAX -1
często jest dobrym wyborem):Jak zauważono w kodzie,
break
magicznie nie przeskoczy do następnej iteracji zewnętrznej pętli - więc jeśli masz kod poza wewnętrzną pętlą, to podejście wymaga więcej kontroli. W takim przypadku rozważ inne rozwiązania.To podejście działa z pętlami
for
iwhile
nie działaforeach
. W przeciwnym razieforeach
nie będziesz mieć dostępu do kodu do ukrytego modułu wyliczającego, więc nie możesz go zmienić (a nawet jeśli nie możeszIEnumerator
mieć metody „MoveToEnd”).Podziękowania dla autorów komentarza:
i = INT_MAX - 1
sugestia Metafor
/foreach
komentarz ygoe .Właściwa
IntMax
przez jmbpianouwagą o kodzie po wewnętrznej pętlę blizpasta
źródło
To rozwiązanie nie dotyczy C #
Dla osób, które znalazły to pytanie w innych językach, JavaScript, Java i D zezwalają na przerwy w pracy i kontynuuje :
źródło
Użyj odpowiedniej osłony w zewnętrznej pętli. Ustaw osłonę w wewnętrznej pętli przed zerwaniem.
Lub jeszcze lepiej: wyodrębnij wewnętrzną pętlę do metody i wyjdź z zewnętrznej pętli, gdy zwróci fałsz.
źródło
Nie cytuj mnie, ale możesz użyć goto zgodnie z sugestią w MSDN. Istnieją inne rozwiązania, takie jak flaga sprawdzana w każdej iteracji obu pętli. Wreszcie możesz użyć wyjątku jako naprawdę ciężkiego rozwiązania swojego problemu.
IŚĆ DO:
Stan: schorzenie:
Wyjątek:
źródło
Czy jest możliwe przefakturowanie zagnieżdżonej pętli for na metodę prywatną? W ten sposób możesz po prostu „wrócić” z metody, aby wyjść z pętli.
źródło
[&] { ... return; ... }();
Wydaje mi się, że ludzie bardzo nie lubią takich
goto
stwierdzeń, więc poczułem potrzebę nieco tego wyjaśnienia.Wierzę, że „emocje”, które ludzie w
goto
końcu sprowadzają się do zrozumienia kodu i (nieporozumień) na temat możliwych implikacji wydajnościowych. Zanim odpowiem na pytanie, najpierw przejdę do kilku szczegółów na temat jego kompilacji.Jak wszyscy wiemy, C # jest kompilowany do IL, który jest następnie kompilowany do asemblera za pomocą kompilatora SSA. Dam trochę wglądu, jak to wszystko działa, a następnie spróbuję odpowiedzieć na pytanie.
Od C # do IL
Najpierw potrzebujemy fragmentu kodu C #. Zacznijmy od prostych:
Zrobię to krok po kroku, aby dać ci dobry obraz tego, co dzieje się pod maską.
Pierwsze tłumaczenie: od
foreach
do równoważnejfor
pętli (Uwaga: używam tutaj tablicy, ponieważ nie chcę wchodzić w szczegóły IDisposable - w takim przypadku musiałbym również użyć IEnumerable):Drugie tłumaczenie:
for
ibreak
jest tłumaczone na łatwiejszy odpowiednik:Tłumaczenie i trzeci (jest to odpowiednik kodu IL): możemy zmienić
break
iwhile
do gałęzi:Kompilator wykonuje te czynności w jednym kroku, ale daje wgląd w proces. Kod IL, który ewoluuje z programu C #, jest dosłownym tłumaczeniem ostatniego kodu C #. Możesz zobaczyć tutaj: https://dotnetfiddle.net/QaiLRz (kliknij „zobacz IL”)
Jedną z rzeczy, które zauważyłeś tutaj, jest to, że podczas procesu kod staje się bardziej złożony. Najłatwiejszym sposobem na zaobserwowanie tego jest fakt, że potrzebowaliśmy coraz więcej kodu, aby stwierdzić to samo. Możesz także o to argumentować
foreach
,for
,while
ibreak
są rzeczywiście krótkie Ręce zagoto
, co jest częściowo prawdą.Od IL do asemblera
Kompilator .NET JIT to kompilator SSA. Nie będę tutaj wchodził w wszystkie szczegóły formularza SSA i jak stworzyć optymalizujący kompilator, to po prostu za dużo, ale może dać podstawowe zrozumienie tego, co się stanie. Dla głębszego zrozumienia najlepiej zacząć czytać o optymalizacji kompilatorów (lubię tę książkę za krótkie wprowadzenie: http://ssabook.gforge.inria.fr/latest/book.pdf ) i LLVM (llvm.org) .
Każdy kompilator optymalizacyjny opiera się na fakcie, że kod jest łatwy i ma przewidywalne wzorce . W przypadku pętli FOR korzystamy z teorii grafów do analizy gałęzi, a następnie optymalizujemy takie rzeczy, jak cykle w naszych gałęziach (np. Gałęzie do tyłu).
Jednak teraz mamy gałęzie przekazujące do implementacji naszych pętli. Jak można się domyślić, jest to rzeczywiście jeden z pierwszych kroków, które JIT zamierza naprawić, w następujący sposób:
Jak widać, mamy teraz odgałęzienie, które jest naszą małą pętlą. Jedyną rzeczą, która wciąż jest tutaj nieprzyjemna, jest gałąź, z którą skończymy z powodu naszej
break
oświadczenie. W niektórych przypadkach możemy to przenieść w ten sam sposób, ale w innych pozostanie.Dlaczego więc kompilator to robi? Cóż, jeśli uda nam się rozwinąć pętlę, możemy ją wektoryzować. Możemy nawet być w stanie udowodnić, że dodawane są tylko stałe, co oznacza, że cała nasza pętla może zniknąć w powietrzu. Podsumowując: czyniąc wzorce przewidywalnymi (powodując, że gałęzie są przewidywalne), możemy udowodnić, że pewne warunki utrzymują się w naszej pętli, co oznacza, że możemy wykonywać magię podczas optymalizacji JIT.
Jednak gałęzie mają tendencję do przełamywania tych miłych, przewidywalnych wzorców, co jest czymś optymalizującym, a więc niechęcią. Przerwij, kontynuuj, goto - wszyscy zamierzają przełamać te przewidywalne wzorce - i dlatego nie są tak naprawdę „mili”.
Powinieneś również zdać sobie sprawę z tego, że proste
foreach
jest bardziej przewidywalne niż kilkagoto
stwierdzeń, które są wszędzie. Pod względem (1) czytelności i (2) z perspektywy optymalizatora jest to zarówno lepsze rozwiązanie.Inną rzeczą wartą wspomnienia jest to, że jest to bardzo istotne dla optymalizacji kompilatorów do przypisywania rejestrów do zmiennych (proces zwany alokacją rejestrów ). Jak zapewne wiesz, w twoim CPU jest tylko skończona liczba rejestrów i są one zdecydowanie najszybszymi elementami pamięci w twoim sprzęcie. Zmienne używane w kodzie, który znajduje się w najbardziej wewnętrznej pętli, częściej przypisują rejestr, podczas gdy zmienne poza twoją pętlą są mniej ważne (ponieważ ten kod prawdopodobnie jest mniej trafiony).
Pomoc, zbyt duża złożoność ... co powinienem zrobić?
Najważniejsze jest to, że zawsze powinieneś używać konstrukcji językowych, które masz do dyspozycji, które zwykle (domyślnie) budują przewidywalne wzorce dla twojego kompilatora. Staraj się unikać dziwnych oddziałów, jeśli to możliwe (konkretnie:
break
,continue
,goto
lubreturn
w środku niczego).Dobrą wiadomością jest to, że te przewidywalne wzorce są łatwe do odczytania (dla ludzi) i łatwe do wykrycia (dla kompilatorów).
Jeden z tych wzorów nosi nazwę SESE, co oznacza Single Entry Single Exit.
A teraz dochodzimy do prawdziwego pytania.
Wyobraź sobie, że masz coś takiego:
Najłatwiejszym sposobem uczynienia z tego przewidywalnego wzoru jest po prostu
if
całkowite wyeliminowanie :W innych przypadkach można również podzielić metodę na 2 metody:
Zmienne tymczasowe? Dobry, zły czy brzydki?
Możesz nawet zdecydować o zwróceniu wartości logicznej z pętli (ale ja osobiście wolę formularz SESE, ponieważ tak właśnie kompilator to zobaczy i myślę, że jest łatwiejszy do odczytania).
Niektórzy uważają, że łatwiej jest użyć zmiennej tymczasowej i proponują takie rozwiązanie:
Ja osobiście jestem przeciwny takiemu podejściu. Spójrz jeszcze raz, jak skompilowany jest kod. Teraz zastanów się, co to zrobi z tymi ładnymi, przewidywalnymi wzorami. Zrób zdjęcie?
Racja, pozwól mi to przeliterować. Stanie się tak, że:
more
zmienną, która jest używana tylko w przepływie kontrolnym.more
zostanie usunięta z programu i pozostaną tylko gałęzie. Te gałęzie zostaną zoptymalizowane, więc z wewnętrznej pętli wydostaniesz się tylko jedna gałąź.more
jest zdecydowanie używana w wewnętrznej pętli, więc jeśli kompilator jej nie zoptymalizuje, ma dużą szansę na przypisanie do rejestru (który zużywa cenną pamięć rejestru).Podsumowując: optymalizator w twoim kompilatorze sprawi wiele kłopotów, aby dowiedzieć się, że
more
jest używany tylko do przepływu sterowania, aw najlepszym przypadku scenariusz przetłumaczy go na jedną gałąź poza zewnętrzną dla pętla.Innymi słowy, najlepszym scenariuszem jest to, że zakończy się odpowiednikiem tego:
Moja osobista opinia na ten temat jest dość prosta: jeśli tak właśnie zamierzaliśmy, uczyńmy świat łatwiejszym zarówno dla kompilatora, jak i czytelności, i napiszmy to od razu.
tl; dr:
Dolna linia:
goto
lubbool more
, wybierz ten pierwszy.źródło
yield return
. Oznacza to, że chociaż łatwo jest wykazać, że istnieją użyteczne przypadki, w przypadku większości kodów „na poziomie aplikacji” jest to prawdopodobnie bardzo niski poziom frustracji w C #, z wyłączeniem tych „tylko pochodzących z” C / C ++ ;-) Czasem też tęsknię Czasami „rzut” Ruby (odwijanie bez wyjątku), ponieważ pasuje również do tej domeny.goto
, nie czyni twojego kodu bardziej czytelnym - argumentowałbym, że to, co się dzieje, jest dokładnie odwrotnie: w rzeczywistości twój przepływ kontrolny będzie zawierał więcej węzłów - co jest w zasadzie definicja złożoności. Samo unikanie go w celu samodyscypliny brzmi nieproduktywnie pod względem czytelności.goto
po prostu nie mają dyscypliny, aby ich nie nadużywać.dodaj do funkcji / metody i użyj wczesnego powrotu lub przestaw pętle w klauzulę while. goto / wyjątki / cokolwiek z pewnością nie jest tutaj odpowiednie.
źródło
Poprosiłeś o kombinację szybkiego, przyjemnego, bez użycia wartości logicznej, bez użycia goto i C #. Wykluczyłeś wszystkie możliwe sposoby robienia tego, co chcesz.
Najszybszym i najmniej brzydkim sposobem jest użycie goto.
źródło
Czasami miło jest wyodrębnić kod do jego własnej funkcji i niż użyć wczesnego powrotu - wczesne powroty są złe:)
źródło
Widziałem wiele przykładów, które używają słowa „przerwa”, ale żaden nie używa słowa „kontynuuj”.
Nadal wymagałoby to jakiejś flagi w wewnętrznej pętli:
źródło
Odkąd po raz pierwszy widziałem
break
w C kilka dekad temu, ten problem mnie denerwuje. Miałem nadzieję, że jakieś ulepszenie języka będzie miało rozszerzenie break, które zadziała w ten sposób:źródło
Pamiętam z czasów studenckich, że powiedziano, że matematycznie można udowodnić, że możesz zrobić wszystko w kodzie bez goto (tzn. Nie ma sytuacji, w której goto jest jedyną odpowiedzią). Tak więc, nigdy nie używam goto (tylko moje osobiste preferencje, nie sugerując, że mam rację czy źle)
W każdym razie, aby wydostać się z zagnieżdżonych pętli, robię coś takiego:
... mam nadzieję, że tym, którzy mnie lubią, są anty-goto „fanboys” :)
źródło
Tak to zrobiłem. Nadal obejście.
źródło
Najłatwiejszym sposobem zakończenia podwójnej pętli byłoby bezpośrednie zakończenie pierwszej pętli
źródło
Pętle mogą być przerywane przy użyciu niestandardowych warunków w pętli, co pozwala uzyskać czysty kod.
Za pomocą tego kodu uzyskujemy następujące dane wyjściowe:
źródło
Inną opcją jest anonimowa funkcja . Bez goto, bez etykiety, bez nowej zmiennej, bez nowej nazwy funkcji i jeden wiersz krótszy niż przykład metody anonimowej u góry.
źródło
Rzuć niestandardowy wyjątek, który wychodzi poza pętlę zewnętrzną.
Działa dla
for
,foreach
lubwhile
dowolnego rodzaju pętli i dowolnego języka, który używatry catch exception
blokuźródło
źródło
Widzę, że zaakceptowałeś odpowiedź, w której osoba odsyła twoje stwierdzenie goto, podczas gdy we współczesnym programowaniu i w opinii eksperta goto jest zabójcą, nazwaliśmy go zabójcą w programowaniu, które mają pewne powody, o których nie będę tutaj omawiać w tym momencie, ale rozwiązanie twojego pytania jest bardzo proste, możesz użyć flagi boolowskiej w tego rodzaju scenariuszu, tak jak pokażę to w moim przykładzie:
proste i jasne. :)
źródło
Czy w ogóle spojrzałeś na
break
słowo kluczowe? OoTo tylko pseudo-kod, ale powinieneś zobaczyć, co mam na myśli:
Jeśli myślisz o
break
byciu funkcją jakbreak()
, to jego parametrem byłaby liczba pętli, z których można zerwać. Ponieważ znajdujemy się w trzeciej pętli kodu tutaj, możemy wyrwać się ze wszystkich trzech.Podręcznik: http://php.net/break
źródło
Myślę, że chyba, że chcesz zrobić „boolowską rzecz”, jedynym rozwiązaniem jest rzucenie. Czego oczywiście nie powinieneś robić ...!
źródło