// dictionary
変換トレイト.
標準ライブラリには、型変換をトレイトとして表現したFrom/IntoとTryFrom/TryIntoが用意されています。
From/Intoは失敗せず、損失もない変換にだけ実装するのが規約で、変換しても値が変わらないことをコードの意図として表現できます。
TryFrom/TryIntoは失敗しうる変換用で、結果をResult型で返すため、収まらない値をエラーとして検出できます。
収まらない値が黙って変わるキャストと違って変換の安全性がコードから読み取れるため、これらのトレイトで書ける変換はこちらが推奨されます。
fn main() {
let price: u8 = 200; // 商品の値段(円)
let total = i64::from(price) * 3; // 拡大変換は失敗しないのでFrom
println!("3個の合計: {}円", total);
let stock: i64 = 300;
match u8::try_from(stock) { // 縮小変換は失敗する可能性があるのでtry_from
Ok(count) => println!("u8で扱えます: {}", count),
Err(_) => println!("{}はu8(0〜255)に収まりません", stock),
}
}Playgroundで開くFromとIntoの違い
From<T> for Uは「TからUを作れる」、Into<U> for Tは「TをUに変換できる」を表し、同じ変換を反対側から見たトレイトです。呼び出し方が異なるだけですが、実装のしかたは後述のとおり非対称です。
fn main() {
let n: u8 = 200;
let a = i64::from(n); // From: 変換先の型を明示して呼ぶ
let b: i64 = n.into(); // Into: 変換先は型注釈や文脈から推論される
assert_eq!(a, b);
}Playgroundで開く標準ライブラリのブランケット実装により、Fromを実装すると対応するIntoは自動的に使えるようになります(逆は成り立ちません)。そのため自作型に実装するのはFromだけでよく、公式ドキュメントもFrom(またはTryFrom)側の実装を推奨しています1。
使い分けの慣習
どちらを呼んでも結果は同じため使い分けは慣習ですが、コミュニティではおおむね次の形が共有されています。
| 場面 | 慣習 |
|---|---|
| 変換先を明示して読みやすくしたいとき | i64::from(x)(数値変換やString::from("...")など) |
| 変換先が文脈から自明なとき | x.into()(型注釈済みの変数への代入、Ok(x.into())のような戻り値の位置など) |
| ジェネリック境界 | impl Into<String>のようにIntoを使う( From実装の型に加え、Intoのみ実装の型も受け取れる) |
浮動小数点数がからむ変換
浮動小数点型がからむ変換では、FromもTryFromも実装されていない組み合わせが多く、asによるキャストしか手段がないケースがあります。変換先で元の値を必ず正確に表現できる組み合わせにのみ標準ライブラリがFromを実装しているためです。
| 変換 | 使えるトレイト | 理由 |
|---|---|---|
f32 → f64 |
From |
拡大変換なので値がそのまま保たれる |
f64 → f32 |
なし(asのみ) |
精度が落ちる/範囲外は無限大になる |
f32・f64 → 整数型 |
なし(asのみ) |
小数部が失われる |
u8/i8/u16/i16 → f32・f64 |
From |
仮数部(f32は24ビット)に必ず収まる |
u32/i32 → f64 |
From |
仮数部(f64は53ビット)に必ず収まる |
u32/i32 → f32・u64i64以上 → f64 |
なし(asのみ) |
桁が仮数部を超えると丸めが起きる |
TryFromも同様に用意されていないため、f64::try_fromやi32::try_fromで書こうとするとトレイト境界を満たさずコンパイルエラーになります。
fn main() {
let ratio: f64 = 0.5;
let _a = f32::try_from(ratio); // エラー: E0277(f32はTryFrom<f64>未実装)
let _b = i32::try_from(ratio); // エラー: E0277(i32はTryFrom<f64>未実装)
let _c = f32::from(1i32); // エラー: E0277(f32はFrom<i32>未実装)
}Playgroundで開くそのため、これらの変換はキャストで書きます。範囲外の値が飽和したりNaNが0になったりする挙動はasの規則どおりなので、丸め方を決めたい場合はround・trunc・floorなどを先に呼びます。
fn main() {
let price: f64 = 128.7; // 税込価格
println!("四捨五入: {}円", price.round() as i32); // 129円
let single = price as f32; // f64→f32は精度が落ちるがasなら可能
println!("f32にすると: {}", single); // 128.7
println!("f64に戻すと: {}", f64::from(single)); // 128.6999969482422(誤差が見える)
}Playgroundで開く補足
TryFrom/TryIntoも同じ構図
TryFromを実装するとTryIntoが自動的に使えるようになる点、ジェネリック境界にはTryIntoを使う点など、From/Intoとまったく同じ関係です。失敗時のエラー型は関連型Errorで表現され、整数型どうしの縮小変換ではTryFromIntErrorが返ります。
浮動小数点数から整数への変換で失敗を検出したい場合
TryFromがない(Rust 1.97時点)ため、収まらない値を弾きたいときは自前で判定します。asは範囲外を飽和させるだけで、値が壊れたことを知らせてくれません。
fn to_i32(value: f64) -> Option<i32> {
// 有限かつ範囲内で、小数部を持たない値だけを受け付ける
if value.is_finite() && value.trunc() == value
&& value >= i32::MIN as f64 && value <= i32::MAX as f64
{
Some(value as i32)
} else {
None
}
}
fn main() {
println!("{:?}", to_i32(1200.0)); // Some(1200)
println!("{:?}", to_i32(1200.5)); // None(小数部がある)
println!("{:?}", to_i32(1e10)); // None(asなら2147483647に飽和してしまう)
}Playgroundで開くFootnotes
この辞書が使われているページ
backlinks 1