Rustの型変換(From・Into・TryFrom・FromStr・AsRef)
プログラムは、いつも何かを変換しています。フォームに入力された文字列は数値になります。3つの数値のタプルは Color になります。あるエラー型は別のエラー型になります。Rustには、こうした変換のための標準トレイトが少しだけ用意されています。一度覚えてしまえば、実際のコードのあちこちで目にするようになります。
これらのトレイトは、海外旅行に持っていく変換プラグのようなものです。それぞれが1つの用途のために作られています。いつでも差し込めるもの(From)。合わないことがあって、そのときは教えてくれるもの(TryFrom)。ラベルを読んで中身を組み立てるもの(FromStr)。そして、手持ちのものを何でも差し込めるようにしてくれるもの(AsRef)。このページでは、1つずつゆっくり見ていきます。as については型キャストのページで、From についてはよく使うトレイトのページで少し触れているので、その続きとして読んでください。
as だけではだめなの?
Section titled “as だけではだめなの?”as は手軽ですが、2つの限界があります。1つは、数値や char、bool のような単純な組み込み型どうしでしか使えないこと。もう1つは、決して失敗しないことです。値が収まらないときは、何も言わずに別の数値を返してきます。
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 の場合: 44try_from の場合: Err(TryFromIntError(PosOverflow))from の場合: 300この 44 は、何か月も気づかれずに潜んでいるタイプのバグです。変換トレイトは、3つの点でこの問題を解決してくれます。
- 正直である。
Fromは、何も問題が起こりえないときにしか実装されません。問題が起こりうるときはTryFromを使い、Resultが返ってきます。 - 自分で作った型にも使える。
(255, 0, 0) as Colorとは書けませんが、From<(u8, u8, u8)> for Colorなら実装できます。 - ほかのコードが理解してくれる。
?演算子や.parse()、多くのライブラリ関数は、これらのトレイトの上に作られています。
目安としては、as は手早く安全に済む数値の変換だけにとどめ、それ以外ではトレイトを使いましょう。
From と Into:必ず成功する変換
Section titled “From と Into:必ず成功する変換”From は「自分はあの型から自分自身を作る方法を知っています」という意味です。実はもう使ったことがあるはずです。多くの標準の型が 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 を実装する
Section titled “自分の型に 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 を1つ書くだけで、変換の書き方が2つ手に入るのです。
Color::from(tuple)は「これからColorを作る」と読めます。tuple.into()は「これを、ここで必要な型に変える」と読めます。
なぜ let sky: Color と型を書いたのでしょうか? .into() は、何に変換するかを言っていないからです。タプルは、理屈の上ではいろいろな型に変換できます。型注釈で、どれのことなのかをRustに伝えているのです。関数の引数などから変換先の型がすでにわかる場合は、型注釈はいりません。
.into() は関数の引数でもよく見かけます。fn new(title: impl Into<String>) のような関数は &str も String も受け取れて、中で title.into() を呼びます。そこでは変換先がわかっているので、型注釈は不要です。このパターンは練習問題2で実際に試せます。
エラー処理での From:? がエラーを変換するしくみ
Section titled “エラー処理での 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" のような設定ファイルの1行を読む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 がなければ、2つ目の ? はコンパイルできません。ParseIntError を AppError に変える方法を、Rustが知らないからです。impl From があれば、関数の本体はすっきりしたままです。エラー型の設計については独自のエラー型のページで、こうした From の実装を代わりに書いてくれるクレートについてはanyhowとthiserrorのページで、さらに詳しく説明しています。
TryFrom と TryInto:失敗するかもしれない変換
Section titled “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(); とも書けます。TryInto は2021エディションからプレリュードに入っているので、何もインポートする必要はありません。
自分の型に TryFrom を実装する
Section titled “自分の型に TryFrom を実装する”0から100までしか持てない Percentage 型を作ってみましょう。すべての 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():文字列から値を作る
Section titled “FromStr と .parse():文字列から値を作る”いちばんよく変換するのは文字列です。ファイルもコマンドライン引数もWebのフォームも、文字列を渡してきます。FromStr トレイトは「&str からこの型を作る方法」を表すもので、.parse() はそれを手軽に呼び出すための方法です。
"42".parse::<i32>() と書くと、Rustは i32 の FromStr の実装を探して実行します。::<i32> の部分(「ターボフィッシュ」と呼ばれています)で、ほしい型を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 を実装する
Section titled “自分の型に 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 by 40".parse::<Size>()); println!("{:?}", "30xbig".parse::<Size>());}実行結果:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)細かい点が2つあります。1つ目は、FromStr ではエラー型の名前が TryFrom のような Error ではなく Err だということです。昔からの名前の付け方のクセなので、最初は混同しても気にしないでください。コンパイラが教えてくれます。2つ目は、トレイトを実装するには use std::str::FromStr; が必要ですが、.parse() を呼ぶだけなら必要ないということです。
.map_err(|_| SizeError::BadNumber) は、数値の解析エラーを独自のエラーに差し替えています。1か所でしか使わないときは、From を実装する代わりにこう書くと便利です。
AsRef:「中身を見せてくれるもの」なら何でも受け取る
Section titled “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("buy milk and eggs");
println!("{}", word_count("hello rust world")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}実行結果:
344この例の文字列は英語のままにしています。split_whitespace() は空白で区切って単語を数えるので、単語の間に空白を入れない日本語の文章では、うまく数えられないからです。
関数の中では、呼び出し側が何を渡したかに関係なく、text.as_ref() でふつうの &str が得られます。
標準ライブラリの AsRef:パス
Section titled “標準ライブラリの 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 よりずっと少ないです。見かけたときに「あれか」とわかれば十分です。
どれを使えばいいの?
Section titled “どれを使えばいいの?”迷ったときは、まずこの表を見てください。
| やりたいこと | 使うもの | 例 |
|---|---|---|
| 数値の型どうしを手早く変換する(切り捨てが起きてもよい) | 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() メソッドが使えるようになります。
よくある間違い
Section titled “よくある間違い”変換先の型がわからないのに .into() を呼ぶ。
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(); | ++++++++++++「型注釈が必要です」というエラーで、help では「name に型を明示しましょう」と提案しています。&str は String にも Box<str> にも Vec<u8> にも、ほかにもいろいろな型に変換できます。Rustは当てずっぽうで選んだりしません。let name: String = "Ferris".into(); のように型を書くか、単に String::from("Ferris") と書きましょう。
From ではなく Into を実装してしまう。 同じことのように見えるので、ハマりやすい落とし穴です。
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> が実装されていない」というエラーです。note にあるように、? は From トレイトを使ってエラーを変換します。自動で付いてくるおまけは一方通行なのです。From を実装すれば Into が手に入りますが、Into を実装しても From は手に入りません。そして ? も、多くのライブラリのコードも、求めているのは From です。ですから、実装するのはいつも From にして、Into は自動で付いてくるのに任せましょう。(Rustのリンターである Clippy も、これには警告を出してくれます。)
1. 時刻を読み取る
Section titled “1. 時刻を読み取る”hour: u8 と minute: u8 を持つ Time 構造体を作ってください。FromStr を実装して、"09:45".parse::<Time>() が動くようにしましょう。: がないとき、どちらかの部分が数値でないとき、時が23より大きいか分が59より大きいときは、エラーを返してください。"09:45"、"25:00"、"lunch" で試しましょう。ヒント: 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", "lunch"] { 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 は実在しない時刻ですlunch -> エラー: ':' がありません.ok_or("':' がありません") の後ろの ? が動くのは、String が From<&str> を実装しているからです。エラー処理の節で見たのと同じ From の技を、今回は標準の型で使っています。
2. キッチンタイマー
Section titled “2. キッチンタイマー”minutes: u32 と seconds: u32 を持つ Timer 構造体を作ってください。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>を使うと、コピーせずに、さまざまな型の入力を低コストで受け取れます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. From<Celsius> for Fahrenheit を実装しました。ほかに自動で手に入るものは何ですか?
2. 300になるかもしれない i32 を u8 に変換する、安全な呼び出しはどれですか?
3. "30x40".parse::<Size>() が動くようにするには、何を実装しますか?
4. let name = "Ferris".into(); がコンパイルできないのはなぜですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”std::convert— 標準ライブラリのドキュメント(英語):From・Into・TryFrom・TryInto・AsRef・AsMutがまとまっている場所で、どれをいつ実装すべきかの短いガイドもあります。FromStr— 標準ライブラリのドキュメント(英語):.parse()の裏にあるトレイトで、実装の完全な例が載っています。AsRef— 標準ライブラリのドキュメント(英語):AsRefとBorrowの違いや、低コストな参照の変換にAsRefが使われる理由を説明しています。- 型変換 — Rust By Example 日本語版:
From/Into、TryFrom/TryInto、文字列との相互変換について、実行できる短い章がそろっています。 - Conversions — Rust API Guidelines(英語):自分の型にどの変換トレイトを実装すべきかについての、コミュニティのアドバイスです。