Rust では変数の型が自動で変換される場面が多々あります。
receiver.method(…) 形式でのメソッドを呼び出しで行われる型変換と、let 文や関数引数等で行われる型変換は仕組みが異なり、本記事では後者の let 文や関数引数等で行われる型変換についてまとめます。
メソッド呼び出しについては別記事にしました。
参考「[Rust] メソッド呼び出し時におけるメソッド探索の仕組み: 自動参照 & 自動参照外し及び Unsized 型強制 – Qiita」
0. まとめ
※ Rust では [T] 型を「スライス」と呼ぶ場合と &[T] 型を「スライス」と呼ぶ場合がありますが、多くの場合は &[T] を意味します。本記事では前者の [T] の意味で使用します。
-
- 配列 [T; N] とスライス [T] は異なる型である (is-a 関係でない) 。
-
- 配列 [T; N] は「Unsized 型強制」され、スライス [T] になる。
- ベクタ Vec は「Deref 型強制」され、スライス [T] になる。
配列 [T; N] の「Unsized 型強制」の仕組みの概要は以下の通り:
-
- 配列 [T; N] は Unsize<[T]> を (コンパイラによって自動的に) 実装する
-
- 参照等に対し、CoerceUnsized トレイトがブランケット実装され、Rust の型強制 (暗黙的、自動的な型変換) の対象になる
例: impl<‘a, ‘b, T, const N: usize> CoerceUnsized<&’a [T]> for &’b [T; N] where ‘b: ‘a {}
ライフタイムに関して配列の参照がスライスの参照よりも長生きのとき、型強制サイト (型強制が可能な場所) において、「Unsized 型強制」によって配列の参照 &[T; N] がスライスの参照 &[T] に型強制される
ベクタ Vec の「Deref 型強制」の仕組みの概要は以下の通り:
-
- ベクタ Vec は Deref を実装し、Rust の型強制 (暗黙的、自動的な型変換) の対象になる
型強制サイト (型強制が可能な場所) において、「Deref 型強制」によってベクタの参照 &Vec がスライスの参照 &[T] に型強制される
※ Rust では構造体やトレイト等を継承することはできません (継承に近いことをする手段はありますが本記事では不説明) 。
※「スーパートレイト」と「サブトレイト」はそれ自体は継承関係や is-a 関係ではありません。
※ Rust におけるサブタイプ (派生型) はライフタイム関係の機能で、継承の機能ではありません。
参考「Using Supertraits to Require One Trait’s Functionality Within Another Trait – Advanced Traits – The Rust Programming Language」
参考「Subtyping and Variance – The Rust Reference」
1. 配列 [T; N] から変換
1.1. 配列 [T; N] とスライス [T] は異なる型
配列 [T; N] とスライス [T] は is-a 関係でなく、直接型変換することはできません。
let foo: [i32; 3] = [10, 20, 30];
// let bar: [i32] = foo; // 型変換不可: [i32; 3] -> [i32]
1.2. Unsizeトレイトは動的サイズ型に変換できる特性
Rust において “sized” はコンパイル時にサイズを決定できる (固定サイズである) 特性を意味し、”unsize” は接頭辞 “un-” が名詞 “size” についているので直訳すると「固定サイズという特性をなくす」、すなわち「固定サイズ型から動的サイズ型に変換する」という意味になります。
参考「un-の意味・使い方・読み方 | Weblio英和辞書」
Unsize トレイトはコンパイラによって自動的に実装されます。
配列 [T; N] は Unsize<[T]> を実装し、型強制サイト (型強制が可能な場所) において CoerceUnsized トレイトの実装に従って (※後述) 配列 [T; N] をスライス [T] に変換できることを意味します。
※ let slice: [_] = array; のような単なる変数束縛は CoerceUnsized トレイトがブランケット実装されない (※後述) ため、型強制されません (型指定ありの let 文自体は型強制サイトです) 。
参考「Unsize in std::marker – Rust」
参考「Coercion sites – Type coercions – The Rust Reference」
1.3. CoerceUnsized トレイトのブランケット実装
Rust には、トレイト境界等の条件を満たす型に他のトレイトを実装させる「ブランケット実装」という機能があります。
参考「Using Trait Bounds to Conditionally Implement Methods – Traits: Defining Shared Behavior – The Rust Programming Language」
CoerceUnsized トレイトは参照等の「ポインタのような型」に対してブランケット実装されます。
例えば、&T -> &U に関して以下のように実装されています。
impl<'a, 'b, T, U> CoerceUnsized<&'a U> for &'b T
where
'b: 'a,
T: Unsize<U> + ?Sized,
U: ?Sized,
{
}
参考「CoerceUnsized in std::ops – Rust」
トレイト境界 B: A は「型 B がトレイト A を実装している」関係 (has-a 関係) を条件とすることを表します。
ライフタイム境界 ‘b: ‘a は「ライフタイム b は少なくともライフタイム a の間に生きている必要がある」すなわち「ライフタイム b はライフタイム a よりも長生きである」条件を表します。
参考「Bounds – Rust By Example」(「トレイト境界」)
参考「Bounds – Rust By Example」(「ライフタイム境界」)
Sized トレイトはコンパイラによって自動的に実装されます。
英語が苦手だと混乱しやすいですが、Sized トレイトは「固定サイズである特性」で、Unsize トレイトは「動的サイズ型に変換できる特性」なので、排反ではありません (実際、配列 [T; N] は Sized と Unsize<[T]> の両方を実装します) 。
トレイト境界 ?Sized は暗黙のトレイト境界 Sized を除去するためのものですが、「Sized トレイトを実装しない型ても良い」という意味なので、「Sized トレイトを実装している型も」条件を満たします。
参考「Unsize in std::marker – Rust」
参考「Sized in std::marker – Rust」
参考「Sized – Special types and traits – The Rust Reference」
よって [T; N]: Unsize<[T]> の関係から type T = [T; N];, type U = [T]; として CoerceUnsized トレイトがブランケット実装されます。
impl<'a, 'b, T, const N: usize> CoerceUnsized<&'a [T]> for &'b [T; N]
where
'b: 'a,
{
}
1.4. Unsized 型強制
前述の通り、Rust において “unsize” は「固定サイズ型から動的サイズ型に変換する」という意味です。
コンパイラによって Unsize トレイトが実装され、結果として CoerceUnsized トレイトがブランケット実装されます。
コンパイラは型強制サイトにおいて CoerceUnsized トレイト の実装に基づいて型強制をします (「Unsized 型強制」) 。
let foo: &[i32; 3] = &[10, 20, 30];
let bar: &[i32] = foo; // Unsized 型強制: &[i32; 3] -> &[i32]
参考「CoerceUnsized in std::ops – Rust」
参考「Unsized Coercions – Type coercions – The Rust Reference」
Unsize は手動で (Rust 上のコードによって) 実装することはできませんが、「Unsized 型強制」の条件から、以下のコードのように配列とスライスの型変換を疑似的に表現することができます。
struct Something<T: ?Sized>(T);
type MyArray = Something::<[i32; 3]>;
type MySlice = Something::<[i32]>;
let foo: &MyArray = &Something([10, 20, 30]);
let bar: &MySlice = foo; // Unsized 型強制: &MyArray -> &MySlice
2. ベクタ Vec から変換
2.1. Deref 型強制
ベクタの場合は配列よりも単純で、ベクタ Vec が Deref を実装しているため、型強制サイトにおいて「Deref 型強制」されます。
let foo: &Vec<i32> = &vec![10, 20, 30];
let bar: &[i32] = foo; // Deref 型強制: &Vec<i32> -> &[i32]
参考「Deref – Vec in std::vec – Rust」
参考「Deref in std::ops – Rust」
ちなみに「Deref 型強制」の条件だけ見ると、理論上、参照外し演算子 * を用いて Vec -> [T] の型強制ができることになりますが、実際にはスライス [T] が動的サイズ型のため、少なくとも現バージョン (Rust 2018, 1.55.0) の Rust では利用できません。
let foo: Vec<i32> = vec![10, 20, 30];
// let bar: [i32] = *foo; // Deref 型強制: *foo -> *Deref::deref(&foo) // 現バージョンでは不可
2.2. Deref トレイトの使用例
Deref トレイトの動作を確認するために、例として実装します。
use std::ops::Deref;
struct Foo(i32);
impl Deref for Foo {
type Target = i32;
fn deref(&self) -> &i32 {
&self.0
}
}
//
let foo: Foo = Foo(23);
let bar: &Foo = &foo;
let baz: &i32 = bar; // Deref 型強制: &Foo -> &i32
let qux: i32 = *foo; // Deref 型強制: *foo -> *Deref::deref(&foo)
参考「Deref in std::ops – Rust」
3. 推移的な型強制
3.1. 自動参照外しと Unsized 型強制は両立できない (今後のバージョンで変更される可能性あり?)
理想的には推移的に型強制されて欲しいですが、少なくとも現在 (Rust 2018, 1.55.0) のバージョンでは自動参照外しと Unsized 型強制の両立ができません。
Rust のリファレンスに
T_1 to T_3 where T_1 coerces to T_2 and T_2 coerces to T_3
(transitive case)Note that this is not fully supported yet.
と書かれているため、”not fully supported yet” な (「まだ完全にはサポートされていない」) ケースかと思います。
※ receiver.method(…) 形式でのメソッド呼び出しに関しては仕組みが異なるため、参照外しと Unsized 型強制の両方が行われます。
参考「Coercion types – Type coercions – The Rust Reference」
参考「[Rust] メソッド呼び出し時におけるメソッド探索の仕組み: 自動参照 & 自動参照外し及び Unsized 型強制 – Qiita」
let foo: &[i32; 3] = &[10, 20, 30];
let bar: &[i32] = foo; // Unsized 型強制: &[i32; 3] -> &[i32]
let baz: &[i32; 3] = &&&foo; // 自動参照外し: &&&&[i32; 3] -> &[i32; 3]
// let qux: &[i32] = &&&foo; // エラー発生
trait Foo {
fn something(&self, _other: &[i32]);
}
impl Foo for [i32] {
fn something(&self, _other: &[i32]) {
println!("Foo");
}
}
//
let foo: &[i32; 3] = &[10, 20, 30];
let bar: &[i32; 3] = &[40, 50, 60];
foo.something(bar); // bar は Unsized 型強制: &[i32; 3] -> &[i32]
<[i32] as Foo>::something(foo, bar); // Unsized 型強制: &[i32; 3] -> &[i32]
// foo.something(&&&bar); // エラー発生
// <[i32] as Foo>::something(&&&foo, &&&bar); // エラー発生
//
let baz: &[i32] = foo; // Unsized 型強制: &[i32; 3] -> &[i32]
let qux: &[i32] = bar; // Unsized 型強制: &[i32; 3] -> &[i32]
baz.something(qux); // qux は自動参照外し: &&&&[i32] -> &[i32]
<[i32] as Foo>::something(baz, qux); // 自動参照外し: &&&&[i32] -> &[i32]
3.2. 自動参照外しと Deref 型強制は両立できる
そもそも Deref 型強制は参照外しするときに起きる (T: Deref のとき &T -> &U) ため、現バージョンの Rust でも自動参照外しと Deref 型強制の併用はできます。
参考「More on Deref coercion – Deref in std::ops – Rust」
let foo: &Vec<i32> = &vec![10, 20, 30];
let bar: &[i32] = foo; // Deref 型強制: &Vec<i32> -> &[i32]
let baz: &Vec<i32> = &&&foo; // 自動参照外し: &&&&Vec<i32> -> &Vec<i32>
let qux: &[i32] = &&&foo; // 推移的な型強制: &&&&Vec<i32> -> &Vec<i32> -> &[i32]
trait Foo {
fn something(&self, _other: &[i32]);
}
impl Foo for [i32] {
fn something(&self, _other: &[i32]) {
println!("Foo");
}
}
//
let foo: &Vec<i32> = &vec![10, 20, 30];
let bar: &Vec<i32> = &vec![10, 20, 30];
foo.something(bar); // bar は Deref 型強制: &Vec<i32> -> &[i32]
<[i32] as Foo>::something(foo, bar); // Deref 型強制: &Vec<i32> -> &[i32]
foo.something(&&&bar); // &&&bar は推移的な型強制: &&&&Vec<i32> -> &Vec<i32> -> &[i32]
<[i32] as Foo>::something(&&&foo, &&&bar); // 推移的な型強制: &&&&Vec<i32> -> &Vec<i32> -> &[i32]
//
let baz: &[i32] = foo; // Deref 型強制: &Vec<i32> -> &[i32]
let qux: &[i32] = bar; // Deref 型強制: &Vec<i32> -> &[i32]
baz.something(qux); // qux は自動参照外し: &&&&[i32] -> &[i32]
<[i32] as Foo>::something(baz, qux); // 自動参照外し: &&&&[i32] -> &[i32]