Перейти к содержимому

Преобразования в Rust: From, Into, TryFrom, FromStr и AsRef

Программы постоянно что-то преобразуют. Текст из формы становится числом. Кортеж из трёх чисел становится Color. Один тип ошибки превращается в другой. Для этого в Rust есть небольшое семейство стандартных трейтов, и когда ты с ними познакомишься, начнёшь замечать их повсюду в настоящем коде.

Представь эти трейты как переходники, которые берёшь с собой в поездку за границу. Каждый сделан для одной задачи. Одни подходят всегда (From). Другие могут не подойти и честно об этом скажут (TryFrom). Третьи читают надпись и по ней собирают вещь (FromStr). А четвёртые просто позволяют подключить то, что у тебя есть (AsRef). На этой странице мы спокойно разберём каждый. С as ты уже встречался в уроке Приведение типов, а с From — в уроке Популярные трейты, так что будем опираться на них.

as — это быстро, но у него два ограничения. Он работает только между простыми встроенными типами вроде чисел, char и bool. И он никогда не сообщает о неудаче: если значение не помещается, ты молча получаешь другое число.

fn main() {
let items_in_cart: i32 = 300;
// `as` втискивает 300 в u8 и молча ошибается
let squeezed = items_in_cart as u8;
println!("через as: {squeezed}");
// `try_from` замечает проблему и сообщает о ней
let checked = u8::try_from(items_in_cart);
println!("через try_from: {:?}", checked);
// `from` есть только там, где преобразование никогда не теряет данные
let widened = i64::from(items_in_cart);
println!("через from: {widened}");
}

Вывод:

через as: 44
через try_from: Err(TryFromIntError(PosOverflow))
через from: 300
▶ Попробуй сам

Вот такие 44 — ошибка, которая может прятаться месяцами. Трейты преобразования исправляют это тремя способами:

  • Они честные. From реализуют только там, где ничего не может пойти не так. Если что-то может сломаться, используют TryFrom, и он возвращает Result.
  • Они работают с твоими типами. Написать (255, 0, 0) as Color нельзя, а реализовать From<(u8, u8, u8)> for Color — можно.
  • Их понимает другой код. На этих трейтах построены оператор ?, .parse() и множество библиотечных функций.

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

From и Into: преобразования, которые работают всегда

Заголовок раздела «From и Into: преобразования, которые работают всегда»

From говорит: «я умею создавать себя из того, другого типа». Ты им уже пользовался. Его реализуют многие стандартные типы:

let greeting = String::from("привет"); // &str -> String
let big = i64::from(42_i32); // i32 -> i64, всегда помещается
let precise = f64::from(7_u8); // u8 -> f64, всегда точно
let scores = Vec::from([90, 85, 77]); // массив -> Vec<i32>

Обрати внимание, чего здесь нет: не существует i32::from(some_i64). i64 может не поместиться в i32, поэтому Rust не делает вид, что это всегда работает. Мы вернёмся к этому в разделе о TryFrom.

Здесь Color можно создать из кортежа значений красного, зелёного и синего:

#[derive(Debug)]
struct Color {
red: u8,
green: u8,
blue: u8,
}
impl From<(u8, u8, u8)> for Color {
fn from((red, green, blue): (u8, u8, u8)) -> Self {
Color { red, green, blue }
}
}
fn main() {
let orange = Color::from((255, 136, 0));
println!("{:?}", orange);
// Мы не писали `Into`, но получаем его даром:
let sky: Color = (135, 206, 235).into();
println!("небо: r={} g={} b={}", sky.red, sky.green, sky.blue);
}

Вывод:

Color { red: 255, green: 136, blue: 0 }
небо: r=135 g=206 b=235
▶ Попробуй сам

Вот что здесь приятно. Стандартная библиотека говорит: «если B реализует From<A>, то A автоматически реализует Into<B>». Так что один impl From даёт два способа преобразования:

  • Color::from(tuple) читается как «создай Color из этого».
  • tuple.into() читается как «преврати это в то, что здесь нужно».

Зачем мы написали let sky: Color? Потому что .into() не говорит, во что превращать. Кортеж теоретически можно преобразовать во множество типов. Аннотация подсказывает Rust, какой ты имеешь в виду. Если Rust и так видит целевой тип — например, из параметра функции, — аннотация не нужна.

.into() часто встречается и в параметрах функций. Функция вроде fn new(title: impl Into<String>) принимает и &str, и String, а внутри вызывает title.into(). Целевой тип там Rust знает, поэтому аннотация не нужна. Этот приём ты сам попробуешь во втором упражнении практики.

From в обработке ошибок: как ? преобразует ошибки

Заголовок раздела «From в обработке ошибок: как ? преобразует ошибки»

Вот место, где From незаметно делает за тебя много работы. Когда ? встречает Err, он не просто возвращает его. Сначала он вызывает для ошибки From::from, чтобы превратить её в тот тип ошибки, который возвращает твоя функция.

Поэтому если функция возвращает твой собственный AppError, а AppError реализует From<ParseIntError>, то ? сам превратит ошибку разбора в AppError:

use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
MissingEquals,
BadNumber(ParseIntError),
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::BadNumber(e)
}
}
// Читает строку настроек вроде "max_users = 50"
fn read_setting(line: &str) -> Result<(String, u32), AppError> {
let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?;
let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError
Ok((key.trim().to_string(), number))
}
fn main() {
println!("{:?}", read_setting("max_users = 50"));
println!("{:?}", read_setting("max_users = lots"));
println!("{:?}", read_setting("max_users 50"));
}

Вывод:

Ok(("max_users", 50))
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
Err(MissingEquals)
▶ Попробуй сам

Без impl From второй ? не скомпилировался бы: Rust не знал бы, как превратить ParseIntError в AppError. А с ним тело функции остаётся чистым. О том, как проектировать типы ошибок, подробнее рассказано на странице Свои ошибки, а на странице anyhow и thiserror — о крейтах, которые пишут такие реализации From за тебя.

TryFrom и TryInto: преобразования, которые могут не удаться

Заголовок раздела «TryFrom и TryInto: преобразования, которые могут не удаться»

Некоторые преобразования работают только иногда. 300 не помещается в u8. -5 — не допустимый процент. Для таких случаев в Rust есть TryFrom. Он работает как From, но возвращает Result, так что случай «не получилось» тебе придётся обработать. И это хорошо: неудача не проскользнёт мимо тебя.

Встроенную версию ты уже видел в начале страницы: u8::try_from(300_i32) возвращает Err, а не неправильное число. И точно так же, как к From прилагается Into, к TryFrom прилагается TryInto, поэтому let small: Result<u8, _> = big.try_into(); тоже работает. Начиная с редакции 2021 года TryInto входит в прелюдию, так что ничего импортировать не нужно.

Создадим тип Percentage, который может хранить только значения от 0 до 100. Если каждый Percentage создаётся через try_from, то каждый Percentage в программе гарантированно корректен. Проверяешь один раз, на входе, и больше никогда.

#[derive(Debug)]
struct Percentage(u8);
#[derive(Debug)]
struct OutOfRange(i32);
impl TryFrom<i32> for Percentage {
type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if (0..=100).contains(&value) {
Ok(Percentage(value as u8)) // безопасно: мы только что проверили диапазон
} else {
Err(OutOfRange(value))
}
}
}
fn main() {
println!("{:?}", Percentage::try_from(85));
println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into();
println!("{:?}", battery);
}

Вывод:

Ok(Percentage(85))
Err(OutOfRange(150))
Err(OutOfRange(-5))
▶ Попробуй сам

Строка type Error = OutOfRange; говорит Rust, какую ошибку возвращает это преобразование. Выбираешь её ты. Здесь она хранит неправильное значение, чтобы тот, кто получит ошибку, мог показать полезное сообщение.

Чаще всего преобразовывать приходится текст. Файлы, аргументы командной строки и веб-формы — всё это даёт тебе строки. Трейт FromStr описывает, «как создать этот тип из &str», а .parse() — удобный способ его вызвать.

Когда ты пишешь "42".parse::<i32>(), Rust находит реализацию FromStr для i32 и запускает её. Часть ::<i32> (её называют «турбофиш», turbofish) говорит Rust, какой тип тебе нужен. Аннотация типа у переменной тоже подойдёт.

let age = "42".parse::<i32>(); // Ok(42)
let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)
let broken = "forty-two".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })

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

Представь багетную мастерскую, где клиенты вводят размеры вроде "30x40". Научим Rust это читать:

use std::str::FromStr;
#[derive(Debug)]
struct Size {
width: u32,
height: u32,
}
#[derive(Debug)]
enum SizeError {
MissingX,
BadNumber,
}
impl FromStr for Size {
type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?;
let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?;
let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?;
Ok(Size { width, height })
}
}
fn main() {
println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 на 40".parse::<Size>());
println!("{:?}", "30xbig".parse::<Size>());
}

Вывод:

Ok(Size { width: 30, height: 40 })
Err(MissingX)
Err(BadNumber)
▶ Попробуй сам

Две мелочи. Во-первых, FromStr называет свой тип ошибки Err, а не Error, как TryFrom. Это старая причуда в именах, так что не переживай, если поначалу будешь их путать: компилятор напомнит. Во-вторых, use std::str::FromStr; нужен, чтобы реализовать трейт, но не чтобы вызывать .parse().

.map_err(|_| SizeError::BadNumber) подменяет ошибку разбора числа нашей собственной ошибкой. Это удобная альтернатива реализации From, когда она нужна только в одном месте.

AsRef: принимаем всё, что может одолжить тебе представление

Заголовок раздела «AsRef: принимаем всё, что может одолжить тебе представление»

Трейты выше создают новое значение. AsRef другой. Он ничего не создаёт. Он просто говорит: «я могу дёшево одолжить тебе себя в виде вот этого типа».

Зачем это нужно? Допустим, ты пишешь функцию, которая считает слова. Если она принимает &str, вызывающему со String придётся писать &name. Если она принимает String, вызывающему с &str придётся выделять новую String. С impl AsRef<str> работают все варианты:

fn word_count(text: impl AsRef<str>) -> usize {
text.as_ref().split_whitespace().count()
}
fn main() {
let note = String::from("купить молоко и яйца");
println!("{}", word_count("учим rust вместе")); // &str
println!("{}", word_count(&note)); // &String
println!("{}", word_count(note)); // String
}

Вывод:

3
4
4
▶ Попробуй сам

Внутри функции text.as_ref() даёт обычный &str, что бы ни передал вызывающий.

Скорее всего, ты уже пользовался AsRef, сам того не замечая. Функции для работы с файлами в std::fs выглядят так:

pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>

Поэтому работают и fs::read_to_string("notes.txt"), и fs::read_to_string(my_string), и fs::read_to_string(my_path_buf). Тот же приём можно использовать в своём коде. Этот пример смотрит только на имена путей, поэтому настоящие файлы не трогает:

use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String {
match path.as_ref().extension() {
Some(ext) => ext.to_string_lossy().to_string(),
None => String::from("(нет)"),
}
}
fn main() {
println!("{}", extension_of("notes.txt"));
println!("{}", extension_of(String::from("holiday.jpeg")));
println!("{}", extension_of(PathBuf::from("backups/2024/site.tar.gz")));
println!("{}", extension_of("README"));
}

Вывод:

txt
jpeg
gz
(нет)
▶ Попробуй сам

AsMut — та же идея, только он одалживает изменяемое представление. Например, и Vec, и массив могут одолжить себя как изменяемый срез:

fn reset_all(scores: &mut impl AsMut<[u32]>) {
for score in scores.as_mut() {
*score = 0;
}
}
// Оба вызова работают:
// reset_all(&mut vec![12, 7, 30]);
// reset_all(&mut [3, 1, 4]);

AsMut нужен гораздо реже, чем AsRef. Достаточно узнавать его, когда встретишь.

Если не уверен, начни с этой таблицы:

Тебе нужно… Используй Пример
Быстро преобразовать числовые типы, соглашаясь на обрезание as x as f64
Преобразовать, когда неудача невозможна From i64::from(n), String::from("hi")
То же, когда целевой тип уже известен Into let s: String = "hi".into();
Преобразовать, когда неудача возможна TryFrom / TryInto u8::try_from(300)
Создать значение из текста FromStr / .parse() "42".parse::<i32>()
Дёшево принимать разные строки или пути AsRef fn f(p: impl AsRef<Path>)
Превратить значение в текст для людей Display → .to_string() 42.to_string()

Последняя строка достаётся даром: реализуй Display (см. Популярные трейты), и у каждого значения твоего типа появится метод .to_string().

Вызов .into(), когда Rust не может понять целевой тип.

fn main() {
let name = "Ferris".into();
println!("{name}");
}
error[E0283]: type annotations needed
--> src/main.rs:2:9
|
2 | let name = "Ferris".into();
| ^^^^ ---- type must be known at this point
|
= note: the type must implement `From<&str>`
= note: required for `&str` to implement `Into<_>`
help: consider giving `name` an explicit type
|
2 | let name: /* Type */ = "Ferris".into();
| ++++++++++++

Компилятор просит аннотацию типа: он должен знать тип в этом месте. &str может превратиться в String, Box<str>, Vec<u8> и не только. Угадывать Rust не станет. Добавь тип: let name: String = "Ferris".into(); — или просто напиши String::from("Ferris").

Реализация Into вместо From. Кажется, что это одно и то же, поэтому в эту ловушку легко попасть:

use std::num::ParseIntError;
#[derive(Debug)]
struct AppError(String);
impl Into<AppError> for ParseIntError {
fn into(self) -> AppError {
AppError(self.to_string())
}
}
fn read_port(text: &str) -> Result<u16, AppError> {
let port = text.parse::<u16>()?;
Ok(port)
}
error[E0277]: `?` couldn't convert the error to `AppError`
--> src/main.rs:13:35
|
12 | fn read_port(text: &str) -> Result<u16, AppError> {
| --------------------- expected `AppError` because of this
13 | let port = text.parse::<u16>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `AppError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

Компилятор сообщает, что ? не смог преобразовать ошибку в AppError, потому что AppError не реализует From<ParseIntError>. Подарок работает только в одну сторону: реализация From даёт Into, но реализация Into не даёт From. А ? и множество библиотечного кода требуют именно From. Поэтому всегда реализуй From, а Into появится сам. (Clippy, линтер Rust, даже предупреждает об этом.)

Создай структуру Time с полями hour: u8 и minute: u8. Реализуй FromStr, чтобы работало "09:45".parse::<Time>(). Возвращай ошибку, если нет :, если часть — не число, или если час больше 23, а минута больше 59. Проверь на "09:45", "25:00" и "обед". Подсказка: split_once(':') даёт две половины. В качестве типа ошибки вполне подойдёт простая String.

Показать решение
use std::str::FromStr;
#[derive(Debug)]
struct Time {
hour: u8,
minute: u8,
}
impl FromStr for Time {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let (h, m) = text.split_once(':').ok_or("нет ':'")?;
let hour: u8 = h.parse().map_err(|_| "час — не число")?;
let minute: u8 = m.parse().map_err(|_| "минуты — не число")?;
if hour > 23 || minute > 59 {
return Err(format!("{text} — такого времени не бывает"));
}
Ok(Time { hour, minute })
}
}
fn main() {
for input in ["09:45", "25:00", "обед"] {
match input.parse::<Time>() {
Ok(t) => println!("{input} -> {} ч {} мин", t.hour, t.minute),
Err(e) => println!("{input} -> ошибка: {e}"),
}
}
}

Вывод:

09:45 -> 9 ч 45 мин
25:00 -> ошибка: 25:00 — такого времени не бывает
обед -> ошибка: нет ':'
▶ Попробуй сам

? после .ok_or("нет ':'") работает, потому что String реализует From<&str>. Это тот же трюк с From из раздела про обработку ошибок, только на этот раз со стандартным типом.

Создай структуру Timer с полями minutes: u32 и seconds: u32. Реализуй From<u32>, чтобы общее число секунд превращалось в Timer (150 — это 2 минуты 30 секунд). Затем напиши fn start(timer: impl Into<Timer>), которая выводит время, и вызови её с 150 и с Timer { minutes: 1, seconds: 5 }. Подсказка: минуты — это total / 60, а остаток — total % 60. Любой тип уже умеет преобразовываться в самого себя, поэтому передать Timer тоже получится.

Показать решение
struct Timer {
minutes: u32,
seconds: u32,
}
impl From<u32> for Timer {
fn from(total: u32) -> Self {
Timer { minutes: total / 60, seconds: total % 60 }
}
}
fn start(timer: impl Into<Timer>) {
let t = timer.into();
println!("Таймер установлен на {}:{:02}", t.minutes, t.seconds);
}
fn main() {
start(150_u32);
start(Timer { minutes: 1, seconds: 5 });
}

Вывод:

Таймер установлен на 2:30
Таймер установлен на 1:05
▶ Попробуй сам

Мы написали только From<u32>, а start принимает и число, и Timer. Число работает через наш From, а Timer — потому что стандартная библиотека говорит: любой тип преобразуется в самого себя.

  • as — для быстрого приведения чисел, и он может молча терять данные. Трейты преобразования явные, безопасные и работают с твоими типами.
  • Реализуй From для преобразований, которые работают всегда, и Into достанется даром. .into() должен знать целевой тип.
  • Оператор ? вызывает From, чтобы преобразовывать типы ошибок, поэтому impl From<OtherError> for MyError делает обработку ошибок короткой.
  • Используй TryFrom / TryInto, когда преобразование может не удаться. Они возвращают Result.
  • Реализуй FromStr, чтобы для текста работал .parse::<YourType>().
  • Используй impl AsRef<str> или impl AsRef<Path>, чтобы дёшево, без копирования, принимать разные типы входных данных.

Проверь себя

1. Ты реализовал From<Celsius> for Fahrenheit. Что ещё ты получаешь автоматически?

2. Какой вызов безопасно превращает i32, который может оказаться равным 300, в u8?

3. Что нужно реализовать, чтобы работало "30x40".parse::<Size>()?

4. Почему let name = "Ferris".into(); не компилируется?

Результат: 0 / 4