Pętla Foreach z przerwaniem / powrotem vs. pętla while z jawnym niezmiennikiem i warunkiem końcowym

17

Jest to najpopularniejszy sposób (wydaje mi się) sprawdzania, czy wartość znajduje się w tablicy:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Jednak w książce, którą czytałem wiele lat temu, prawdopodobnie przez Wirtha lub Dijkstry, powiedziano, że ten styl jest lepszy (w porównaniu do pętli while z wyjściem w środku):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

W ten sposób dodatkowy warunek wyjścia staje się jawną częścią niezmiennika pętli, nie ma żadnych ukrytych warunków i wychodzi z pętli, wszystko jest bardziej oczywiste i bardziej w sposób ustrukturyzowany. Generalnie wolałem ten ostatni wzór, gdy tylko było to możliwe, i użyłem forpętli do iteracji tylko od ado b.

A jednak nie mogę powiedzieć, że pierwsza wersja jest mniej przejrzysta. Może jest to jeszcze bardziej zrozumiałe i łatwiejsze do zrozumienia, przynajmniej dla bardzo początkujących. Więc wciąż zadaję sobie pytanie, które z nich jest lepsze?

Może ktoś może podać uzasadnienie na korzyść jednej z metod?

Aktualizacja: To nie jest kwestia punktów zwrotnych z wielu funkcji, lambdów lub znalezienia elementu w tablicy jako takiej. Chodzi o to, jak pisać pętle z bardziej złożonymi niezmiennikami niż pojedyncza nierówność.

Aktualizacja: OK, widzę sens ludzi, którzy odpowiadają i komentują: Włączyłem tutaj pętlę foreach, która sama w sobie jest już znacznie bardziej przejrzysta i czytelna niż pętla while. Nie powinnam tego robić. Ale jest to również interesujące pytanie, więc zostawmy je takie, jakie są: pętla foreach i dodatkowy warunek w środku, lub pętla while z wyraźną niezmiennikiem pętli i późniejszym warunkiem. Wydaje się, że wygrywa pętla foreach z warunkiem i wyjściem / przerwaniem. Stworzę dodatkowe pytanie bez pętli foreach (dla listy połączonej).

Danila Piatov
źródło
2
Przytoczone tutaj przykłady kodu mieszają kilka różnych problemów. Wczesne i wielokrotne zwroty (które dla mnie idą do rozmiaru metody (nie pokazano)), wyszukiwanie tablic (które wymaga dyskusji o lambdach), foreach vs. bezpośrednie indeksowanie ... To pytanie byłoby bardziej jasne i łatwiejsze do odpowiedz, jeśli skupiał się tylko na jednym z tych problemów naraz.
Erik Eidt,
1
Wiem, że są to przykłady, ale istnieją języki, które mają interfejsy API do obsługi dokładnie tego przypadku użycia. To collection.contains(foo)
znaczy
2
Możesz znaleźć książkę i przeczytać ją teraz, aby zobaczyć, co faktycznie napisano.
Thorbjørn Ravn Andersen
1
„Lepsze” to bardzo subiektywne słowo. To powiedziawszy, można na pierwszy rzut oka stwierdzić, co robi pierwsza wersja. Że druga wersja robi dokładnie to samo, wymaga pewnej analizy.
David Hammen,

Odpowiedzi:

19

Myślę, że w przypadku prostych pętli, takich jak te, standardowa pierwsza składnia jest znacznie jaśniejsza. Niektórzy uważają, że wielokrotne zwroty są mylące lub pachną kodem, ale w przypadku tak małego fragmentu kodu nie sądzę, aby był to prawdziwy problem.

To staje się nieco bardziej dyskusyjne dla bardziej złożonych pętli. Jeśli zawartość pętli nie mieści się na ekranie i ma kilka zwrotów w pętli, należy argumentować, że wiele punktów wyjścia może utrudnić utrzymanie kodu. Na przykład, jeśli musiałbyś upewnić się, że jakaś metoda utrzymywania stanu została uruchomiona przed wyjściem z funkcji, łatwo byłoby pominąć dodanie jej do jednej z instrukcji return i spowodowałbyś błąd. Jeśli wszystkie warunki końcowe można sprawdzić w pętli while, masz tylko jeden punkt wyjścia i możesz dodać ten kod po nim.

To powiedziawszy, szczególnie w przypadku pętli dobrze jest wypróbować jak najwięcej logiki w oddzielnych metodach. Pozwala to uniknąć wielu przypadków, w których druga metoda miałaby zalety. Pętle Lean z wyraźnie oddzieloną logiką będą miały większe znaczenie niż który z tych stylów używasz. Ponadto, jeśli większość kodu źródłowego aplikacji używa jednego stylu, powinieneś pozostać przy tym stylu.

Natanael
źródło
56

To jest łatwe.

Prawie nic nie jest ważniejsze niż czytelność dla czytelnika. Pierwszy wariant, który znalazłem, był niezwykle prosty i przejrzysty.

Druga „ulepszona” wersja, musiałam przeczytać kilka razy i upewnić się, że wszystkie warunki brzegowe są prawidłowe.

Istnieje ZERO DOUBT, który ma lepszy styl kodowania (pierwszy jest znacznie lepszy).

Teraz - to, co jest CZYSTE dla ludzi, może różnić się w zależności od osoby. Nie jestem pewien, czy istnieją w tym celu obiektywne standardy (choć publikowanie na forum takim jak ten i uzyskiwanie informacji od różnych osób może pomóc).

W tym konkretnym przypadku mogę jednak powiedzieć, dlaczego pierwszy algorytm jest bardziej przejrzysty: wiem, jak wygląda i działa iteracja C ++ nad składnią kontenera. Zinternalizowałem to. Ktoś UNFAMILIAR (jego nowa składnia) z tą składnią może preferować drugą odmianę.

Ale kiedy poznasz i zrozumiesz tę nową składnię, jest to podstawowa koncepcja, której możesz po prostu użyć. W przypadku iteracji pętli (druga) należy dokładnie sprawdzić, czy użytkownik PRAWIDŁOWO sprawdza, czy wszystkie warunki brzegowe zapętlają całą tablicę (np. Mniej niż w miejsce mniejszego lub równego tego samego indeksu używanego do testowania i do indeksowania itp.).

Lewis Pringle
źródło
4
Nowe jest względne, tak jak było już w standardzie 2011. Drugie demo nie jest oczywiście C ++.
Deduplicator
Alternatywnym rozwiązaniem, jeśli chcesz korzystać z jednego punktu wyjścia byłoby ustawić flagę longerLength = true, a następnie return longerLength.
Cullub,
@Deduplicator Dlaczego nie jest to druga wersja demonstracyjna C ++? Nie rozumiem, dlaczego nie, czy brakuje mi czegoś oczywistego?
Rakete1111,
2
@ Rakete1111 Surowe tablice nie mają żadnych właściwości takich jak length. Jeśli faktycznie został zadeklarowany jako tablica, a nie wskaźnik, mogliby użyć sizeof, lub gdyby była std::array, poprawną funkcją składową jest to size(), że nie ma lengthwłaściwości.
IllusiveBrian
@IllusiveBrian: sizeofbyłby w bajtach ... Najbardziej ogólny od czasu C ++ 17 std::size().
Deduplicator,
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…] Wszystko jest bardziej oczywiste i bardziej ustrukturyzowane.

Nie do końca. Zmienna iwystępuje na zewnątrz pętli, a tu a zatem część zakresu zewnętrznej, podczas gdy (pun przeznaczone) xz foristnieje -loop tylko w zakresie pętli. Zakres jest jednym z bardzo ważnych sposobów na wprowadzenie struktury do programowania.

zero
źródło
1
@ruakh Nie jestem pewien, co zabrać z twojego komentarza. Wygląda to na nieco pasywno-agresywne, tak jakby moja odpowiedź była sprzeczna z tym, co napisano na stronie wiki. Proszę opracować.
null
„Programowanie strukturalne” jest terminem sztuki o konkretnym znaczeniu, a OP jest obiektywnie poprawny, że wersja nr 2 jest zgodna z zasadami programowania strukturalnego, podczas gdy wersja nr 1 nie. Z twojej odpowiedzi wydawało się, że nie znasz terminu sztuki i interpretujesz go dosłownie. Nie jestem pewien, dlaczego mój komentarz jest pasywno-agresywny; Miałem to po prostu jako informacyjny.
ruakh
@ruakh Nie zgadzam się, że wersja 2 jest bardziej zgodna z zasadami w każdym aspekcie i wyjaśniłam to w mojej odpowiedzi.
null
Mówisz „nie zgadzam się”, jakby to była subiektywna sprawa, ale tak nie jest. Powrót z wnętrza pętli jest kategorycznym naruszeniem zasad programowania strukturalnego. Jestem pewien, że wielu entuzjastów programowania strukturalnego jest fanami zmiennych o minimalnym zasięgu, ale jeśli zmniejszysz zakres zmiennej, odchodząc od programowania strukturalnego, to odejdziesz od programowania strukturalnego, kropki, a zmniejszenie zakresu zmiennej nie cofnie że.
ruakh
2

Dwie pętle mają inną semantykę:

  • Pierwsza pętla po prostu odpowiada na proste pytanie typu tak / nie: „Czy tablica zawiera obiekt, którego szukam?” Robi to w możliwie najkrótszy sposób.

  • Druga pętla odpowiada na pytanie: „Jeśli tablica zawiera szukany obiekt, jaki jest indeks pierwszego dopasowania?” Ponownie robi to w możliwie najkrótszy sposób.

Ponieważ odpowiedź na drugie pytanie zawiera ściśle więcej informacji niż odpowiedź na pierwsze, możesz wybrać odpowiedź na drugie pytanie, a następnie uzyskać odpowiedź na pierwsze pytanie. W return i < array.length;każdym razie tak właśnie działa linia .

Uważam, że zwykle najlepiej jest po prostu użyć narzędzia, które pasuje do tego celu, chyba że możesz ponownie użyć już istniejącego, bardziej elastycznego narzędzia. To znaczy:

  • Korzystanie z pierwszego wariantu pętli jest w porządku.
  • Zmiana pierwszego wariantu, aby ustawić boolzmienną i break, również jest w porządku. (Unika drugiej returninstrukcji, odpowiedź jest dostępna w zmiennej zamiast funkcji return).
  • Używanie std::findjest w porządku (ponowne użycie kodu!).
  • Jednak wyraźne kodowanie znaleziska, a następnie redukcja odpowiedzi do „ boolnie”, nie jest.
cmaster - przywróć monikę
źródło
Byłoby miło, gdyby downvoters zostawili komentarz ...
cmaster - przywróć monikę
2

Zasugeruję trzecią opcję w ogóle:

return array.find(value);

Istnieje wiele różnych powodów iteracji po tablicy: Sprawdź, czy istnieje konkretna wartość, przekształć tablicę w inną tablicę, oblicz wartość zagregowaną, odfiltruj niektóre wartości z tablicy ... Jeśli używasz zwykłej pętli for, nie jest jasne na pierwszy rzut oka, w jaki sposób używana jest pętla for. Jednak większość współczesnych języków ma bogate interfejsy API w swoich strukturach tablicowych, które czynią te różne zamiary bardzo wyraźnymi.

Porównaj przekształcanie jednej tablicy w drugą za pomocą pętli for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

i za pomocą mapfunkcji w stylu JavaScript :

array.map((value) => value * 2);

Lub sumując tablicę:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

przeciw:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Jak długo zajmuje ci zrozumienie, co to robi?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

przeciw

array.filter((value) => (value >= 0));

We wszystkich trzech przypadkach, mimo że pętla for jest z pewnością czytelna, musisz poświęcić kilka chwil, aby dowiedzieć się, jak używana jest pętla for i sprawdzić, czy wszystkie liczniki i warunki wyjścia są poprawne. Nowoczesne funkcje w stylu lambda sprawiają, że cele pętli są wyjątkowo wyraźne i wiadomo na pewno, że wywoływane funkcje API są poprawnie zaimplementowane.

Większość współczesnych języków, w tym JavaScript , Ruby , C # i Java , używa tego stylu funkcjonalnej interakcji z tablicami i podobnymi kolekcjami.

Ogólnie rzecz biorąc, chociaż nie sądzę, aby korzystanie z pętli było koniecznie niewłaściwe i jest to kwestia osobistego gustu, zdecydowanie sprzyjam używaniu tego stylu pracy z tablicami. Wynika to szczególnie z większej przejrzystości w określaniu, co robi każda pętla. Jeśli twój język ma podobne funkcje lub narzędzia w swoich standardowych bibliotekach, sugeruję rozważenie przyjęcia tego stylu!

Kevin
źródło
2
Polecając array.findnasuwa się pytanie, jak mamy wówczas do omówienia najlepszego sposobu wdrożenia array.find. Jeśli nie używasz sprzętu z wbudowaną findoperacją, musimy tam napisać pętlę.
Barmar,
2
@Barmar Nie zgadzam się. Jak wskazałem w mojej odpowiedzi, wiele często używanych języków zapewnia funkcje takie jak findw ich standardowych bibliotekach. Niewątpliwie biblioteki te implementują findi używają ich pętli dla pętli, ale to właśnie robi dobra funkcja: oddziela szczegóły techniczne od konsumenta funkcji, pozwalając programiście nie myśleć o tych szczegółach. Tak więc, mimo że findprawdopodobnie jest implementowany z pętlą for, nadal pomaga uczynić kod bardziej czytelnym, a ponieważ często znajduje się w standardowej bibliotece, użycie go nie powoduje znaczącego obciążenia ani ryzyka.
Kevin,
4
Ale inżynier oprogramowania musi wdrożyć te biblioteki. Czy te same zasady inżynierii oprogramowania nie dotyczą autorów bibliotek jak programistów aplikacji? Pytanie dotyczy ogólnie pisania pętli, a nie najlepszego sposobu wyszukiwania elementu tablicy w określonym języku
Barmar
4
Innymi słowy, poszukiwanie elementu tablicy jest tylko prostym przykładem, którego użył do zademonstrowania różnych technik zapętlania.
Barmar,
-2

Wszystko sprowadza się do dokładnie tego, co należy rozumieć przez „lepiej”. Dla praktycznych programistów ogólnie oznacza to wydajność - tj. W tym przypadku wyjście bezpośrednio z pętli pozwala uniknąć jednego dodatkowego porównania, a zwrócenie stałej logicznej pozwala uniknąć podwójnego porównania; to oszczędza cykle. Dijkstra jest bardziej zainteresowany tworzeniem kodu, który będzie łatwiejszy do udowodnienia . [wydawało mi się, że edukacja CS w Europie traktuje „sprawdzanie poprawności kodu” znacznie poważniej niż edukacja CS w Stanach Zjednoczonych, gdzie siły ekonomiczne zdominowały praktykę kodowania]

PMar
źródło
3
PMar, pod względem wydajności obie pętle są prawie równoważne - oba mają dwa porównania.
Danila Piatov,
1
Jeśli ktoś naprawdę troszczy się o wydajność, zastosuje szybszy algorytm. np. posortuj tablicę i przeprowadź wyszukiwanie binarne lub użyj Hashtable.
user949300,
Danila - nie wiesz, co za tym kryje się struktura danych. Iterator jest zawsze szybki. Dostęp indeksowany może być czasem liniowym, a długość nawet nie musi istnieć.
gnasher729