Каковы точные правила автоматического разыменования (auto-dereferencing) в Rust?
Основные правила автоматического разыменования в Rust
Автоматическое разыменование (auto-dereferencing) в Rust — это механизм, который применяется компилятором только при вызове методов и при работе с операторами (например, индексация []). Оно не работает для обычных операций присваивания или передачи аргументов в функции — там нужно явное разыменование через *.
Правило №1: Вызов методов (Method Dispatch)
Когда вы вызываете метод на ссылке, Rust автоматически применяет столько разыменований, сколько нужно, чтобы найти подходящую реализацию метода. Это работает благодаря трейту Deref.
fn main() {
let s = String::from("hello");
let r = &s; // r: &String
// Компилятор видит: r — это &String
// Он ищет метод len() для &String — не находит
// Затем разыменовывает: *r — это String
// Находит len() для String — вызывает его
println!("{}", r.len()); // Работает!
// Можно ещё глубже:
let rr = &&s; // rr: &&String
println!("{}", rr.len()); // Тоже работает — два разыменования
}
Правило №2: Оператор индексации []
Оператор [] — это синтаксический сахар для вызова метода index() из трейта Index. Поэтому к нему применяются те же правила автоматического разыменования.
fn main() {
let v = vec![1, 2, 3];
let v_ref = &v; // v_ref: &Vec<i32>
// Компилятор ищет impl Index для &Vec<i32>
// Не находит — разыменовывает до Vec<i32>
// Находит impl Index для Vec<i32>
println!("{}", v_ref[0]); // Работает!
}
Правило №3: Сравнение и арифметические операторы
Для операторов сравнения (==, !=, < и т.д.) и арифметических операторов (+, - и т.д.) автоматическое разыменование не применяется. Нужно явно использовать *.
fn main() {
let a = 5;
let b = 10;
let ra = &a;
let rb = &b;
// Ошибка! cannot compare &{integer} with &{integer}
// if ra == rb { }
// Правильно — явное разыменование:
if *ra == *rb {
println!("Equal");
}
}
Правило №4: Присваивание и передача в функции
Когда вы присваиваете значение или передаёте аргумент в функцию, автоматическое разыменование не работает. Компилятор требует точного соответствия типов.
fn takes_i32(x: i32) {
println!("{}", x);
}
fn main() {
let x = 5;
let y = &x;
// Ошибка! expected i32, found &i32
// takes_i32(y);
// Правильно:
takes_i32(*y);
}
Дополнительное правило: Цепочки разыменований
Компилятор применяет разыменование до тех пор, пока не найдёт подходящий метод или не исчерпает все возможные уровни. Максимальное количество автоматических разыменований — 10 (это ограничение безопасности).
fn main() {
let s = String::from("hello");
let r1 = &s; // &String
let r2 = &r1; // &&String
let r3 = &r2; // &&&String
// Работает — 3 автоматических разыменования
println!("{}", r3.len());
}
Краткая памятка:
- Вызов методов — авторазыменование работает
- Оператор индексации
[]— работает (как метод) - Арифметика и сравнение — не работает, нужно явное
* - Передача в функции и присваивание — не работает
- Максимум 10 уровней автоматического разыменования
Как это работает под капотом: трейт Deref
В основе автоматического разыменования лежит трейт Deref. Когда вы пишете r.method(), компилятор:
- Проверяет, есть ли метод
methodдля типаT(гдеr: &T). - Если нет — применяет
Deref::deref(&r), получая&U(гдеU— целевой тип). - Повторяет шаги 1-2, пока не найдёт метод или не превысит лимит.
Важно понимать, что Deref возвращает ссылку на целевой тип, а не само значение. Поэтому в случае с r.len() компилятор фактически вызывает (*r).len() или (**r).len() и т.д.
Вы можете реализовать Deref для своих типов, чтобы включить автоматическое разыменование:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let m = MyBox(String::from("hello"));
// Автоматическое разыменование сработает
println!("{}", m.len());
}
Этот механизм также используется для умных указателей, таких как Box<T>, Rc<T> и Arc<T>, позволяя им вести себя как обычные ссылки при вызове методов.
Ответов пока нет.
Войдите чтобы оставить ответ