Czy należy unikać <= i> = podczas używania liczb całkowitych, na przykład w pętli For? [Zamknięte]

15

Wyjaśniłem moim uczniom, że testowanie równe nie jest wiarygodne dla zmiennych zmiennoprzecinkowych, ale jest dobre dla liczb całkowitych. W podręczniku, którego używam, jest napisane, że łatwiej jest czytać> oraz <niż> = i <=. Zgadzam się do pewnego stopnia, ale w pętli For? Czy nie jest bardziej jasne, aby pętla określała wartości początkowe i końcowe?

Czy brakuje mi czegoś, co autor podręcznika ma rację?

Innym przykładem są testy zasięgu, takie jak:

jeśli wynik> 89 ocena = „A”
, jeśli wynik> 79 ocena = „B” ...

Dlaczego po prostu nie powiesz: jeśli wynik> = 90?


źródło
11
Niestety, ponieważ nie ma obiektywnej różnicy w zachowaniu między tymi opcjami, sprowadza się to do badania opinii na temat tego, co ludzie uważają za bardziej intuicyjne, a ankiety nie są odpowiednie dla witryn StackExchange takich jak ta.
Ixrec,
1
To nie ma znaczenia Naprawdę.
Auberon,
4
W rzeczywistości jest to obiektywnie odpowiedzialne. Czuwaj ...
Robert Harvey
9
@Ixrec Zawsze uważam za interesujące, że „najlepsza praktyka” nie jest uważana za odpowiedni temat. Kto nie chce ulepszyć lub uczynić swojego kodu bardziej czytelnym? Jeśli ludzie się nie zgadzają, wszyscy dowiadujemy się więcej o tym problemie i możemy ... nawet ... zmienić nasze zdanie! Ugh, tak trudno było powiedzieć. Robert Harvey mówi, że może odpowiedzieć na to obiektywnie. To będzie interesujące.
1
@nocomprende Głównie dlatego, że „najlepsza praktyka” jest wyjątkowo niejasnym i nadużywanym terminem, który może odnosić się do przydatnych porad opartych na obiektywnych faktach dotyczących języka, ale równie często odnosi się do „najpopularniejszej opinii” (lub opinii tego, kto używa tego terminu) gdy w rzeczywistości wszystkie opcje są jednakowo ważne i nieważne. W takim przypadku możesz jedynie obiektywnie argumentować, ograniczając odpowiedź do pewnych rodzajów pętli, tak jak robił to Robert, i jak zauważyłeś w komentarzu, który nie do końca odpowiada na pytanie.
Ixrec,

Odpowiedzi:

36

W kręconych językach programowania z tablicami zerowymi zwykle zapisuje się fortakie pętle:

for (int i = 0; i < array.Length, i++) { }

Przechodzi przez wszystkie elementy w tablicy i jest zdecydowanie najczęstszym przypadkiem. Unika używania <=lub >=.

Jedyny moment, który musiałby to zmienić, to konieczność pominięcia pierwszego lub ostatniego elementu, przejścia w przeciwnym kierunku lub przejścia z innego punktu początkowego lub innego punktu końcowego.

W przypadku kolekcji w językach, które obsługują iteratory, częściej można to zobaczyć:

foreach (var item in list) { }

Co całkowicie eliminuje porównania.

Jeśli szukasz twardej i szybkiej reguły, kiedy użyć <=vs <, nie ma takiej; użyj tego, co najlepiej wyraża Twoją intencję. Jeśli twój kod musi wyrażać pojęcie „mniej niż lub równe 55 mil na godzinę”, to znaczy <=, że nie <.

>= 90Bardziej sensowne jest udzielenie odpowiedzi na pytanie dotyczące zakresów ocen , ponieważ 90 to rzeczywista wartość brzegowa, a nie 89.

Robert Harvey
źródło
9
Nie omija całkowicie porównań, po prostu zamiata je pod dywan, przenosząc je do implementacji modułu wyliczającego. : P
Mason Wheeler
2
Oczywiście, ale to już nie przemierza tablicy, prawda?
Robert Harvey
4
Ponieważ tablice są najczęstszym przypadkiem użycia dla fortakich pętli. Podana tutaj forma forpętli będzie natychmiast rozpoznawalna dla każdego programisty z odrobiną doświadczenia. Jeśli chcesz uzyskać bardziej szczegółową odpowiedź w oparciu o bardziej szczegółowy scenariusz, musisz uwzględnić ją w swoim pytaniu.
Robert Harvey
3
„użyj tego, co najlepiej wyraża Twoją intencję” <- This!
Jasper N. Brouwer,
3
@ JasperN.Brouwer To jest jak zerowe prawo programowania. Wszystkie reguły stylów i konwencje kodowania upadają z największą wstydem, gdy ich mandaty są sprzeczne z przejrzystością kodu.
Nie będę istniał Idonotexist
16

To nie ma znaczenia

Ale ze względu na argument, przeanalizujmy dwie opcje: a > bvs a >= b.

Wytrzymać! To nie jest równoważne!
OK, a następnie a >= b -1vs a > blub a > bvs a >= b +1.

Hm, a >bi a >= boba wyglądają lepiej niż a >= b - 1i a >= b +1. Co to w ogóle 1jest? Argumentowałbym więc, że wszelkie korzyści z posiadania >zamiast >=lub odwrotnie są eliminowane przez dodawanie lub odejmowanie losowych liczb 1s.

Ale co jeśli to liczba? Czy lepiej powiedzieć a > 7czy a >= 6? Poczekaj sekundę. Czy poważnie spieramy się, czy lepiej używać >vs >=i ignorować zmienne zakodowane na stałe? Tak naprawdę staje się pytanie, czy a > DAYS_OF_WEEKjest lepsze niż a >= DAYS_OF_WEEK_MINUS_ONE... czy to jest a > NUMBER_OF_LEGS_IN_INSECT_PLUS_ONEvs a >= NUMBER_OF_LEGS_IN_INSECT? Wróciliśmy do dodawania / odejmowania 1s, tylko tym razem w nazwach zmiennych. A może debatowanie, czy najlepiej zastosować próg, limit, maksimum.

I wygląda na to, że nie ma ogólnej zasady: zależy od tego, co jest porównywane

Ale tak naprawdę są o wiele ważniejsze rzeczy do poprawienia w kodzie i znacznie bardziej obiektywne i rozsądne wytyczne (np. Limit znaków X na linię), które wciąż mają wyjątki.

Thanos Tintinidis
źródło
1
OK, nie ma ogólnej zasady. (Zastanawiam się, dlaczego podręcznik wydawał się określać ogólną zasadę, ale mogę go puścić). Nie ma zgodnej opinii, że to była notatka, którą przegapiłem.
2
@nocomprende, ponieważ standardy kodowania i formatowania są zarówno subiektywne, jak i bardzo kontrowersyjne. Przez większość czasu żadne z nich nie ma znaczenia, ale programiści wciąż toczą nad nimi święte wojny. (mają znaczenie tylko wtedy, gdy niechlujny kod może powodować błędy)
1
Widziałem wiele szalonych dyskusji na temat standardów kodowania, ale ta może po prostu wziąć ciasto. Naprawdę głupiutki.
JimmyJames
@JimmyJames dotyczy dyskusji na temat >vs >=czy dyskusji na temat tego, czy dyskusja na temat >vs >=jest znacząca? chociaż prawdopodobnie najlepiej unikać omawiania tego: p
Thanos Tintinidis,
2
„Programy mają być odczytywane przez ludzi i tylko przypadkowo w celu uruchomienia komputerów” - Donald Knuth. Używaj stylu, który ułatwia czytanie, zrozumienie i utrzymanie.
simpleuser
7

Obliczeniowo nie ma różnicy w kosztach przy użyciu <lub w >porównaniu do <=lub >=. Jest obliczany równie szybko.

Jednak większość pętli będzie liczyła od 0 (ponieważ wiele języków używa 0 indeksowania dla swoich tablic). Tak więc kanoniczna pętla for w tych językach jest

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

zrobienie tego za pomocą <=wymagałoby dodania gdzieś -1, aby uniknąć błędu off-by one

for(int i = 1; i <= length; i++){
   array[i-1] = //...
   //...
}

lub

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

Oczywiście, jeśli język używa indeksowania 1, wówczas użyłbyś <= jako warunku ograniczającego.

Kluczem jest to, że wartości wyrażone w warunku są tymi z opisu problemu. Czytanie jest łatwiejsze

if(x >= 10 && x < 20){

} else if(x >= 20 && x < 30){

}

na półotwartą przerwę niż

if(x >= 10 && x <= 19){

} else if(x >= 20 && x <= 29){

}

i muszę wykonać matematykę, aby wiedzieć, że nie ma żadnej możliwej wartości między 19 a 20

maniak zapadkowy
źródło
Widzę ideę „długości”, ale co, jeśli używasz tylko wartości, które skądś pochodzą i mają na celu iterację od wartości początkowej do wartości końcowej. Przykładem klasy był procent marży od 5 do 10. Czy nie jest łatwiej powiedzieć: for (markup = 5; markup <= 10; markup ++) ... ? Dlaczego miałbym przejść na test: markup <11 ?
6
@nocomprende nie zrobiłbyś tego, jeśli wartości graniczne z opisu problemu wynoszą 5 i 10, to lepiej, aby były one wyraźnie zawarte w kodzie, a nie nieznacznie zmienione.
maniak zapadkowy
@nocomprende Prawo format jest bardziej oczywiste, gdy górna granica to parametr: for(markup = 5; markup <= MAX_MARKUP; ++markup). Wszystko inne by to skomplikowało.
Navin,
1

Powiedziałbym, że nie chodzi o to, czy powinieneś użyć> czy> =. Chodzi o to, aby użyć czegokolwiek, co pozwala pisać ekspresyjny kod.

Jeśli uznasz, że musisz dodać / odjąć jeden, rozważ użycie drugiego operatora. Uważam, że dobre rzeczy dzieją się, gdy zaczynasz od dobrego modelu swojej domeny. Wtedy logika się pisze.

bool IsSpeeding(int kilometersPerHour)
{
    const int speedLimit = 90;
    return kilometersPerHour > speedLimit;
}

To jest o wiele bardziej wyraziste niż

bool IsSpeeding(int kilometersPerHour)
{
    const int speedLimit = 90;
    return kilometersPerHour >= (speedLimit + 1);
}

W innych przypadkach preferowany jest inny sposób:

bool CanAfford(decimal price, decimal balance)
{
    return balance >= price;
}

O wiele lepszy niż

bool CanAfford(decimal price, decimal balance)
{
    const decimal epsilon = 0e-10m;
    return balance > (price - epsilon);
}

Proszę wybaczyć „prymitywną obsesję”. Oczywiście chciałbyś tutaj użyć odpowiednio typu Velocity i Money, ale pominąłem je dla zwięzłości. Chodzi o to: użyj wersji, która jest bardziej zwięzła i pozwala skupić się na problemie biznesowym, który chcesz rozwiązać.

sara
źródło
2
Przykład ograniczenia prędkości jest złym przykładem. Osiągnięcie prędkości 90 km / h w strefie 90 km / h nie jest uważane za przekroczenie prędkości. Ograniczenie prędkości jest wliczone.
eidsonator,
masz absolutną rację, pierdnięcie z mojej strony. edytuj go, aby użyć lepszego przykładu, mam nadzieję, że nie jest to całkowicie stracone.
sara,
0

Jak wskazałeś w swoim pytaniu, testowanie równości zmiennych zmiennoprzecinkowych nie jest wiarygodne.

To samo dotyczy <=i >=.

Jednak nie ma takiego problemu z niezawodnością dla typów całkowitych. Moim zdaniem autorka wyraziła swoją opinię na temat tego, co jest bardziej czytelne.

To, czy się z nim zgadzasz, jest oczywiście Twoją opinią.

Dan Pichelman
źródło
Czy jest to pogląd ogólnie uznawany przez innych autorów i siwe włosy w terenie (właściwie mam siwe włosy)? Jeśli jest to tylko „Opinia jednego reportera”, mogę to powiedzieć uczniom. Właściwie to mam. Nigdy wcześniej nie słyszałem o tym pomyśle.
Płeć autora nie ma znaczenia, ale i tak zaktualizowałem swoją odpowiedź. Wolę wybierać <lub <=bazować na tym, co najbardziej naturalne dla konkretnego problemu, który rozwiązuję. Jak zauważyli inni, w pętli FOR <ma większy sens w językach typu C. Istnieją inne przypadki użycia, które faworyzują <=. Skorzystaj ze wszystkich dostępnych narzędzi, kiedy i gdzie to stosowne.
Dan Pichelman,
Nie zgadzać się. Nie mogą być problemy z niezawodnością typów całkowitych: w C, należy rozważyć: for (unsigned int i = n; i >= 0; i--)czy for (unsigned int i = x; i <= y; i++)jeśli ydzieje się UINT_MAX. Ups, te pętle na zawsze.
jamesdlin,
-1

Każdy z relacji <, <=, >=, >a także == i !=mają swoje przypadków użycia do porównywania dwóch wartości zmiennoprzecinkowych. Każdy ma określone znaczenie i należy wybrać odpowiedni.

Podam przykłady przypadków, w których chcesz dokładnie tego operatora dla każdego z nich. (Pamiętaj jednak o NaN).

  • Masz drogą czystą funkcję, fktóra przyjmuje zmiennoprzecinkową wartość jako dane wejściowe. Aby przyspieszyć obliczenia, zdecydujesz się dodać cache ostatnio obliczonych wartości, czyli odwzorowanie tabeli odnośników xdo f(x). Naprawdę będziesz chciał użyć ==do porównania argumentów.
  • Chcesz wiedzieć, czy możesz znacznie podzielić przez liczbę x? Prawdopodobnie chcesz użyć x != 0.0.
  • Chcesz wiedzieć, czy wartość xmieści się w przedziale jednostkowym? (x >= 0.0) && (x < 1.0)jest prawidłowy warunek.
  • Obliczyłeś wyznacznik dmacierzy i chcesz powiedzieć, czy jest ona pozytywnie określona? Nie ma powodu, aby używać niczego innego d > 0.0.
  • Chcesz wiedzieć, czy Σ n = 1,…, ∞ n −α jest rozbieżna? Przetestowałbym alpha <= 1.0.

Matematyka zmiennoprzecinkowa (ogólnie) nie jest dokładna. Ale to nie znaczy, że powinieneś się go bać, traktować go jak magię, a na pewno nie zawsze traktować dwie zmiennoprzecinkowe wielkości, jeśli są w środku 1.0E-10. Takie postępowanie naprawdę złamie Twoją matematykę i spowoduje, że wydarzy się wiele dziwnych rzeczy.

  • Na przykład, jeśli użyjesz rozmytego porównania ze wspomnianą wyżej pamięcią podręczną, wprowadzisz zabawne błędy. Co gorsza, funkcja nie będzie już czysta, a błąd zależy od wcześniej obliczonych wyników!
  • Tak, nawet jeśli x != 0.0i yjest skończoną wartością zmiennoprzecinkową, y / xnie musi być skończony. Warto jednak wiedzieć, czy y / xnie jest ona skończona z powodu przepełnienia, czy dlatego, że operacja nie była matematycznie dobrze zdefiniowana na początek.
  • Jeśli masz funkcję, która ma warunek wstępny, że parametr wejściowy xmusi znajdować się w przedziale jednostkowym [0, 1), byłbym naprawdę zdenerwowany, gdyby wywołał błąd potwierdzenia podczas wywołania za pomocą x == 0.0lub x == 1.0 - 1.0E-14.
  • Wyznaczone przez ciebie wyznaczniki mogą być niedokładne. Ale jeśli zamierzasz udawać, że macierz nie jest pozytywnie określona, ​​gdy obliczona jest wyznacznik 1.0E-30, nic nie zyskuje. Wszystko, co zrobiłeś, to zwiększyło prawdopodobieństwo udzielenia złej odpowiedzi.
  • Tak, twój argument alphamoże być obciążony błędami zaokrąglania i dlatego alpha <= 1.0może być prawdziwy, nawet jeśli prawdziwa wartość matematyczna wyrażenia alphazostała obliczona, może być naprawdę większa niż 1. Ale w tym momencie nie możesz nic na to poradzić.

Jak zawsze w inżynierii oprogramowania, obsługuj błędy na odpowiednim poziomie i usuwaj je tylko raz. Jeśli dodasz błędy zaokrąglania w kolejności 1.0E-10(wydaje się, że jest to magiczna wartość, której większość ludzi używa, nie wiem dlaczego) za każdym razem, gdy porównujesz liczby zmiennoprzecinkowe, wkrótce wystąpią błędy w kolejności 1.0E+10

5gon12eder
źródło
1
Najwybitniejsza odpowiedź, choć trzeba przyznać, tylko na inne pytanie niż zadane.
Dewi Morgan,
-2

Rodzaj warunkowy stosowany w pętli może ograniczać rodzaje optymalizacji, które kompilator może wykonać, na lepsze lub na gorsze. Na przykład biorąc pod uwagę:

uint16_t n = ...;
for (uint16_t i=1; i<=n; i++)
  ...  [loop doesn't modify i]

kompilator mógłby założyć, że powyższy warunek powinien spowodować zakończenie pętli po pętli n -tego przejścia, chyba że n może 65535, a pętla może wyjść w inny sposób niż przez przekroczenie n. Jeśli te warunki mają zastosowanie, kompilator musi wygenerować kod, który spowodowałby uruchomienie pętli, dopóki coś innego niż powyższy warunek nie spowoduje jej wyjścia.

Jeśli zamiast tego pętla została zapisana jako:

uint16_t n = ...;
for (uint16_t ctr=0; ctr<n; ctr++)
{
  uint16_t i = ctr+1;
  ... [loop doesn't modify ctr]
}

wtedy kompilator mógłby bezpiecznie założyć, że pętla nigdy nie będzie musiała wykonywać więcej niż n razy, a zatem może być w stanie wygenerować bardziej wydajny kod.

Pamiętaj, że każde przepełnienie podpisanymi typami może mieć paskudne konsekwencje. Dany:

int total=0;
int start,lim,mult; // Initialize values somehow...
for (int i=start; i<=lim; i++)
  total+=i*mult;

Kompilator może przepisać to jako:

int total=0;
int start,lim,mult; // Initialize values somehow...
int loop_top = lim*mult;
for (int i=start; i<=loop_top; i+=mult)
  total+=i;

Taka pętla zachowywałaby się identycznie jak oryginał, gdyby w obliczeniach nie wystąpiło przepełnienie, ale mogłaby działać wiecznie, nawet na platformach sprzętowych, gdzie przepełnienie liczb całkowitych normalnie miałoby spójną semantykę zawijania.

supercat
źródło
Tak, myślę, że jest trochę dalej w podręczniku. Jeszcze nie wzięto pod uwagę. Optymalizacje kompilatora są przydatne do zrozumienia i należy unikać przepełnienia, ale tak naprawdę nie była to motywacja do mojego pytania, które dotyczyło bardziej tego, jak ludzie rozumieją kod. Czy łatwiej jest odczytać x> 5 lub x> = 6? Cóż, to zależy ...
O ile nie piszesz dla Arduino, maksymalna wartość int będzie nieco wyższa niż 65535.
Corey
1
@Corey: Maksymalna wartość dla uint16_t będzie wynosić 65535 na dowolnej platformie, na której istnieje typ; Użyłem uint16_t w pierwszym przykładzie, ponieważ spodziewałbym się, że więcej osób zna dokładną maksymalną wartość uint16_t niż uint32_t. Ten sam punkt miałby zastosowanie w obu przypadkach. Drugi przykład jest agnostyczny w odniesieniu do typu „int” i reprezentuje krytyczny punkt behawioralny, którego wiele osób nie rozpoznaje na temat współczesnych kompilatorów.
supercat,