Avatar ·

Каковы точные правила автоматического разыменования (auto-dereferencing) в Rust?

Всем привет! Я начал изучать Rust недавно и постоянно путаюсь в том, когда и как компилятор автоматически разыменовывает ссылки. Например, я вижу, что можно вызвать метод `len()` на `&String`, хотя `len()` определён для `String`. Или можно передать `&i32` в функцию, которая ожидает `i32`, и Rust сам разыменует его? Вот примеры, которые меня сбивают с толку: ```rust fn main() { let s = String::from("hello"); let r = &s; println!("{}", r.len()); // Работает! Но r — это &String, а не String let x = 5; let y = &x; let z = *y; // Тут я понимаю — нужно явно разыменовать println!("{}", z); // А вот тут — почему работает? let v = vec![1, 2, 3]; let v_ref = &v; println!("{}", v_ref[0]); // v_ref — это &Vec, но мы используем индексацию } ``` Я читал про трейты `Deref` и `DerefMut`, но не до конца понимаю, как именно компилятор решает, когда применять автоматическое разыменование. Есть ли чёткий набор правил? И почему в одних случаях нужно писать `*y`, а в других — нет? Хотелось бы получить подробное объяснение с примерами, чтобы наконец разобраться. Заранее спасибо! <<>>

Основные правила автоматического разыменования в 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(), компилятор:

  1. Проверяет, есть ли метод method для типа T (где r: &T).
  2. Если нет — применяет Deref::deref(&r), получая &U (где U — целевой тип).
  3. Повторяет шаги 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>, позволяя им вести себя как обычные ссылки при вызове методов.