Уроки · 26
Деление на ноль
В уроке «Направление» мы делим dx на distance,
ничего заранее не проверяя. А в уроках «Погоня за мышью»
и «Точная остановка» перед тем же самым делением стоит
проверка: if distance > 0.0. Разница не случайная — и дело как раз
в делении на ноль.
Что случается при делении на ноль
Заголовок раздела «Что случается при делении на ноль»Если курсор окажется ровно в той же точке, что и центр (или кружок) —
dx и dy будут равны нулю, а значит и distance тоже станет
нулём. Тогда dx / distance — это деление нуля на ноль.
У такого деления нет обычного числового ответа. В Rust это не
считается ошибкой и не останавливает программу — просто результат
получается особым: он называется NaN (сокращение от английского “not
a number” — “не число”). Это не бесконечность и не какая-то конкретная
величина — это как бы ответ “непонятно, что здесь получить”.
NaN не лечится
Заголовок раздела «NaN не лечится»Как только NaN где-то появился, он не превращается обратно в обычное
число сам по себе. Любое действие с ним — сложение, умножение, что
угодно — тоже даёт NaN. Число, испорченное один раз, остаётся
испорченным при любых дальнейших вычислениях с ним.
Когда это неважно, а когда важно
Заголовок раздела «Когда это неважно, а когда важно»В уроке «Направление» center_x и center_y
считаются заново на каждом кадре — мы уже разбирали, что это значит,
в «Где живёт переменная». Даже
если на одном кадре курсор окажется точно в центре и получится NaN —
это касается только этого кадра: на следующем всё считается заново с
чистого листа, и как только курсор чуть-чуть отойдёт от центра, NaN
пропадёт сам, никак не задержавшись.
А в уроке «Погоня за мышью» x и y — это mut-переменные вне
loop: они специально хранят своё значение между кадрами, чтобы
кружок помнил, где он был. Если NaN хоть раз попадёт туда, он
останется там навсегда — дальнейшие вычисления его не исправляют.
Кружок пропадёт с экрана и уже не вернётся, даже когда курсор снова
отойдёт. Именно поэтому там стоит if distance > 0.0: она не даёт
опасному нулю вообще дойти до деления, пока он не испортил
переменную, которая должна помнить своё значение надолго.
Почему нельзя просто “починить” x после
Заголовок раздела «Почему нельзя просто “починить” x после»Может показаться, что можно обойтись без проверки заранее, а вместо
этого поправить x и y, если они уже стали NaN. Но x += direction_x * step — это x = x + direction_x * step: чтобы
прибавить что-то к x, Rust сначала берёт то значение, которое в нём
уже лежит. Если там NaN, результат снова NaN — += не лечит,
потому что обязательно использует уже испорченное значение.
Починить может только прямое присваивание конкретному числу — но тогда
встаёт вопрос, какое число подставить, и его пришлось бы отдельно
запоминать заранее на всякий случай. Проще не допустить NaN одной
проверкой, чем потом чинить уже испорченную переменную.
Что здесь происходит
Заголовок раздела «Что здесь происходит»- Деление нуля на ноль даёт особое значение
NaN— не ошибку и не бесконечность, а “непонятно что”. NaNне проходит сам: всё, что с ним делаешь дальше, тоже становитсяNaN.- Если переменная пересчитывается заново каждый кадр — один
NaNне страшен, он забывается вместе с кадром. - Если переменная хранит значение между кадрами —
NaN, попавший туда однажды, остаётся навсегда. Такие места стоит защищать проверкой заранее, а не полагаться на то, что “само пройдёт”. +=и подобные сокращения используют текущее значение переменной, поэтому не лечатNaN. Починить может только прямое присваивание конкретному числу — а придумывать, какому именно, обычно сложнее, чем просто проверить заранее.