Команда разработчиков прислала мне на ревью свой API, в одной из частей которого множество поддерживаемых значений выражалось в виде трёх чисел:

  • Минимального допустимого значения.

  • Инкремента.

  • Максимального допустимого значения.

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

Например, если минимальное значение равно 5, инкремент равен 10, а максимум 30, то валидными значениями будут такие:

5

Минимум

15

Минимум + 1 × инкремент

25

Минимум + 2 × инкремент

30

Максимум

Команда сообщила, что если инкремент равен нулю, то поддерживаются только минимальное и максимальное значения.

Я указал, что эта архитектура искушает пользователя делить на ноль.

Вот функция, которую он может написать:

int closestSupportedValue(int desired)
{
    int nearest = minimum +
        ((desired - minimum + increment/2) / increment) * increment;
    return std::clamp(nearest, minimum, maximum);
}

Сначала мы находим число больше минимального, кратное инкременту, которое ближе всего к желаемому значению (desired). (Можно настроить округление, соответствующим образом изменив +increment/2.) Затем мы ограничиваем это значение допустимым диапазоном.

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

Я рекомендовал команде удалить ноль из API. Если поддерживаются только два значения, то пусть инкремент будет равен maximum - minimum¹. Если поддерживается только одно значение, то инкремент пусть будет равен 1.

Ещё немного ненужной информации: в эту ловушку попалась команда разработчиков прогноза нагрузок на электросети. Класс PowerGridForecast описывает последовательность объектов PowerGridData, начинающихся с StartTime, где каждый прогноз описывает промежуток времени, описываемый BlockDuration. Иными словами, n-ный элемент вектора описывает прогноз, начинающийся в StartTime + n × BlockDuration и длящийся BlockDuration.

При создании функции, находящей блок, описывающий конкретный момент времени, можно написать нечто подобное:

PowerGridData FindForecastForDateTime(
    PowerGridForecast forecast, DateTime time)
{
    var elapsed = time - forecast.StartTime;
    var index = elapsed / forecast.BlockDuration;
    if (index < 0 || index >= forecast.Forecast.Count) {
        return null; // прогноз для этого индекса отсутствует
    }
    return forecast.Forecast[index];
}

Мы определяем время, прошедшее после начала первого прогноза, определяем, индексу какого блока оно принадлежит, и возвращаем элемент этого индекса, если он существует.

Команда решила, что если прогноза нет, то свойство Forecast должно возвращать пустой вектор (это хорошо), а BlockDuration будет равно нулю (а это плохо).

Команда решила, что если прогноза нет, то размер блока не важен, ведь он описывает нечто несуществующее, так кого волнует, что он равен нулю? Но, как мы уже поняли, нулевая длительность блока означает, что при вычислении индекса выполнится деление на ноль.

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

¹ Или же можно просто присвоить инкременту любое положительное значение больше maximum - minimum, однако не стоит выбирать чрезмерно большое значение, потому что в противном случае возникает риск целочисленного переполнения.

² И у них вряд ли есть бюджеты для отправки разработчика в далёкую страну только для проверки прохождения тестового случая. «Начальник, нам очень важно отправить разработчика на Таити, чтобы обеспечить хорошее покрытие тестами».

Комментарии (3)


  1. Goron_Dekar
    02.11.2025 07:59

    Читаю статью как месседж "деление провоцирует деление на ноль, не вынуждайте разработчиков делить"

    Помимо нуля, инкремент может быть очень мал, но всё же не равен нулю — что может привести к переполнению


  1. ArtyomOchkin
    02.11.2025 07:59

    if "инкремент" == 0:

    return 0;

    Разве так нельзя?


    1. Goron_Dekar
      02.11.2025 07:59

      Блин, нет же!

      Это даже хуже, чем просто отсутствие проверки!

      А представьте себе, что речь идёт о заведомо отрицательном параметре? Или, упаси бог, комплексном!