はじめに
- 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 の検索が可能
let s: &str = "hello";
let b = h.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
はメソッドのレシーバーの型です。 -
&self
はself: &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 による自動型変換
-
型
T
がDeref<Target=U>
を実装している場合、代入や引数渡しなどのタイミングで&T
から&U
への自動的な型変換が行われます。-
Deref<Target=U>
を実装しているというのはtype Deref::Target = U
として Deref を実装しているという意味です。
-
-
標準ライブラリで Deref を実装している有名な型は
String
です。 -
String
はDeref<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 による自動型変換も同時に行われるため、型
T
がDeref<Target=U>
を実装しているときT
はU
のメソッドを実行することができます。 - また具体的に 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
はその名前からも察せられるように型変換に関係したトレイトです。 - 型
T
がAsRef<U>
を実装している場合、T
は&U
として借用が可能だということになります。 - これを使うと何がうれしいかというと、例えば関数の引数の型を
T
ではなくAsRef<T>
とすることで T に変換可能な型ならなんでも受け取ることができるようになるので、関数呼び出し時に効率良く記述できるようになります。 - 具体的な挙動を
String
型を使って見ていきます。String
はAsRef<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
は型変換に関係するトレイトで、型T
がBorrow<U>
を実装している場合T
は&U
として借用ができます。 -
これだけ見ると
AsRef
とほぼ同じですし実際に定義もほぼ同じなのですが、Borrow
は比較を効率的に行うために設計されたトレイトであり、定義としては表現されていませんがBorrow<T>
の時にT
はEq
やOrd
などを実装して比較可能にしましょう、だとか、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())
}
- キーの型
K
がBorrow<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
は別の型の値を消費してインスタンスを生成するためのトレイトです。 -
T
がFrom<U>
を実装しているときU
の値を使ってT
のインスタンスをコンストラクトできます。 - 例として
String
はFrom<&str>
を実装しているので&str
からString
をコンストラクトできます。
let s: &str = "hello";
let s: String = String::from(s);
Into トレイトで別の型のインスタンスを生成する
-
std::convert::Into
はインスタンスを消費して別の型の値を生成するためのトレイトです。 -
T
がInto<U>
を実装しているときT
の値からU
のインスタンスをコンストラクトできます。 - 便利な使われ方として
Into<T>
を関数の引数の型とすることでT
を生成可能な型なら何でも受け取れるようになるので、呼び出し側での型変換が必要なくなり効率良く関数呼び出しを行うことができます。 - 例として
&str
はInto<String>
を実装しているので&str
からString
を生成できます。
let s: &str = "hello";
let s: String = s.into();
- ここで見た
str
のInto<String>
という実装は実は直接は定義されておらず、ジェネリックな定義としてT
がFrom<U>
を実装していたらU
はInto<T>
を実装していることになるとされており、これを使ってString
がFrom<&str>
を実装しているので&str
が自動的にInto<String>
を実装していることになるという仕組みを利用しています。
impl<T, U> Into<U> for T where U: From<T>
{
fn into(self) -> U {
U::from(self)
}
}
おわりに
- 型変換関係のトレイトはだいたい
String
で実装されているようなので具体的な実装なんかは String のドキュメント をながめてみると更に詳しくなれるかなと思いました。