メソッド呼出では、ドット演算子の左辺が self に適合するよう型変換される。

仕様

左辺が self の型に適合するまで、以下の型変換ルールを再帰的に適用する。

  1. 元の型をそのまま試す。

  2. 元の型を不変参照した型を試す (自動参照)。

  3. 元の型を可変参照した型を試す (自動参照)。

  4. 元の型が Deref を実装するなら、参照を外した型を試す (自動逆参照)。

  5. 元の型が固定長配列なら、その全体のスライス型を試す。

注意点

仕様を知らないと、予想外の型で処理が進んでしまう。これにより、コンパイルエラーのメッセージが意味不明に思えたり、より悪くコンパイルエラーにならない場合だと、意図したのと別の型で処理が実行される、等の事態が起きてしまう。

対策

関数呼出の記法は、ドット演算子の記法より型変換が弱い。そのため、上記の仕様が関係していると疑われる場合、記法を変えると問題の理解に役立つ可能性がある。

例えば、x.method() の場合、<SomeStruct>::method(x)<SomeStruct>::method(&x) 等を試すとよい (参考: トレイトが絡む場合は <&SomeStruct as SomeTrait>::method(&x) のような記法も使える)。

事例 1.

コンパイルエラーが意味不明

以下のコードでは、前述のどのルールでもエラーになるが、エラーメッセージについては二つ目のルールを基準に出力されている。そのため、他のルールを想定して読んでいると、エラーメッセージの意味が通らなくなる。

  1. 一つ目のルールの適用は失敗する。

    • <[i32] as SomeTrait>::method(arr) が試される。

    • つまり、T[i32] になる。

    • しかし、TSized に違反する。

    ※ 型パラメタは暗黙で Sized 境界を持つ。

  2. 二つ目のルールの適用も失敗する。

    • <&[i32] as SomeTrait>::method(&arr) が試される。

    • つまり、T&[i32] になる。

    • ここで、TSized に適合する。

    • しかし、T'static に違反する。

    Any'static ライフタイム境界を持つ。


use std::any::Any;

fn main() {
    test(&[1, 2, 3]);
}

fn test(arr: &[i32]) {
    arr.method();
}

trait SomeTrait {
    fn method(&self) {}
}

impl<T: Any> SomeTrait for T {
    // NOP.
}

error[E0521]: borrowed data escapes outside of function
 --> src\main.rs:8:5
  |
7 | fn test(arr: &[i32]) {
  |         ---  - let's call the lifetime of this reference `'1`
  |         |
  |         `arr` is a reference that is only valid in the function body
8 |     arr.method();
  |     ^^^^^^^^^^^^
  |     |
  |     `arr` escapes the function body here
  |     argument requires that `'1` must outlive `'static`

事例 2.

意図しない型での処理の実行

以下のコードでは、いくつかの値と型をペアで出力しているが、文字列スライス型でのみ失敗している。これは筆者が誤って文字列スライスを Sized として処理、コンパイラがその不備を修正するため、型変換でスライス型をスライスの参照型にした結果である。

  1. 一つ目のルールの適用は失敗する。

    • <str as Report>::report("text") が試される。

    • つまり、Tstr になる。

    • しかし、TSized に違反する。

    ※ 型パラメタは暗黙で Sized 境界を持つ。

  2. 二つ目のルールの適用は成功する。

    • <&str as Report>::report(&"text") が試される。

    • つまり、T&str になる。

    • ここで、TSized に適合する。


use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::Display;
use std::sync::OnceLock;

fn main() {
    (&false as &bool).report();
    (&42 as &i32).report();
    ("text" as &str).report();
}

trait Report {
    fn report(&self);
}

impl<T: Any + Display> Report for T {
    fn report(&self) {
        let type_id = TypeId::of::<T>();
        let header = *header_map().get(&type_id).unwrap_or(&"?");
        println!("{}: {}", header, self);
    }
}

fn header_map() -> &'static HashMap<TypeId, &'static str> {
    static MAP: OnceLock<HashMap<TypeId, &'static str>> = OnceLock::new();
    MAP.get_or_init(|| {
        let mut result = HashMap::new();
        result.insert(TypeId::of::<bool>(), "b");
        result.insert(TypeId::of::<i32>(), "i");
        result.insert(TypeId::of::<str>(), "s");
        result
    })
}

b: false
i: 42  
?: text