コンテンツにスキップ

Rustの型変換(From・Into・TryFrom・FromStr・AsRef)

プログラムは、いつも何かを変換しています。フォームに入力された文字列は数値になります。3つの数値のタプルは Color になります。あるエラー型は別のエラー型になります。Rustには、こうした変換のための標準トレイトが少しだけ用意されています。一度覚えてしまえば、実際のコードのあちこちで目にするようになります。

これらのトレイトは、海外旅行に持っていく変換プラグのようなものです。それぞれが1つの用途のために作られています。いつでも差し込めるもの(From)。合わないことがあって、そのときは教えてくれるもの(TryFrom)。ラベルを読んで中身を組み立てるもの(FromStr)。そして、手持ちのものを何でも差し込めるようにしてくれるもの(AsRef)。このページでは、1つずつゆっくり見ていきます。as については型キャストのページで、From についてはよく使うトレイトのページで少し触れているので、その続きとして読んでください。

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 の場合: 44
try_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 は「自分はあの型から自分自身を作る方法を知っています」という意味です。実はもう使ったことがあるはずです。多くの標準の型が 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 を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エディションからプレリュードに入っているので、何もインポートする必要はありません。

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 を返します。文字列は、いつだって間違っている可能性があるからです。

お客さんが "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(&note)); // &String
println!("{}", word_count(note)); // String
}

実行結果:

3
4
4
▶ 実際に試してみよう

この例の文字列は英語のままにしています。split_whitespace() は空白で区切って単語を数えるので、単語の間に空白を入れない日本語の文章では、うまく数えられないからです。

関数の中では、呼び出し側が何を渡したかに関係なく、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() を呼ぶ。

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 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> が実装されていない」というエラーです。note にあるように、? は From トレイトを使ってエラーを変換します。自動で付いてくるおまけは一方通行なのです。From を実装すれば Into が手に入りますが、Into を実装しても From は手に入りません。そして ? も、多くのライブラリのコードも、求めているのは From です。ですから、実装するのはいつも From にして、Into は自動で付いてくるのに任せましょう。(Rustのリンターである Clippy も、これには警告を出してくれます。)

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 の技を、今回は標準の型で使っています。

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> を使うと、コピーせずに、さまざまな型の入力を低コストで受け取れます。

理解度チェック

1. From<Celsius> for Fahrenheit を実装しました。ほかに自動で手に入るものは何ですか?

2. 300になるかもしれない i32 を u8 に変換する、安全な呼び出しはどれですか?

3. "30x40".parse::<Size>() が動くようにするには、何を実装しますか?

4. let name = "Ferris".into(); がコンパイルできないのはなぜですか?

スコア: 0 / 4