Help us understand the problem. What is going on with this article?

Rust 初心者が自動型変換や型変換関係のトレイトを自信を持って扱えるようになるための型変換まとめ 8 パターン

More than 1 year has passed since last update.

はじめに

  • Rust をはじめてみて特によくわからなくて使っていて不安になっていたのは型変換関係の仕組みです。
  • String&str として扱えますとか Vec<T>&[T] として扱えますとかいうのは、そういうものなのかー便利だなーと思う半面、よくわからないなーこわいなーと感じていました。
  • この記事ではそんな型変換がよくわからなくてもやっとしている Rust 初心者が型変換関係の全貌をなんとなく把握して自信を持って扱うことができるようになるために、自動型変換や型変換関係のトレイトの利用パターンを 8 種類まとめてみました。

まとめ

  • はじめにまとめを記載して、あとから解説をしていきます。

パターン1. メソッド呼び出し時の自動参照解決

fn main() {
    let s: String = "hello".to_string();
    // fn len(&self) -> usize を呼び出す
    // String -> &String に自動変換されて呼び出される
    assert_eq!(s.len(), 5);
    // &String は当然そのまま実行できる
    let ss: &String = &s;
    assert_eq!(ss.len(), 5);
    // &&String -> &String に自動変換されて呼び出される
    let sss: &&String = &&s;
    assert_eq!(sss.len(), 5);
}

パターン2. 参照外し時の型変換

fn main() {
    let b: Box<String> = Box::new("hello".to_string());
    // Box は Deref<T> を実装するので参照外し演算子 * 呼び出し時に T を返却する
    let s: String = *b;
    assert_eq!(s, "hello");
}

パターン3. Deref による自動変換

fn main() {
    let b: Box<String> = Box::new("hello".to_string());
    // Box は Deref<T> を実装するので &Box は &T へ自動で変換される
    let s: &String = &b;
    assert_eq!(*s, "hello");
}

パターン4. Deref による自動変換とメソッド呼び出し時の自動参照解決の組み合わせ

fn main() {
    let b: Box<String> = Box::new("hello".to_string());
    // 1. メソッド呼び出しで Box -> &Box への自動変換
    // 2. Box が Deref<T> を実装しているので &Box -> &String へ変換
    // 3. &String を引数に持つ String::len が実行される
    assert_eq!(b.len(), 5);
}

パターン5. AsRef である型として借用可能な型を扱う

use std::convert::AsRef;

// 引数 s は AsRef<str> なので str として借用可能な任意の値を受けることができる
fn is_hello<T>(s: T) where T: AsRef<str> {
    // AsRef として受け取った値は as_ref() で指定した型の参照として利用する
    assert_eq!(s.as_ref(), "hello");
}

fn main() {
    // String は AsRef<str> を実装している
    let s: String = "hello".to_string();
    is_hello(s);
    // str も AsRef<str> を実装している
    is_hello("hello");
}

パターン6. Borrow である型として借用可能で比較も可能な型を扱う

use core::borrow::Borrow;
use std::collections::HashMap;

fn main() {
    // String は Borrow<str> を実装しているので borrow() で &str を取得できる
    let s: String = "hello".to_string();
    let s: &str = s.borrow();

    // String をキーとする HashMap を作成
    let mut h: HashMap<String, bool> = HashMap::new();
    let s: String = "hello".to_string();
    h.insert(s, true);
    // String は Borrow<str> を実装しているので &str で String をキーとする HashMap の検索が可能a
    let s: &str = "hello";
    let b = s.get(s);
    assert_eq!(b.is_some(), true);
    assert_eq!(b.unwrap(), &true);
}

パターン7. From で別の型からインスタンスを生成する

let s: &str = "hello";
let s: String = String::from(s);

パターン8. Into で別の型のインスタンスを生成する

let s: &str = "hello";
let s: String = s.into();

メソッド呼び出し時の型変換

  • Rust のメソッド呼び出しでは定義された引数の型に合わせ、レシーバーの値を借用して参照型にしたり、参照型のレシーバーから参照を外したりという変換が自動で行われます。

  • 具体的な挙動の確認のため Rectangle 構造体とそのメソッド aera を定義します。

struct Rectangle { width: i32, height: i32 }

impl Rectangle {
    fn area(&self) -> i32 { self.width * self.height }
}
  • area メソッドの第一引数の &self はメソッドのレシーバーの型です。
  • &selfself: &Rectangle のシンタックスシュガーなのでメソッド定義は以下と同義です。
impl Rectangle {
    fn area(self: &Rectangle) -> i32 { self.width * self.height }
}
  • area メソッドの呼び出しはこのように行います。
fn main() {
    let r: Rectangle = Rectangle{ width: 2, height: 3 };
    assert_eq!(r.area(), 6);
}
  • メソッドのレシーバー r の型は Rectangle なので area のレシーバーの型である &Rectangle とは異なりますが、メソッド呼び出し時にレシーバーの型に合わせて自動で参照型に変換されるため、そのまま実行することができます。

  • また、参照が余分についている場合にはメソッドのレシーバーの型に合わせて自動で参照が外されます。

let rr = &r;
assert_eq!(rr.area(), 6, "rr は &Rectangle 型なので引数の型と一致しているので正常に実行できる");
let rrr = &&r;
assert_eq!(rrr.area(), 6, "rrr は &&Rectangle 型なので引数の型 &Rectangle とは異なるが、自動で参照が外されて &Rectangle に変換されてからメソッドが実行される");

参照を外す際の型変換

  • std::ops::Deref は特殊なトレイトで、実装することで参照外し演算子 * をオーバーロードしたり、代入や引数渡しやメソッド呼び出しなどのタイミングで自動的な型変換が行われるようになったりします。
  • Deref による型変換は直感的ではないし自動的に行われるため理解が難しいのですが、便利で強力なため頻繁に利用されているので、数学の公式のようなものだと思ってとりあえずはパターンを憶えてしまうのがいいと思います。

Deref による参照外し演算子のオーバーロード

  • Deref による参照外し演算子のオーバーロードは Box や Rc などのようなポインタ的な型で実装すると便利で、参照外し時にポインタが指し示している値を返すために使われています。
  • ここでは実装の例として i32 の値をフィールドに持ち、参照外し時に i32 の値を返却する Wrapper 型を定義してみます。
use std::ops::Deref;

struct Wrapper { value: i32 }

impl Deref for Wrapper {
    // Deref::Target に参照外し時の返り値の型を定義します
    type Target = i32;

    // deref メソッドで参照外し時の実装を行います
    fn deref(&self) -> &i32 {
        &self.value
    }
}
  • 参照外しを実行すると、value の値が返却されるのが確認できます。
let w = Wrapper{ value: 10 };
assert_eq!(*w, 10, "参照外しで value の値が返却される");
assert_eq!(*w, *Deref::deref(&w), "`*w` と実行した時、内部では `*Deref::deref(&w)` が実行されている");

Deref による自動型変換

  • TDeref<Target=U> を実装している場合、代入や引数渡しなどのタイミングで &T から &U への自動的な型変換が行われます。

    • Deref<Target=U> を実装しているというのは type Deref::Target = U として Deref を実装しているという意味です。
  • 標準ライブラリで Deref を実装している有名な型は String です。

  • StringDeref<Target=str> を実装しているので、&String&str に自動で変換されます。

// String の値を作成
let a: String = "hello".to_string();
// 普通の借用
let b: &String = &a;
// String は Deref<Target=str> を実装しているので &String から &str に自動で変換可能
let c: &str = &a;
// 引数の型が &str の関数呼び出し時にも &String から &str へ自動で変換される
let f = |s: &str| println!("{}", s);
f(&a);

メソッド呼び出し時の自動参照外しと Deref による自動型変換を組み合わせる

  • 最初に見たとおりメソッド呼び出し時にはレシーバーの型に合わせて参照を付けたり外したりする処理が自動で行われます。
  • この時に Deref による自動型変換も同時に行われるため、型 TDeref<Target=U> を実装しているとき TU のメソッドを実行することができます。
  • また具体的に String と str で挙動を確認していきます。
// String の値を作成する
let a: String = "hello".to_string();
// String は Deref<Target=str> を実装しているので String は str::is_ascii を実行できます
assert_eq!(a.is_ascii(), true);
// このとき自動的に行われている型変換を展開するとこのようになります
// 1. メソッド呼び出し時に自動で参照が付けられる
let b: &String = &a;
// 2. Deref の自動型変換で &String が &str になる
let c: &str = b;
// 3. &str のメソッドが実行される
assert_eq!(c.is_ascii(), true);

DerefMut

  • Deref のサブトレイトで可変参照を扱う DerefMut トレイトもあります。
  • DerefMut を実装するとミュータブルなインスタンスの参照外し時にミュータブルな参照を返却することができます。
  • DerefMut はサブトレイトなので DerefMut を実装するときは Deref も実装する必要があります。
use std::ops::{Deref, DerefMut};

struct Wrapper { value: i32 }

impl Deref for Wrapper {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.value }
}

impl DerefMut for Wrapper {
    fn deref_mut(&mut self) -> &mut i32 { &mut self.value }
}

fn main() {
    let mut w = Wrapper{ value: 10 };
    *w = 20;
    assert_eq!(*w, 20)
}

AsRef トレイトである型として借用可能な型を扱う

  • std::convert::AsRef はその名前からも察せられるように型変換に関係したトレイトです。
  • TAsRef<U> を実装している場合、T&U として借用が可能だということになります。
  • これを使うと何がうれしいかというと、例えば関数の引数の型を T ではなく AsRef<T> とすることで T に変換可能な型ならなんでも受け取ることができるようになるので、関数呼び出し時に効率良く記述できるようになります。
  • 具体的な挙動を String 型を使って見ていきます。StringAsRef<str> を実装しているので &str としての借用が可能です。
use std::convert::AsRef;

fn is_hello_str(s: &str) {
    assert_eq!("hello", s);
}

fn is_hello_asref<T: AsRef<str>>(s: T) {
    assert_eq!("hello", s.as_ref());
}

fn main() {
    let s: String = "hello".to_string();

    // is_hello_str は &str を引数とするので呼び出し前に as_str で変換する必要がある
    is_hello_str(s.as_str());

    // is_hello_asref は AsRef<str> を引数とし String は AsRef<str> を実装しているので呼び出し時の型変換が必要なくなり便利
    is_hello_asref(s);
}s

Borrow トレイトである型として借用可能であり比較が可能な型を扱う

  • std::borrow::Borrow は型変換に関係するトレイトで、型 TBorrow<U> を実装している場合 T&U として借用ができます。
  • これだけ見ると AsRef とほぼ同じですし実際に定義もほぼ同じなのですが、Borrow は比較を効率的に行うために設計されたトレイトであり、定義としては表現されていませんが Borrow<T> の時に TEqOrd などを実装して比較可能にしましょう、だとか、T のインスタンス x y があるとき x.borrow() == y.borrow() であり x == Y が成り立つようにしましょう、だとかドキュメントで推奨されている制約があります。
  • このためただ単に型変換を伴う借用が可能であることを表す場合は AsRef を実装し、比較のたの型変換を行いたいという限られた場合のみ Borrow を実装する、くらいでおぼえておけばいいと思います。

  • 実際に HashMap<K, V> での使われ方をみてみます。

  • HashMap::get の定義はこのようになっています。

    pub fn get_key_value<Q: ?Sized>(&self, k: &Q) -> Option<(&K, &V)>
        where K: Borrow<Q>,
              Q: Hash + Eq
    {
        self.search(k).map(|bucket| bucket.into_refs())
    }
  • キーの型 KBorrow<Q> であり、一致したキーを検索する HashMap::search の中で q.eq(k.borrow()) という比較が行われています。
  • このように引数で Borrow を使うことで get 呼び出し時に呼び出し側での型変換が省略されて便利です。
use std::collections::HashMap;

fn main() {
    let mut h = HashMap::<String, bool>::new();
    h.insert("hello".to_string(), true);
    // String は Borrow<str> を実装しているので検索文字列として &str を渡すことができる
    let v = h.get("hello");
    assert!(v.is_some());
    assert_eq!(v.unwrap(), &true);
}

From トレイトで別の型からインスタンスを生成する

  • std::convert::From は別の型の値を消費してインスタンスを生成するためのトレイトです。
  • TFrom<U> を実装しているとき U の値を使って T のインスタンスをコンストラクトできます。
  • 例として StringFrom<&str> を実装しているので &str から String をコンストラクトできます。
let s: &str = "hello";
let s: String = String::from(s);

Into トレイトで別の型のインスタンスを生成する

  • std::convert::Into はインスタンスを消費して別の型の値を生成するためのトレイトです。
  • TInto<U> を実装しているとき T の値から U のインスタンスをコンストラクトできます。
  • 便利な使われ方として Into<T> を関数の引数の型とすることで T を生成可能な型なら何でも受け取れるようになるので、呼び出し側での型変換が必要なくなり効率良く関数呼び出しを行うことができます。
  • 例として &strInto<String> を実装しているので &str から String を生成できます。
let s: &str = "hello";
let s: String = s.into();
  • ここで見た strInto<String> という実装は実は直接は定義されておらず、ジェネリックな定義として TFrom<U> を実装していたら UInto<T> を実装していることになるとされており、これを使って StringFrom<&str> を実装しているので &str が自動的に Into<String> を実装していることになるという仕組みを利用しています。
impl<T, U> Into<U> for T where U: From<T>
{
    fn into(self) -> U {
        U::from(self)
    }
}

おわりに

  • 型変換関係のトレイトはだいたい String で実装されているようなので具体的な実装なんかは String のドキュメント をながめてみると更に詳しくなれるかなと思いました。
nirasan
フリーで開発者をしています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした