Преобразования в Rust: From, Into, TryFrom, FromStr и AsRef
Программы постоянно что-то преобразуют. Текст из формы становится числом. Кортеж из трёх чисел становится Color. Один тип ошибки превращается в другой. Для этого в Rust есть небольшое семейство стандартных трейтов, и когда ты с ними познакомишься, начнёшь замечать их повсюду в настоящем коде.
Представь эти трейты как переходники, которые берёшь с собой в поездку за границу. Каждый сделан для одной задачи. Одни подходят всегда (From). Другие могут не подойти и честно об этом скажут (TryFrom). Третьи читают надпись и по ней собирают вещь (FromStr). А четвёртые просто позволяют подключить то, что у тебя есть (AsRef). На этой странице мы спокойно разберём каждый. С as ты уже встречался в уроке Приведение типов, а с From — в уроке Популярные трейты, так что будем опираться на них.
Почему бы просто не использовать as?
Заголовок раздела «Почему бы просто не использовать as?»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 -> Stringlet 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.
Реализуем From для своего типа
Заголовок раздела «Реализуем From для своего типа»Здесь 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 входит в прелюдию, так что ничего импортировать не нужно.
Реализуем TryFrom для своего типа
Заголовок раздела «Реализуем TryFrom для своего типа»Создадим тип 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 и .parse(): создаём значения из текста
Заголовок раздела «FromStr и .parse(): создаём значения из текста»Чаще всего преобразовывать приходится текст. Файлы, аргументы командной строки и веб-формы — всё это даёт тебе строки. Трейт 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, потому что текст всегда может оказаться неправильным.
Реализуем FromStr для своего типа
Заголовок раздела «Реализуем FromStr для своего типа»Представь багетную мастерскую, где клиенты вводят размеры вроде "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(¬e)); // &String println!("{}", word_count(note)); // String}Вывод:
344Внутри функции text.as_ref() даёт обычный &str, что бы ни передал вызывающий.
AsRef в стандартной библиотеке: пути
Заголовок раздела «AsRef в стандартной библиотеке: пути»Скорее всего, ты уже пользовался 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"));}Вывод:
txtjpeggz(нет)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 this13 | 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, даже предупреждает об этом.)
Практика
Заголовок раздела «Практика»1. Читаем время на часах
Заголовок раздела «1. Читаем время на часах»Создай структуру 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 из раздела про обработку ошибок, только на этот раз со стандартным типом.
2. Кухонный таймер
Заголовок раздела «2. Кухонный таймер»Создай структуру 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
Что почитать
Заголовок раздела «Что почитать»std::convert— документация стандартной библиотеки (на английском): дом дляFrom,Into,TryFrom,TryInto,AsRefиAsMut, с короткой подсказкой, когда реализовывать каждый.FromStr— документация стандартной библиотеки (на английском): трейт, на котором построен.parse(), с полным примером реализации.AsRef— документация стандартной библиотеки (на английском): чемAsRefотличается отBorrowи почему его используют для дешёвых преобразований ссылок.- Преобразования — Rust на примерах (перевод на русский): короткие запускаемые главы о
From/Into,TryFrom/TryIntoи преобразовании в строки и из строк. - Conversions — Rust API Guidelines (на английском): советы сообщества о том, какие трейты преобразования стоит реализовать для своих типов.