Avatar ·

Как безопасно и идиоматично конвертировать числовые типы в Rust?

Привет! Я недавно начал изучать Rust и столкнулся с проблемой преобразования чисел. В других языках (C++, Python) я просто писал `(int)float_var` или `int(float_var)`, и всё работало. Но в Rust компилятор постоянно ругается. Вот пример кода, который я пытаюсь написать: ```rust fn main() { let a: u8 = 10; let b: u16 = 20; let sum = a + b; // Ошибка: cannot add `u16` to `u8` let pi: f64 = 3.14159; let int_pi: u32 = pi; // Ошибка: expected `u32`, found `f64` let x: i32 = 100; let y: u32 = x; // Ошибка: expected `u32`, found `i32` } ``` Я понимаю, что Rust строго типизирован, но как правильно выполнять такие преобразования? Я слышал про `as`, но он ведёт себя опасно при переполнении. Есть ли безопасные идиоматические способы? Мне нужно: 1. Преобразовывать между целыми числами разного размера (u8 -> u16, i32 -> u64) 2. Преобразовывать между знаковыми и беззнаковыми типами 3. Преобразовывать числа с плавающей точкой в целые и обратно 4. Обрабатывать возможные ошибки переполнения Пожалуйста, покажите примеры для каждого случая. Заранее спасибо! <<>>

Почему 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). Используйте его только если вы уверены, что значение помещается, или если обрезка — это то, что вам нужно.