% Преобразования при разыменовании (deref coercions)
Стандартная библиотека Rust реализует особый типаж, 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
.