Как безопасно и идиоматично конвертировать числовые типы в Rust?
Почему Rust не позволяет неявные преобразования?
В Rust каждое число имеет фиксированный размер и знаковость. Неявное преобразование (как в C++) может привести к потере данных, переполнению или неопределённому поведению. Rust заставляет вас явно указывать, что вы хотите делать, чтобы избежать ошибок.
Безопасные методы преобразования
Вот идиоматические способы для разных ситуаций:
1. Преобразование "без потерь" (расширяющие преобразования)
Когда целевой тип гарантированно вмещает исходное значение (например, u8 → u16):
let a: u8 = 10;
let b: u16 = 20;
// Используйте From/Into (безопасно, без ошибок)
let sum = u16::from(a) + b; // явно
let sum2: u16 = a.into() + b; // через Into (когда тип известен)
println!("{}", sum); // 30
// Для знаковых: i8 → i32
let small: i8 = -5;
let big: i32 = i32::from(small);
println!("{}", big); // -5
2. Преобразование с возможной потерей (сужающие преобразования)
Когда значение может не поместиться (u16 → u8, i32 → u16):
let value: u16 = 300;
// Безопасный вариант: TryFrom/TryInto (возвращает Result)
match u8::try_from(value) {
Ok(v) => println!("Преобразовано: {}", v),
Err(e) => println!("Ошибка: {}", e), // 300 не влезает в u8
}
// Или через if let
if let Ok(v) = u8::try_from(value) {
println!("Успех: {}", v);
}
// Для знаковых/беззнаковых: i32 → u32
let signed: i32 = -5;
match u32::try_from(signed) {
Ok(v) => println!("{}", v),
Err(_) => println!("Отрицательное число нельзя преобразовать в u32"),
}
3. Преобразование float в int (с округлением)
let pi: f64 = 3.14159;
// Округление вниз (как (int) в C)
let int_pi: u32 = pi as u32; // 3 — но as не проверяет переполнение!
println!("{}", int_pi);
// Безопасный вариант: проверка диапазона
if pi >= 0.0 && pi <= u32::MAX as f64 {
let safe_pi = pi as u32;
println!("Безопасно: {}", safe_pi);
} else {
println!("Значение вне диапазона");
}
// Лучше: использовать saturating или wrapping
let float_val: f64 = 1.9;
let truncated: u8 = float_val.trunc() as u8; // 1
let rounded: u8 = float_val.round() as u8; // 2
4. Преобразование int в float
let x: i32 = 100;
let y: f64 = x.into(); // безопасно, i32 всегда помещается в f64
println!("{}", y); // 100.0
// Для больших типов: u64 → f64 (может потерять точность!)
let big: u64 = 12345678901234567890;
let f: f64 = big as f64; // as здесь уместен, т.к. From не реализован
println!("{}", f); // может быть округлено
Идиоматический подход: используйте From/TryFrom
- From — для преобразований, которые всегда успешны (расширяющие). Реализуется для пар типов, где потеря невозможна.
- TryFrom — для преобразований, которые могут завершиться ошибкой (сужающие, знаковые/беззнаковые). Возвращает Result.
- Into/TryInto — обёртки для обратного вызова (когда источник известен, а цель выводится).
- as — используйте только когда вы точно знаете, что значение поместится, или когда нужна обрезка/насыщение. Для float→int as — основной способ, но проверяйте диапазон.
Примеры для всех ваших случаев
fn main() {
// 1. Целые разного размера (расширение)
let a: u8 = 10; let b: u16 = 20;
println!("{}", u16::from(a) + b); // 30
// 2. Целые разного размера (сужение)
let big: u16 = 300;
match u8::try_from(big) {
Ok(v) => println!("{}", v),
Err(_) => println!("300 > 255"),
}
// 3. Знаковые/беззнаковые
let signed: i32 = -5;
match u32::try_from(signed) {
Ok(v) => println!("{}", v),
Err(_) => println!("Отрицательное!"),
}
// 4. Float → int
let pi: f64 = 3.14159;
if pi >= 0.0 && pi <= u32::MAX as f64 {
println!("{}", pi as u32); // 3
}
// 5. Int → float
let x: u64 = 12345;
println!("{}", x as f64); // 12345.0
}
Запомните: From для безопасных, TryFrom для рискованных, as — с осторожностью.
<<Использование методов saturating, wrapping и checked
Если вы не хотите использовать трейты From/TryFrom, а предпочитаете явно указать поведение при переполнении, Rust предоставляет набор методов для целых чисел:
let a: u8 = 250;
let b: u8 = 10;
// Насыщение (saturating) — при переполнении остаётся MAX/MIN
let sat_sum = a.saturating_add(b); // 255 (u8::MAX)
let sat_sub = a.saturating_sub(b); // 240
// Обёртывание (wrapping) — переполнение по модулю 2^n
let wrap_sum = a.wrapping_add(b); // 4 (250+10=260 → 260-256=4)
let wrap_sub = a.wrapping_sub(b); // 240
// Проверка (checked) — возвращает Option
match a.checked_add(b) {
Some(v) => println!("{}", v),
None => println!("Переполнение!"),
}
// Для преобразования типов есть аналоги
let big: u16 = 300;
let small: u8 = big as u8; // 44 (300 - 256 = 44)
let sat_small: u8 = big.min(u16::from(u8::MAX)) as u8; // 255
let checked_small = u8::try_from(big); // Err
Эти методы полезны, когда вы пишете низкоуровневый код (например, для эмуляции арифметики с фиксированной разрядностью) или когда переполнение — ожидаемое поведение. Для обычных преобразований всё же предпочтительнее From/TryFrom, так как они самодокументируют намерения и не скрывают ошибки.
Также помните про as: он всегда выполняет wrapping-преобразование для целых чисел (например, 300u16 as u8 даст 44). Используйте его только если вы уверены, что значение помещается, или если обрезка — это то, что вам нужно.
Ответов пока нет.
Войдите чтобы оставить ответ