5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Rust] 配列やベクタが自動でスライスに変換される仕組み

Last updated at Posted at 2021-09-05

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 型強制」の仕組みの概要は以下の通り:

  • ベクタ VecDeref<Target = [T]> を実装し、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 に関して以下のように実装されています。

&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]SizedUnsize<[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 トレイトがブランケット実装されます。

&[T; N] -> &[T] での実装
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 型強制

ベクタの場合は配列よりも単純で、ベクタ VecDeref<Target = [T]> を実装しているため、型強制サイトにおいて「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] の型強制ができることになりますが、実際にはスライス [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 文での例
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<Target = U> のとき &T -> &U) ため、現バージョンの Rust でも自動参照外しと Deref 型強制の併用はできます。

参考「More on Deref coercion - Deref in std::ops - Rust

let 文での例
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]
5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?