Что будет, если переменную разделить и сразу умножить на одно и то же число
easy

Что будет, если переменную разделить и сразу умножить на одно и то же число

Будет ошибка, которую чертовски сложно отловить

В одном из ИТ-пабликов мы увидели такой код на JavaScript:

> 7110 / 100 * 100 === 7110
< false
> 7120 / 100 * 100 === 7120
> true

Читается это так: сначала число 7110 делится на 100 и умножается на сто. Результат деления сравнивается с числом 7110, и JavaScript говорит, что результат не равен. Как будто если разделить на 100 и тут же умножить на 100, ты получишь не то же самое число, с которого начинал.

Во втором примере то же самое, но с числом 7120. Если его разделить на 100 и умножить на 100, получится ровно 7120. Получается, что одни и те же математические действия в двух случаях дали разные результаты. Как такое возможно? 

Если разобрать этот код, мы увидим, что ошибки нет, — но нужно понимать, как работает JavaScript.

Деление и дробные числа

Когда мы делим одно число на другое и они не делятся нацело, то получаем дробное число — целую часть и то, что идёт после запятой. Но компьютер не использует стандартное школьное деление в столбик — вместо этого он представляет число в виде последовательности нулей и единиц и использует побитовые операции для деления. 

Это значит, что он не останавливается, например, после точного вычисления 7110 / 100 = 71,1, а работает со всеми битами сразу. После такого деления у компьютера получается последовательность, например 11101011011011101, где 11010 — это целая часть, а всё остальное — дробная. Если ему понадобится в целой части хранить число побольше, то он просто возьмёт дополнительное место за счёт дробной части. 

Получается, что запятая в такой переменной как бы плавает в зависимости от знаков до запятой, отсюда и название — «число с плавающей запятой» (floating point по-английски). Но когда компьютер забирает разряды у дробной части, он иногда может этим внести небольшую погрешность, например потерять последнюю цифру в дробной части (например, одну миллиардную).

Как точность деления влияет на умножение

Когда мы после деления умножаем результат на 100, то с точки зрения компьютера это просто побитовый сдвиг точки вправо на несколько разрядов. Если у нас всё было посчитано точно, то результат будет таким же, что и до деления.

Но всё дело в том, что иногда компьютер не может что-то поделить, хотя с точки зрения математики там всё просто. В этом случае он заполняет результатами вычисления все доступные нули и единицы в переменной, а остальное отбрасывает. Это значит, что результат уже получился неточный, а приблизительный, и дальше ошибка будет только расти.

Давайте посмотрим, что получается в каждом случае после деления:

Что будет, если переменную разделить и сразу умножить на одно и то же число

В первом случае компьютер не смог поделить 7110 на 100 без остатка, поэтому при умножении он потащил за собой девятки после запятой. Отсюда и неточность при сравнении. 

Как исправить

В JavaScript есть объект Math, который занимается всякой полезной математикой. И у этого объекта есть метод .round(), который может корректно округлить число до ближайшего целого. Зная о возможной ошибке в коде, нам стоит использовать это округление: 

>Math.round(7110/100*100)===7110
<true
>Math.round(7120/100*100)===7120
<true

Где это может пригодиться

Обратите внимание на этот эффект, если пишете программу, в которой используется деление непредсказуемых чисел — например, если пользователь вводит что-то с клавиатуры, а вы потом совершаете с этими числами свои операции. Например, вы получили рост человека, поделили его на какой-то внутренний коэффициент и сравниваете со своими референсными значениями. Сделайте поправку на то, что при делении могла сломаться точность, и либо округляйте число, либо предусматривайте запасы при сравнении. 

Кстати, ровно для этого и существуют тестировщики. Их задача, в частности, в том, чтобы прогнать сквозь программу все возможные варианты значений, которые может ввести пользователь, и отловить вот такие ошибки вычислений. Чаще всего для такого используют автоматические тесты — но чтобы понимать, что тестировать, нужно знать, где может быть ошибка. Теперь вы знаете. 

Бонус: строгое сравнение ===

В нашем коде используется строгое сравнение, которое в JavaScript обозначается тремя знаками равенства. Строгое сравнение означает, что сравниваются не только значения, но и типы сравниваемых данных. Грубо говоря, число 1 и строка с символом ‘1’ с точки зрения строгого сравнения — разные вещи, хотя для людей внешне это одно и то же. 

Есть ещё нестрогий оператор ==. Вот как он работает: 

  1. Берёт оба аргумента сравнения.
  2. Смотрит, к какому единому общем типу данных их можно привести. Например, сделать оба аргумента строками, числами или превратить их в логические элементы.
  3. Сравнивает данные одного типа и понимает, равны они или нет.

В обычных ситуациях сравнение работает хорошо и мы даже не задумываемся о том, как оно устроено внутри. Но иногда нужно точно выяснить, одинаковые ли у нас аргументы по всем параметрам или нет — вот для этого и используется оператор строгого сравнения ===. Он работает так:

  1. Берёт первый аргумент и выясняет его тип — целое число, дробное, логический тип и так далее.
  2. Делает то же самое со вторым аргументом.
  3. Сравнивает их типы между собой. Если они равны — переходит к дальнейшему сравнению. Если не равны — возвращает false.
  4. Если типы равны, то сравнивает значения — и тоже возвращает false, если значения не совпадают между собой.

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Алина Грызлова

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Вам может быть интересно
easy