Yabutan 技術ブログ > Rust 型変換トレイトについて (From, TryFrom, FromStr)

Rust 型変換トレイトについて (From, TryFrom, FromStr)

#Rust

2023-02-26

Rustの型変換の実装について、 この記事では、From, TryFrom, ToString, Display, FromStr トレイトの実装方法と、使い分けについて紹介します。

From, Into

Fromトレイトを実装するとIntoトレイトも自動で実装されます。
実装された型は、from, into メソッドで型変換ができるようになります。

From実装のサンプル

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// (i32, i32)からPointに変換する実装
impl From<(i32, i32)> for Point {
    fn from((x, y): (i32, i32)) -> Self {
        Point { x, y }
    }
}

From実装の使用例

#[test]
fn test_from() {
    let p = Point::from((1, 2));
    assert_eq!(p, Point { x: 1, y: 2 });
}

#[test]
fn test_into() {
    let p: Point = (1, 2).into();
    assert_eq!(p, Point { x: 1, y: 2 });
}

Fromトレイトを実装すると、Intoトレイトも自動で実装されています。
intoメソッドを使う際には、型推論ができる状態か、宣言に型を明記しておきましょう。

TryFrom, TryInto

FromとIntoトレイトと役割は同じですが、Result で返せるので、 型変換できなかった場合を考慮する必要があるときは、こちらを使います。

TryFromトレイトを実装するとTryIntoトレイトも自動で実装されます。
try_from, try_into メソッドで変換ができるようになります。

TryFrom実装のサンプル

#[derive(Debug, PartialEq)]
enum MyEnum {
    A,
    B,
    C,
}

// 変換エラー時のError型
#[derive(Debug, PartialEq)]
struct MyEnumIllegalValue(String);

// i32からMyEnumに変換する実装
impl TryFrom<i32> for MyEnum {
    type Error = MyEnumIllegalValue;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        use MyEnum::*;

        match value {
            0 => Ok(A),
            1 => Ok(B),
            2 => Ok(C),
            _ => Err(MyEnumIllegalValue(format!("Illegal value: {}", value))),
        }
    }
}

MyEnumIllegalValue型は、変換エラー発生時に使う型で、TryFromの関連型として定義しています。
もし、詳細なエラー情報が不要になる場合は、エラー生成にコストがかかるのでなしにしても良いと思います。
(関連型を type Error = (); とするなど)

TryFrom実装の使用例

#[test]
fn test_try_from() {
    assert_eq!(MyEnum::try_from(0), Ok(MyEnum::A));
    assert_eq!(MyEnum::try_from(1), Ok(MyEnum::B));
    assert_eq!(MyEnum::try_from(2), Ok(MyEnum::C));

    assert_eq!(
        MyEnum::try_from(3),
        Err(MyEnumIllegalValue("Illegal value: 3".to_string()))
    );
}

#[test]
fn test_try_into() {
    assert_eq!(0.try_into(), Ok(MyEnum::A));
    assert_eq!(1.try_into(), Ok(MyEnum::B));
    assert_eq!(2.try_into(), Ok(MyEnum::C));

    // Error型と比較している為、目的の型を明示するためにUFCS形式で書いています。
    // ?演算子を使う場合は、型推論が働くので、UFCS形式は不要です。
    assert_eq!(
        <i32 as TryInto<MyEnum>>::try_into(4),
        Err(MyEnumIllegalValue("Illegal value: 4".to_string()))
    );
}

ToString, fmt::Display

特定の型を単純に文字列化したければ、ToStringを実装すればよいですが、 fmt::Displayトレイトで実装しておくと、ToStringも自動的に実装されます。
しかも、こちらの方が柔軟性が高いです。

use std::fmt;

struct Person {
    name: String,
    age: u32,
}

impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}({})", self.name, self.age)
    }
}
#[test]
fn test_display() {
    let person = Person {
        name: "John".to_string(),
        age: 42,
    };

    assert_eq!(person.to_string(), "John(42)");
}

ToStringも自動で実装されているのがわかります。

FromStr

文字列から、parse関数を使って特定の型に変換したい場合には、FromStrトレイトを使うのが直観的です。

FromStr実装のサンプル

use std::fmt;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct Person {
    name: String,
    age: u32,
}

/// 変換失敗時のエラー型
#[derive(Debug, PartialEq)]
struct PersonParseError(String);

// 文字列から変換する実装
impl FromStr for Person {
    type Err = PersonParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use nom::bytes::complete::tag;
        use nom::character::complete::{alpha1, u32};
        use nom::sequence::delimited;
        use nom::IResult;

        /// nomを使って、"name(age)" 形式の文字列をパースする。
        fn parse_person(input: &str) -> IResult<&str, Person> {
            let (input, name) = map(alpha1, |s: &str| s.to_owned())(input)?;
            let (input, age) = delimited(tag("("), u32, tag(")"))(input)?;
            Ok((input, Person { name, age }))
        }

        // 変換に失敗した場合はPersonParseErrorを返す。
        parse_person(s)
            .map(|(_, p)| p)
            .map_err(|_| PersonParseError(s.to_string()))
    }
}

FromStr実装の使用例

#[test]
fn test_from_str() {
    // 文字列からPersonに変換できること。
    let p: Person = "John(42)".parse().unwrap();
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 42);

    // 失敗した場合、PersonParseErrorが返ること。
    let p: Result<Person, _> = "illegal word".parse();
    assert_eq!(p, Err(PersonParseError("illegal word".to_string())));
}

まとめ

型変換したい場合、どのトレイトを実装しておくのが良いか?

  1. 文字列から特定の型に変換したい場合、FromStrを使う。
  2. 特定の型を文字列に変換したい場合、Displayを使う。
  3. 文字列以外の型を、特定の型に変換したい場合、TryFromを使う。
  4. 文字列以外の型を、特定の型を変換したい場合、失敗しないのであれば Fromを使う。