Преобразования при разыменовании (deref coercions)

Стандартная библиотека Rust реализует особый типаж, [Deref]deref. Обычно его используют, чтобы перегрузить *, операцию разыменования:

use std::ops::Deref;

struct DerefExample<T> {
    value: T,
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.value
    }
}

fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

Это полезно при написании своих указательных типов. Однако, в языке есть возможность, связанная с Deref: преобразования при разыменовании. Вот правило: если есть тип U, и он реализует Deref<Target=T>, значения &U будут автоматически преобразованы в &T, когда это необходимо. Вот пример:

fn foo(s: &str) {
    // позаимствуем строку на секунду
}

// String реализует Deref<Target=str>
let owned = "Hello".to_string();

// Поэтому, такой код работает:
foo(&owned);

Амперсанд перед значением означает, что мы берём ссылку на него. Поэтому owned

  • это String, а &owned — &String. Поскольку у нас есть реализация типажа impl Deref<Target=str> for String, &String разыменуется в &str, что устраивает foo().

Вот и всё. Это правило — одно из немногих мест в Rust, где типы преобразуются автоматически. Оно позволяет писать гораздо более гибкий код. Например, тип Rc<T> реализует Deref<Target=T>, поэтому такой код работает:

use std::rc::Rc;

fn foo(s: &str) {
    // позаимствуем строку на секунду
}

// String реализует Deref<Target=str>
let owned = "Hello".to_string();
let counted = Rc::new(owned);

// Поэтому, такой код работает:
foo(&counted);

Мы всего лишь обернули наш String в Rc<T>. Но теперь мы можем передать Rc<String> везде, куда мы могли передать String. Сигнатура foo не поменялась, и работает как с одним, так и с другим типом. Этот пример делает два преобразования: сначала Rc<String преобразуется в String, а потом String в &str. Rust сделает столько преобразований, сколько возможно, пока типы не совпадут.

Другая известная реализация, предоставляемая стандартной библиотекой, это impl Deref<Target=[T]> for Vec<T>:

fn foo(s: &[i32]) {
    // позаимствуем срез на секунду
}

// Vec<T> реализует Deref<Target=[T]>
let owned = vec![1, 2, 3];

foo(&owned);

Вектора могут разыменовываться в срезы.

Разыменование и вызов методов

Deref также будет работать при вызове метода. Другими словами, возможен такой код:

struct Foo;

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

let f = Foo;

f.foo();

Несмотря на то, что f — это не ссылка, а foo принимает &self, это будет работать. Более того, все примеры ниже делают одно и то же:

f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();

Методы Foo можно вызывать и на значении типа &&&&&&&&&&&&&&&&Foo, потому что компилятор сделает столько разыменований, сколько нужно для совпадения типов. А разыменование использует Deref.