6
6

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] メソッド呼び出し時におけるメソッド探索の仕組み: 自動参照 & 自動参照外し及び Unsized 型強制

Last updated at Posted at 2021-09-03

Rust ではメソッド呼び出し時に自動で参照の付け外しや Unsized 型強制が行われます。

メソッド呼び出し以外での型強制については配列やベクタを例として別記事にしました (自動参照外しと Deref 型強制、Unsized 型強制のみ) 。

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

0. まとめ

receiver.method(...) のように、メソッドを呼ぼうとしている変数を「レシーバ (receiver)」と呼ぶ。

  1. レシーバの型から可能な限り参照外し (Deref) と Unsized 型強制を行い、「候補型の一覧」を作る。
  2. 候補型 T それぞれに借用演算子 (参照演算子) を付けた &T, &mut T を候補型に追加する (位置はそれぞれの T のすぐ後) 。
  3. 候補型 T, &T, &mut T それぞれに対し、型の固有メソッドやトレイト実装メソッド (拡張メソッド) を探して、メソッドの名前と Self 引数の self の型が一致するものを探す。
    (※探す段階では Self 引数のライフタイム等は確認されない。)
    (※探す段階では Self 引数以外の引数は確認されない。)

※ Self 引数の可変性については扱いが少し紛らわしいため、後述。

  • 候補型を順番に確認し、最初にメソッドが見つかった候補型に決定する。
  • 候補型が重複する複数のメソッドが見つかった場合は以下の通り:
    • 固有メソッドと拡張メソッドで重複した場合は、固有メソッドに決定される。
    • 拡張メソッド同士で重複した場合はエラーになる。
  • 上記の手順によってメソッドを決定できても、他の条件によってそのメソッドを呼べない場合はエラーになる。

※「完全修飾構文」<Type as Trait>::method(receiver, ...) では Self 引数 (レシーバ receiver) を含む引数で自動参照外しや「Deref 型強制」、「Unsized 型強制」は行われる (※一部両立不可) が、自動参照はされない。
※「完全修飾構文」におけるレシーバの自動参照外しや Unsized 型強制は「メソッド探索」によるものではなく「関数の引数として受け取る」ときに自動参照外しや「Deref 型強制」、「Unsized 型強制」が起きる (※一部両立不可) ため (本記事では不説明) 。
※関数の引数等では自動参照外しと「Unsized 型強制」に関してどちらか一方なら行われるが、「メソッド探索」のような両立はできない (※今後のバージョンで変更される可能性あり) 。

参考「Method call expressions - The Rust Reference
参考「More on Deref coercion - Deref in std::ops - Rust」(「Deref 型強制」)
参考「Unsized Coercions - Type coercions - The Rust Reference」(「Unsized 型強制」)

※ちなみに日本語版の Rust のドキュメントでは "Fully Qualified Syntax" の訳が「パス Path」を補って「フルパス記法」となっているのですが、直訳で「完全修飾構文」で十分意味が分かると思うので、本記事では「完全修飾構文」と呼びます。

参考「Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name - Advanced Traits - The Rust Programming Language
参考「明確化のためのフルパス記法: 同じ名前のメソッドを呼ぶ - 高度なトレイト - The Rust Programming Language 日本語版

1. メソッド探索の流れ

1.1. 候補型の一覧を作る

以下のソースコードの例で考えます。

use std::ops::Deref;

struct Foo([i32; 3]);
impl Foo {
    fn foo(&self) {
        println!("Foo");
    }
    fn bar(&self) {
        println!("Foo");
    }
}
impl Deref for Foo {
    type Target = [i32; 3];

    fn deref(&self) -> &[i32; 3] {
        &self.0
    }
}

struct Bar(Foo);
impl Bar {
    fn bar(&self) {
        println!("Bar");
    }
}
impl Deref for Bar {
    type Target = Foo;

    fn deref(&self) -> &Foo {
        &self.0
    }
}

// 
let receiver = &&&Bar(Foo([10, 20, 30]));

receiver.bar(); // 候補型 &Bar
receiver.foo(); // 候補型 &Foo
receiver.len(); // 候補型 &[i32]

// 候補型 &[i32; 3] のパターンは略

Rust のプリミティブ型参照 &TDeref トレイトを実装していて、他の Deref トレイトを実装する型と同様に参照外しされます。

参照外し: *x == *Deref::deref(&x)

参考「reference - Rust
参考「Deref in std::ops - Rust

可能な限り参照外しと Unsized 型強制を行い、候補型の一覧を作ります。

参考「Unsized Coercions - Type coercions - The Rust Reference」(「Unsized 型強制」)

  • &&&Bar
  • &&Bar: 参照外し
  • &Bar: 参照外し
  • Bar: 参照外し
  • Foo: 参照外し
  • [i32; 3]: 参照外し
  • [i32]: Unsized 型強制

候補型 T それぞれに借用演算子 (参照演算子) を付けた &T, &mut T を候補型に追加 (自動参照) します。

  • &&&Bar
    • &&&&Bar, &mut &&&Bar
  • &&Bar: 参照外し
    • &&&Bar, &mut &&Bar
  • &Bar: 参照外し
    • &&Bar, &mut &Bar
  • Bar: 参照外し
    • &Bar, &mut Bar
  • Foo: 参照外し
    • &Foo, &mut Foo
  • [i32; 3]: 参照外し
    • &[i32; 3], &mut [i32; 3]
  • [i32]: Unsized 型強制
    • &[i32], &mut [i32]

参考「Method call expressions - The Rust Reference

1.2. 候補型の一覧の順番でメソッドを探す

候補型の一覧から bar メソッドを順番に探索すると、理論上は以下の 2 つが考えられます。

  • 候補型 &Bar
    • Bar::bar(&self) すなわち Bar::bar(self: &Bar)
  • 候補型 &Foo
    • Foo::bar(&self) すなわち Foo::bar(self: &Foo)

Bar::bar(&self) の方が先に見つかるため、候補型 &Bar のメソッド Bar::bar(&self) になります。

foo メソッドも同様に探索して、候補型 &Foo のメソッド Foo::foo(&self) に決定します。

&selfself: &Self のシンタックスシュガー (「簡略表記 self」) で、関数の引数等で参照外しに用いられる「参照パターン」ではありません。

参考「Functions - The Rust Reference」(「簡略表記 self」、ShorthandSelf)
参考「Reference patterns - Patterns - The Rust Reference」(「参照パターン」、ReferencePattern)

2. 候補型が重複する複数のメソッドが見つかる場合

2.1. 拡張メソッドが複数ある場合

候補型が異なる複数のメソッドが見つかった場合には先に見つかるメソッドに決定されます (※前述) が、候補型が重複するトレイト実装メソッド (拡張メソッド) が複数あるとエラーになります。

2.1.1. 同一の型が実装するメソッドの場合

※「候補型」と「メソッドを実装する型」は同じとは限りません (参照の「借用 self」の場合やトレイト実装メソッドの場合) 。

trait Foo {
    fn something(&self);
}

trait Bar {
    fn something(&self);
}

struct Something;
impl Foo for Something {
    fn something(&self) {
        println!("Foo");
    }
}
impl Bar for Something {
    fn something(&self) {
        println!("Bar");
    }
}

//
let receiver = &Something;

receiver.something(); // エラー発生

ここでは Something 型が Foo トレイトと Bar トレイトを実装していて、それぞれ something(&self) メソッドを持っているため、このままではエラーになります。

コンパイラの出力例
error[E0034]: multiple applicable items in scope
  --> src/main.rs:25:10
   |
25 | receiver.something(); // エラー発生
   |          ^^^^^^^^^ multiple `something` found
   |
note: candidate #1 is defined in an impl of the trait `Foo` for the type `Something`
  --> src/main.rs:12:5
   |
12 |     fn something(&self) {
   |     ^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `Bar` for the type `Something`
  --> src/main.rs:17:5
   |
17 |     fn something(&self) {
   |     ^^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #1
   |
25 | Foo::something(&receiver); // エラー発生
   | ^^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #2
   |
25 | Bar::something(&receiver); // エラー発生
   | ^^^^^^^^^^^^^^^^^^^^^^^^^

候補型 &Something

  • 候補 #1: Something 型における Foo::something(&self) の実装
  • 候補 #2: Something 型における Bar::something(&self) の実装

これを解決するには、「固有メソッドと拡張メソッドに分離する」(※後述) か、「型やトレイトを明示するメソッド呼び出しに書き換え」ます (「完全修飾構文」) 。

※「完全修飾構文」では Self 引数を含む引数で自動参照外しや「Deref 型強制」、「Unsized 型強制」は行われます (※一部両立不可) が、自動参照はされません。

// ... 型の定義略

//
let receiver = &Something;

<Something as Foo>::something(receiver); // トレイトを明示
// or
<Something as Foo>::something(&receiver); // トレイトを明示: 自動参照外し
// or
Foo::something(receiver); // Self 引数に基づく静的ディスパッチ

// Note: Self 引数に基づく静的ディスパッチ時は自動参照外しされない
// Foo::something(&receiver); // エラー発生

参考「E0034 - Rust Compiler Error Index」("error[E0034]: multiple applicable items in scope")
参考「Disambiguating Function Calls - Call expressions - The Rust Reference
参考「Qualified paths - Paths - The Rust Reference

参考「[Rust] 引数 self や戻り値の型 Self に基づくメソッドディスパッチ - Qiita

2.1.2. 異なる型が実装するメソッドの場合

※「候補型」と「メソッドを実装する型」は同じとは限りません (参照の「借用 self」の場合やトレイト実装メソッドの場合) 。

異なる型が実装するメソッドであっても、同一の候補型に対して、複数の拡張メソッドが実装されることがあります。

trait Foo {
    fn something(&self);
}

trait Bar {
    fn something(self); // &self でなく self
}

struct Something;
impl Foo for Something {
    fn something(&self) {
        println!("Foo");
    }
}

impl Bar for &Something { // Something でなく &Something
    fn something(self) { // &self でなく self
        println!("Bar");
    }
}

//
let receiver = &Something;

receiver.something(); // エラー発生
コンパイラの出力例
error[E0034]: multiple applicable items in scope
  --> src/main.rs:26:14
   |
26 |     receiver.something(); // エラー発生
   |              ^^^^^^^^^ multiple `something` found
   |
note: candidate #1 is defined in an impl of the trait `Foo` for the type `Something`
  --> src/main.rs:12:9
   |
12 |         fn something(&self) {
   |         ^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `Bar` for the type `&Something`
  --> src/main.rs:18:9
   |
18 |         fn something(self) {
   |         ^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #1
   |
26 |     Foo::something(&receiver); // エラー発生
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the associated function for candidate #2
   |
26 |     Bar::something(&receiver); // エラー発生
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^

候補型 &Something

  • 候補 #1: Something 型における Foo::something(&self) の実装
  • 候補 #2: &Something 型における Bar::something(self) の実装

※メソッドの Self 引数の &self は「借用 self」、借用なしの self は「消費 self」と呼ばれます。
※「消費 self」の実例として Into::into(self) メソッド等があります。

参考「self - Rust

2.2. 固有メソッドと拡張メソッドがある場合

同一の候補型に対して固有メソッドと拡張メソッドが見つかった場合には固有メソッドに決定されます。

2.2.1. 同一の型が実装するメソッドの場合

※「候補型」と「メソッドを実装する型」は同じとは限りません (参照の「借用 self」の場合やトレイト実装メソッドの場合) 。

trait Bar {
    fn something(&self);
}

struct Foo;
impl Foo {
    fn something(&self) {
        println!("Foo");
    }
}
impl Bar for Foo {
    fn something(&self) {
        println!("Bar");
    }
}

//
let receiver = &Foo;

receiver.something(); // 候補型 &Foo

候補型 &Foo

  • 候補 #1: Foo 型における固有メソッド Foo::something(&self) の実装
  • 候補 #2: Foo 型における拡張メソッド Bar::something(&self) の実装

固有メソッドが優先され、Foo::something(&self) に決定されます。

2.2.2. 異なる型が実装するメソッドの場合

※「候補型」と「メソッドを実装する型」は同じとは限りません (参照の「借用 self」の場合やトレイト実装メソッドの場合) 。

trait Bar {
    fn something(self); // &self でなく self
}

struct Foo;
impl Foo {
    fn something(&self) {
        println!("Foo");
    }
}

impl Bar for &Foo { // Foo でなく &Foo
    fn something(self) { // &self でなく self
        println!("Bar");
    }
}

//
let receiver = &Foo;

receiver.something(); // 候補型 &Foo

候補型 &Foo

  • 候補 #1: Foo 型における固有メソッド Foo::something(&self) の実装
  • 候補 #2: &Foo 型における拡張メソッド Bar::something(self) の実装

固有メソッドが優先され、Foo::something(&self) に決定されます。

3. メソッド探索で考慮されない条件

メソッドは決定できても他の条件が考慮されないために意図したメソッドが呼べず、意図した動作をしなかったりエラーとなったりすることがあります。

3.1. Self 引数以外の引数の条件

Rust では関数の引数によるオーバーロードはできないため、メソッドでも同様に (Self 引数を除いて) メソッド探索時に引数を考慮しません。
そのため、メソッド決定後に引数が一致しない場合はエラーになります。

trait Bar {
    fn something(&self, _i: i32);
}

struct Foo;
impl Foo {
    fn something(&self) {
        println!("Foo");
    }
}

impl Bar for Foo {
    fn something(&self, _i: i32) {
        println!("Bar");
    }
}

//
let receiver = &Foo;

receiver.something(23); // エラー発生

候補型 &Foo

  • 候補 #1: Foo 型における固有メソッド Foo::something(&self) の実装
  • 候補 #2: Foo 型における拡張メソッド Bar::something(&self, _i: i32) の実装

メソッド探索で Self 引数以外の引数を考慮せず Foo::something(&self) に決定され、引数の値 23 を渡すことができずにエラーになります。

コンパイラの出力例
error[E0061]: this function takes 0 arguments but 1 argument was supplied
  --> src/main.rs:22:10
   |
22 | receiver.something(23); // エラー発生
   |          ^^^^^^^^^ -- supplied 1 argument
   |          |
   |          expected 0 arguments
   |
note: associated function defined here
  --> src/main.rs:8:8
   |
8  |     fn something(&self) {
   |        ^^^^^^^^^ -----

型やトレイトを明示 (「完全修飾構文」) することで特定のメソッドを呼ぶことができます。

※「完全修飾構文」では Self 引数を含む引数で自動参照外しや「Deref 型強制」、「Unsized 型強制」は行われます (※一部両立不可) が、自動参照はされません。

// ... 型の定義略

//
let receiver = &Foo;

<Foo as Bar>::something(receiver, 23); // トレイトを明示
// or
<Foo as Bar>::something(&receiver, 23); // トレイトを明示: 自動参照外し
// or
Bar::something(receiver, 23); // Self 引数に基づく静的ディスパッチ

// Note: Self 引数に基づく静的ディスパッチ時は自動参照外しされない
// Bar::something(&receiver, 23); // エラー発生

参考「Disambiguating Function Calls - Call expressions - The Rust Reference
参考「Qualified paths - Paths - The Rust Reference

参考「[Rust] 引数 self や戻り値の型 Self に基づくメソッドディスパッチ - Qiita

3.2. Self 引数の可変性

Self 引数の可変性について、可変参照 &mut T は考慮されますが、レシーバの型が mut T の場合の mut は考慮されません。
Rust の公式ドキュメントに「(メソッド探索で) レシーバの可変性は考慮されません」と書かれているのですが、紛らわしい言い方だと思います。

参考「Method call expressions - The Rust Reference

trait Bar {
    fn something(&self);
}

struct Foo;
impl Foo {
    fn something(&mut self) {
        println!("Foo");
    }
}

impl Bar for Foo {
    fn something(&self) {
        println!("Bar");
    }
}

//
let receiver = &mut Foo;

receiver.something(); // 候補型 &mut Foo, メソッド Foo::something(&mut self)

//
let mut receiver = Foo;

receiver.something(); // 候補型 &Foo, メソッド Bar::something(&self)

レシーバ自体が可変 mut の場合はそれが取り除かれ、ここでは Foo, &Foo, &mut Foo の順に探索され、可変参照を受け取る固有メソッド Foo::something(&mut self) があるにもかかわらず候補型を &Foo として拡張メソッド Bar::something(&self) に決定されます。

型やトレイトを明示 (「完全修飾構文」) かつ可変参照にすれば、可変参照 Self 引数 &mut self を持つメソッドを呼ぶことができます。

※「完全修飾構文」では Self 引数を含む引数で自動参照外しや「Deref 型強制」、「Unsized 型強制」は行われます (※一部両立不可) が、自動参照はされません。

// ... 前略

//
let mut receiver = Foo;

receiver.something(); // 候補型 &Foo, メソッド Bar::something(&self)

Foo::something(&mut receiver); // 型を明示: 候補型 &mut Foo, メソッド Foo::something(&mut self)

参考「Disambiguating Function Calls - Call expressions - The Rust Reference
参考「Qualified paths - Paths - The Rust Reference

3.3. その他

他にもライフタイムや unsafe もメソッド探索で考慮されないようです。

参考「Method call expressions - The Rust Reference

6
6
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
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?