Модуль в Rust: не путайте с остатком от деления

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему вопрос про модуль так популярен?

Если вы когда-нибудь писали код на Rust и сталкивались с отрицательными числами, вы наверняка задавались вопросом: «А где же функция модуля?». На Stack Overflow этот вопрос набрал тысячи просмотров, и не зря. Дело в том, что оператор % в Rust — это не модуль (modulus), а остаток от деления (remainder). Разница тонкая, но критическая, особенно в алгоритмах, криптографии или работе с циклическими структурами.

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

Краткое содержание

  • Что такое модуль и остаток: математическая разница
  • Почему % в Rust — это не модуль
  • Как написать функцию модуля в Rust
  • Примеры из реальной жизни: циклические индексы и хэши
  • Выводы и практические советы

Математика: модуль vs остаток

Давайте начнём с базы. В математике модуль (modulus) — это операция, которая возвращает неотрицательное число, меньшее делителя. Например:

  • 7 mod 3 = 1 (7 делится на 3 с остатком 1)
  • -7 mod 3 = 2 (потому что -7 = 3 * (-3) + 2)

Остаток от деления (remainder) — это то, что остаётся после целочисленного деления. В большинстве языков программирования (C, C++, Java, Rust) оператор % возвращает именно остаток, который может быть отрицательным, если делимое отрицательное. Пример:

  • 7 % 3 = 1 (совпадает)
  • -7 % 3 = -1 (в Rust, потому что -7 / 3 = -2, остаток -1)

Зачем это нужно? Представьте, что вы пишете программу для часов: время 0–23. Если сейчас 22 часа, и вы добавляете 5 часов, то получите 3 часа утра. Это работает с модулем: (22 + 5) mod 24 = 3. Но если использовать остаток, то при отрицательных значениях всё сломается. Например, (-3) % 24 в Rust даст -3, а не 21.

Почему оператор % в Rust — это remainder

В Rust оператор % определён как остаток от деления. Это зафиксировано в документации и поведении стандартной библиотеки. Причина историческая: так сделано в C и многих других языках. Rust следует принципу «явное лучше неявного», но здесь новички часто попадают в ловушку.

fn main() {
    println!("7 % 3 = {}", 7 % 3);   // 1
    println!("-7 % 3 = {}", -7 % 3); // -1
    println!("7 % -3 = {}", 7 % -3); // 1 (да, знак делителя не влияет)
    println!("-7 % -3 = {}", -7 % -3); // -1
}

Обратите внимание: знак результата всегда совпадает со знаком делимого. Это не модуль. Для истинного модуля нужно, чтобы результат был всегда неотрицательным.

Как написать функцию истинного модуля в Rust?

Стандартная библиотека Rust не предоставляет встроенную функцию для модуля (в отличие от Python, где % работает как модуль). Но написать её очень просто. Вот самый распространённый способ — через остаток с коррекцией.

Функция modulus для целых чисел

fn modulus(a: i32, b: i32) -> i32 {
    let r = a % b;
    if r < 0 { r + b.abs() } else { r }
}

fn main() {
    println!("modulus(7, 3) = {}", modulus(7, 3));   // 1
    println!("modulus(-7, 3) = {}", modulus(-7, 3)); // 2
    println!("modulus(7, -3) = {}", modulus(7, -3)); // 1 (модуль всегда неотрицательный)
    println!("modulus(-7, -3) = {}", modulus(-7, -3)); // 2
}

Как это работает: мы берём остаток a % b. Если он отрицательный, добавляем абсолютное значение делителя b.abs(). Это гарантирует, что результат будет в диапазоне [0, |b|).

Универсальная функция для любых целых типов

Если вы хотите использовать модуль с разными типами (i8, i16, i32, i64), можно написать обобщённую функцию с трейтом std::ops::Rem и std::cmp::PartialOrd. Но для начинающих проще использовать конкретный тип, например i32.

use std::ops::Rem;

fn modulus<T>(a: T, b: T) -> T
where
    T: Rem<Output = T> + Copy + PartialOrd + From<i8> + std::ops::Neg<Output = T>,
{
    let r = a % b;
    if r < 0.into() { r + (-b) } else { r }
}

Этот код использует трейты, чтобы работать с разными числовыми типами. Однако для простоты мы рекомендуем первый вариант.

Примеры из реальной жизни: когда модуль незаменим

1. Циклические индексы (кольцевой буфер)

Представьте, что у вас есть массив из 5 элементов, и вы хотите двигаться по нему циклически. Если индекс уходит в минус, модуль вернёт правильное значение.

fn cyclic_index(current: i32, step: i32, size: i32) -> i32 {
    modulus(current + step, size)
}

fn main() {
    let size = 5;
    println!("Сдвиг на -1: {}", cyclic_index(0, -1, size)); // 4 (а не -1)
    println!("Сдвиг на 7: {}", cyclic_index(0, 7, size)); // 2
}

Без модуля вы бы получили отрицательный индекс и панику при обращении к массиву.

2. Хэш-таблицы и распределение ключей

При хэшировании часто нужно отобразить большое число в диапазон [0, N). Если хэш отрицательный, остаток даст отрицательное значение, что приведёт к ошибке. Модуль решает проблему.

fn hash_to_index(hash: i32, table_size: i32) -> i32 {
    modulus(hash, table_size)
}

fn main() {
    let hash = -12345;
    let table_size = 100;
    println!("Индекс для хэша {}: {}", hash, hash_to_index(hash, table_size)); // 55
}

3. Математические алгоритмы (например, алгоритм Евклида)

В алгоритме Евклида для нахождения НОД используется модуль. Если использовать остаток, алгоритм может работать некорректно с отрицательными числами. Правильный модуль гаранти

Блоги

Рекомендаций по книгам