Существует ли в Rust операция взятия модуля (а не остатка от деления)?
Почему в Rust нет встроенного оператора модуля?
В Rust оператор % действительно возвращает остаток от деления (remainder), а не математический модуль. Это сделано намеренно: Rust следует поведению большинства языков системного программирования (C, C++), где результат остатка имеет знак делимого. Математический модуль (также называемый "евклидовым делением") всегда возвращает неотрицательное число.
Встроенной функции для модуля в стандартной библиотеке нет, но Rust дает нам элегантные способы решить эту проблему.
Правильное решение: функция rem_euclid
Начиная с Rust 1.38, для всех целочисленных типов (i8, i16, i32, i64, i128, isize) существует метод rem_euclid. Он вычисляет именно математический модуль:
fn main() {
let a: i32 = -5;
let b: i32 = 3;
// Остаток от деления (remainder)
println!("Остаток: {}", a % b); // -2
// Математический модуль (euclidean remainder)
println!("Модуль: {}", a.rem_euclid(b)); // 1
}
Исправление вашей функции shift_index
Вот как правильно переписать ваш пример:
fn shift_index(current: usize, delta: i32, len: usize) -> usize {
// Преобразуем в i64, чтобы избежать переполнения при сложении
let current_i64 = current as i64;
let delta_i64 = delta as i64;
let len_i64 = len as i64;
// Вычисляем новый индекс с использованием модуля
let new_index = (current_i64 + delta_i64).rem_euclid(len_i64);
// Безопасно преобразуем обратно в usize
new_index as usize
}
fn main() {
let arr = [10, 20, 30, 40, 50];
let len = arr.len();
// Сдвиг на -2 (влево на 2)
let idx = shift_index(0, -2, len);
println!("Новый индекс: {}", idx); // 3
println!("Значение: {}", arr[idx]); // 40
}
Альтернативы: ручная реализация
Если по какой-то причине вы не можете использовать rem_euclid (например, очень старая версия Rust), вот классическая формула:
fn modulo(a: i32, b: i32) -> i32 {
((a % b) + b) % b
}
fn main() {
assert_eq!(modulo(-5, 3), 1);
assert_eq!(modulo(5, 3), 2);
assert_eq!(modulo(0, 3), 0);
}
Эта формула работает, но rem_euclid предпочтительнее, так как она:
- Оптимизирована на уровне компилятора
- Работает для всех целочисленных типов
- Корректно обрабатывает крайние случаи (например, когда
bотрицательное — хотя обычно вы будете использовать положительный модуль)
Важное замечание для usize
Метод rem_euclid доступен только для знаковых типов. Для usize он не определен, потому что usize всегда неотрицательный, и % для него ведет себя как модуль. В вашем случае вы все равно преобразуете в i32 для работы с отрицательным delta, поэтому используйте i64 для безопасности от переполнения.
Идиоматичный Rust — использовать rem_euclid для знаковых типов. Это стандартный, производительный и понятный способ получить математический модуль.
Расширение: работа с числами с плавающей точкой
Если вам нужен модуль для f32 или f64, ситуация немного отличается. Для вещественных чисел метод rem_euclid также существует и работает аналогично:
fn main() {
let angle: f64 = -120.0; // градусов
let period: f64 = 360.0;
// Приводим угол к диапазону [0, 360)
let normalized = angle.rem_euclid(period);
println!("Нормализованный угол: {}°", normalized); // 240.0
}
Обратите внимание: для вещественных чисел rem_euclid может давать небольшие погрешности из-за представления чисел с плавающей точкой, но для большинства практических задач (нормализация углов, циклические буферы) этого достаточно.
Создание трейта для удобства
Если вы часто используете модуль в своем проекте, можно определить собственный трейт:
trait Modulo {
fn modulo(self, rhs: Self) -> Self;
}
impl Modulo for i32 {
fn modulo(self, rhs: Self) -> Self {
self.rem_euclid(rhs)
}
}
impl Modulo for i64 {
fn modulo(self, rhs: Self) -> Self {
self.rem_euclid(rhs)
}
}
// Использование
fn main() {
let a: i32 = -5;
let b: i32 = 3;
println!("{}", a.modulo(b)); // 1
}
Но для разового использования проще и понятнее вызывать rem_euclid напрямую — это идиоматичный Rust.