Na przykład chcę wyświetlić listę przycisków od 0,0,5, ... 5, która przeskakuje za każde 0,5. Używam do tego pętli for i mam inny kolor pod przyciskiem STANDARD_LINE:
var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;
for(var i=0;i<=MAX;i=i+DIFF){
button.text=i+'';
if(i==STANDARD_LINE){
button.color='red';
}
}
W tym przypadku nie powinno być żadnych błędów zaokrąglania, ponieważ każda wartość jest dokładna w IEEE 754, ale walczę, czy powinienem ją zmienić, aby uniknąć porównania równości zmiennoprzecinkowej:
var MAX=10;
var STANDARD_LINE=3;
for(var i=0;i<=MAX;i++){
button.text=i/2.0+'';
if(i==STANDARD_LINE/2.0){
button.color='red';
}
}
Z jednej strony oryginalny kod jest prostszy i przesłany do mnie. Ale zastanawiam się nad jedną rzeczą: czy ja == STANDARD_LINE wprowadza w błąd młodszych kolegów z drużyny? Czy ukrywa to, że liczby zmiennoprzecinkowe mogą zawierać błędy zaokrąglania? Po przeczytaniu komentarzy do tego postu:
wydaje się, że wielu programistów nie wie, że niektóre liczby zmiennoprzecinkowe są dokładne. Czy powinienem unikać porównywania liczb zmiennoprzecinkowych, nawet jeśli jest to ważne w moim przypadku? A może zastanawiam się nad tym?
i
zawsze będą to liczby całkowite na drugiej liście. Spróbuj usunąć drugi/2.0
.button
nic się nie zmienia w twojej pętli. Jak można wyświetlić listę przycisków? Poprzez indeks do tablicy lub inny mechanizm? Jeśli jest to przez dostęp indeksu do tablicy, jest to kolejny argument przemawiający za przejściem na liczby całkowite.Odpowiedzi:
Zawsze unikałbym kolejnych operacji zmiennoprzecinkowych, chyba że wymaga tego model, który obliczam. Arytmetyka zmiennoprzecinkowa jest nieintuicyjna dla większości i jest głównym źródłem błędów. Mówienie o przypadkach, w których powoduje błędy od tych, których nie ma, jest jeszcze bardziej subtelnym rozróżnieniem!
Dlatego użycie liczb zmiennoprzecinkowych jako liczników pętli jest defektem, który czeka na wystąpienie i wymagałby co najmniej grubego komentarza w tle wyjaśniającego, dlaczego warto tutaj używać 0,5, i że zależy to od konkretnej wartości liczbowej. W tym momencie przepisanie kodu w celu uniknięcia liczników zmiennoprzecinkowych będzie prawdopodobnie bardziej czytelną opcją. A czytelność jest obok poprawności w hierarchii wymagań zawodowych.
źródło
DIFF must be an exactly-representable double that evenly divides STANDARD_LINE
. Jeśli nie chcesz pisać tego komentarza (i polegać na tym, że wszyscy przyszli programiści wiedzą wystarczająco dużo o zmiennoprzecinkowym systemie IEEE754 binary64, aby go zrozumieć), nie pisz tego kodu w ten sposób. tzn. nie pisz kodu w ten sposób. Zwłaszcza, że prawdopodobnie nie jest nawet bardziej wydajny: dodawanie FP ma większe opóźnienie niż dodawanie liczb całkowitych i jest zależnością od pętli. Ponadto kompilatory (nawet kompilatory JIT?) Prawdopodobnie lepiej radzą sobie z tworzeniem pętli z licznikami całkowitymi.Zasadniczo pętle należy pisać w taki sposób, aby myśleć o zrobieniu czegoś n razy. Jeśli używasz wskaźników zmiennoprzecinkowych, nie jest to już kwestia robienia czegoś n razy, ale raczej bieganie do momentu spełnienia warunku. Jeśli ten warunek jest bardzo podobny do tego,
i<n
którego spodziewa się tak wielu programistów, kod wydaje się robić jedną rzecz, podczas gdy faktycznie robi inną, co może być łatwo błędnie zinterpretowane przez programistów przeglądających kod.Jest to nieco subiektywne, ale moim skromnym zdaniem, jeśli możesz przepisać pętlę, aby użyć indeksu liczb całkowitych do zapętlenia określonej liczby razy, powinieneś to zrobić. Rozważ więc następującą alternatywę:
Pętla działa pod względem liczb całkowitych. W tym przypadku
i
jest liczbą całkowitą iSTANDARD_LINE
jest również wymuszana na liczbę całkowitą. To oczywiście zmieniłoby położenie linii standardowej, gdyby zdarzyło się zaokrąglić i podobnieMAX
, więc powinieneś nadal starać się zapobiegać zaokrągleniu w celu dokładnego renderowania. Jednak nadal masz tę zaletę, że zmieniasz parametry w kategoriach pikseli, a nie liczb całkowitych, bez martwienia się o porównanie punktów zmiennoprzecinkowych.źródło
i
iSTANDARD_LINE
tylko patrzeć jak całkowitymi. Nie ma przymusu w ogóle, aDIFF
,MAX
aSTANDARD_LINE
to wszystko po prostuNumber
s.Number
s używane jako liczby całkowite powinny być bezpieczne poniżej2**53
, ale wciąż są liczbami zmiennoprzecinkowymi.Zgadzam się ze wszystkimi pozostałymi odpowiedziami, że użycie zmiennej pętli niecałkowitej jest ogólnie złym stylem, nawet w przypadkach takich jak ta, w której będzie działać poprawnie. Ale wydaje mi się, że jest jeszcze jeden powód, dla którego jest to zły styl.
Twój kod „wie”, że dostępne szerokości linii to dokładnie wielokrotności 0,5 od 0 do 5,0. Powinien? Wygląda na to, że decyzja podejmowana przez interfejs użytkownika może się łatwo zmienić (np. Może chcesz, aby odstępy między dostępnymi szerokościami były większe w miarę ich szerokości. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5.0 lub coś takiego).
Twój kod „wie”, że wszystkie dostępne szerokości linii mają „ładne” reprezentacje zarówno jako liczby zmiennoprzecinkowe, jak i dziesiętne. To także wydaje się coś, co może się zmienić. (W pewnym momencie możesz chcieć 0.1, 0.2, 0.3, ...)
Twój kod „wie”, że tekst na przyciskach jest po prostu tym, w co JavaScript zamienia te wartości zmiennoprzecinkowe. To także wydaje się coś, co może się zmienić. (Na przykład, być może kiedyś będziesz potrzebować szerokości takich jak 1/3, których prawdopodobnie nie chciałbyś wyświetlać jako 0,333333333333333 itp. A może chcesz zobaczyć „1.0” zamiast „1”, aby zachować spójność z „1.5” .)
Wszystko to wydaje mi się przejawem pojedynczej słabości, która jest rodzajem mieszania warstw. Te liczby zmiennoprzecinkowe są częścią wewnętrznej logiki oprogramowania. Tekst wyświetlany na przyciskach jest częścią interfejsu użytkownika. Powinny być bardziej osobne niż są w kodzie tutaj. Pojęcia typu „który z nich jest domyślny i należy go wyróżnić?” są sprawami związanymi z interfejsem użytkownika i prawdopodobnie nie powinny być powiązane z tymi zmiennoprzecinkowymi wartościami. Twoja pętla jest naprawdę (a przynajmniej powinna być) pętlą nad przyciskami , a nie nad szerokością linii . Napisane w ten sposób pokusa, by użyć zmiennej pętli przyjmującej wartości niecałkowite, znika: będziesz po prostu używał kolejnych liczb całkowitych lub pętli for ... in / for ....
Mam wrażenie, że większość przypadków, w których można by pokusić się o zapętlanie liczb niecałkowitych, jest taka: istnieją inne powody, całkowicie niezwiązane z numerycznymi zagadnieniami, dlaczego kod powinien być inaczej zorganizowany. (Nie wszystkie przypadki; mogę sobie wyobrazić, że niektóre algorytmy matematyczne mogą być najdokładniej wyrażone w postaci pętli ponad wartościami niecałkowitymi).
źródło
Jeden zapach kodu używa takich pływaków w pętli.
Pętlę można wykonać na wiele sposobów, ale w 99,9% przypadków powinieneś trzymać się przyrostu 1, w przeciwnym razie na pewno będzie zamieszanie, nie tylko przez młodszych programistów.
źródło
Tak, chcesz tego uniknąć.
Liczby zmiennoprzecinkowe są jedną z największych pułapek dla niczego niepodejrzewającego programisty (co z mojego doświadczenia wynika, że prawie wszyscy). Od polegania na testach równości zmiennoprzecinkowej, po reprezentowanie pieniędzy jako zmiennoprzecinkowych - wszystko to jest wielkie bagno. Dodanie jednego pływaka do drugiego jest jednym z największych przestępców. Istnieje wiele tomów literatury naukowej na temat takich rzeczy.
Używaj liczb zmiennoprzecinkowych dokładnie tam, gdzie są one odpowiednie, na przykład podczas wykonywania rzeczywistych obliczeń matematycznych tam, gdzie ich potrzebujesz (takich jak trygonometria, wykreślanie wykresów funkcji itp.) I bądź bardzo ostrożny podczas wykonywania operacji szeregowych. Równość jest na wyciągnięcie ręki. Wiedza na temat tego, który konkretny zestaw liczb jest zgodny ze standardami IEEE, jest bardzo tajemnicza i nigdy bym na nim nie polegał.
W twoim przypadku, nie będzie , zgodnie z prawem Murphys, przyjść do punktu, gdzie zarządzanie chce nie mieć 0.0, 0.5, 1.0 ... ale 0,0, 0,4, 0,8 ... lub cokolwiek; zostaniesz natychmiast przekupiony, a twój młodszy programista (lub sam) będzie debugował długo i mocno, dopóki nie znajdziesz problemu.
W twoim konkretnym kodzie rzeczywiście miałbym zmienną z pętlą całkowitą. Reprezentuje
i
przycisk th, a nie numer bieżący.I prawdopodobnie, dla większej jasności, nie pisałbym,
i/2
alei*0.5
dzięki temu jest jasne, co się dzieje.Uwaga: jak wskazano w komentarzach, JavaScript nie ma osobnego typu dla liczb całkowitych. Ale liczby całkowite do 15 cyfr są gwarantowane jako dokładne / bezpieczne (patrz https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), stąd dla takich argumentów („czy to bardziej mylące / podatne na działanie z liczbami całkowitymi lub niecałkowitymi ”), jest to odpowiednio bliskie oddzielnemu typowi„ ducha ”; w codziennym użytkowaniu (pętle, współrzędne ekranu, indeksy tablic itp.) nie będzie niespodzianek z liczbami całkowitymi reprezentowanymi
Number
jako JavaScript.źródło
Nie sądzę, aby któraś z twoich sugestii była dobra. Zamiast tego wprowadziłbym zmienną dla liczby przycisków na podstawie wartości maksymalnej i odstępów. Następnie wystarczy przejrzeć indeksy samego przycisku.
Może to być więcej kodu, ale jest też bardziej czytelny i bardziej niezawodny.
źródło
Możesz tego uniknąć, obliczając wyświetlaną wartość, zamiast używać licznika pętli jako wartości:
źródło
Arytmetyka zmiennoprzecinkowa jest powolna, a arytmetyka liczb całkowitych jest szybka, więc kiedy używam zmiennoprzecinkowej, nie używałbym jej niepotrzebnie w przypadku, gdy można użyć liczb całkowitych. Przydatne jest zawsze myśleć o liczbach zmiennoprzecinkowych, nawet stałych, jako przybliżonych, z niewielkim błędem. Jest to bardzo przydatne podczas debugowania, aby zastąpić natywne liczby zmiennoprzecinkowe obiektami zmiennoprzecinkowymi plus / minus, w których każdą liczbę traktuje się jako zakres zamiast punktu. W ten sposób odkrywasz rosnące niedokładności po każdej operacji arytmetycznej. Zatem „1,5” należy traktować jako „pewną liczbę między 1,45 a 1,55”, a „1,50” należy traktować jako „pewną liczbę między 1,495 a 1,505”.
źródło