Перейти к содержимому

Уроки · 26

Деление на ноль

В уроке «Направление» мы делим dx на distance, ничего заранее не проверяя. А в уроках «Погоня за мышью» и «Точная остановка» перед тем же самым делением стоит проверка: if distance > 0.0. Разница не случайная — и дело как раз в делении на ноль.

Если курсор окажется ровно в той же точке, что и центр (или кружок) — dx и dy будут равны нулю, а значит и distance тоже станет нулём. Тогда dx / distance — это деление нуля на ноль.

У такого деления нет обычного числового ответа. В Rust это не считается ошибкой и не останавливает программу — просто результат получается особым: он называется NaN (сокращение от английского “not a number” — “не число”). Это не бесконечность и не какая-то конкретная величина — это как бы ответ “непонятно, что здесь получить”.

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

В уроке «Направление» center_x и center_y считаются заново на каждом кадре — мы уже разбирали, что это значит, в «Где живёт переменная». Даже если на одном кадре курсор окажется точно в центре и получится NaN — это касается только этого кадра: на следующем всё считается заново с чистого листа, и как только курсор чуть-чуть отойдёт от центра, NaN пропадёт сам, никак не задержавшись.

А в уроке «Погоня за мышью» x и y — это mut-переменные вне loop: они специально хранят своё значение между кадрами, чтобы кружок помнил, где он был. Если NaN хоть раз попадёт туда, он останется там навсегда — дальнейшие вычисления его не исправляют. Кружок пропадёт с экрана и уже не вернётся, даже когда курсор снова отойдёт. Именно поэтому там стоит if distance > 0.0: она не даёт опасному нулю вообще дойти до деления, пока он не испортил переменную, которая должна помнить своё значение надолго.

Может показаться, что можно обойтись без проверки заранее, а вместо этого поправить x и y, если они уже стали NaN. Но x += direction_x * step — это x = x + direction_x * step: чтобы прибавить что-то к x, Rust сначала берёт то значение, которое в нём уже лежит. Если там NaN, результат снова NaN — += не лечит, потому что обязательно использует уже испорченное значение.

Починить может только прямое присваивание конкретному числу — но тогда встаёт вопрос, какое число подставить, и его пришлось бы отдельно запоминать заранее на всякий случай. Проще не допустить NaN одной проверкой, чем потом чинить уже испорченную переменную.

  • Деление нуля на ноль даёт особое значение NaN — не ошибку и не бесконечность, а “непонятно что”.
  • NaN не проходит сам: всё, что с ним делаешь дальше, тоже становится NaN.
  • Если переменная пересчитывается заново каждый кадр — один NaN не страшен, он забывается вместе с кадром.
  • Если переменная хранит значение между кадрами — NaN, попавший туда однажды, остаётся навсегда. Такие места стоит защищать проверкой заранее, а не полагаться на то, что “само пройдёт”.
  • += и подобные сокращения используют текущее значение переменной, поэтому не лечат NaN. Починить может только прямое присваивание конкретному числу — а придумывать, какому именно, обычно сложнее, чем просто проверить заранее.