Avatar ·

В Rust: в чём разница между self и Self (заглавная и строчная буквы)?

Всем привет! Я начал изучать Rust и наткнулся на странную вещь: в коде часто встречаются `self` и `Self`, но они используются по-разному. Я запутался. Вот пример из книги по Rust, который я пытаюсь понять: ```rust struct Point { x: f64, y: f64, } impl Point { fn new(x: f64, y: f64) -> Self { Self { x, y } } fn distance(&self, other: &Self) -> f64 { let dx = self.x - other.x; let dy = self.y - other.y; (dx * dx + dy * dy).sqrt() } } ``` Я вижу, что в `fn new` возвращается `Self`, а в `fn distance` параметр `other` имеет тип `&Self`. При этом внутри методов я обращаюсь к полям через `self.x`. Но ведь `self` и `Self` — это одно и то же? Почему они пишутся по-разному? Мой вопрос: в чём принципиальная разница между `self` (с маленькой буквы) и `Self` (с большой буквы)? Когда нужно использовать одно, а когда другое? Можно ли вместо `Self` написать название структуры напрямую, например `Point`? И почему внутри `fn new` мы не можем написать `self.x`? Объясните, пожалуйста, на простых примерах. Я только начинаю разбираться с Rust, и мне важно понять эту базу, чтобы не путаться в дальнейшем. <<>>

Ключевое отличие: тип vs значение

В Rust Self (с заглавной S) и self (со строчной s) — это два совершенно разных понятия, хотя они и связаны с текущим типом.

  • Self — это псевдоним типа. Он всегда ссылается на тип, для которого реализуется текущий блок impl. Это аналог this в других языках, но только для типа, а не для экземпляра.
  • self — это значение-экземпляр. Это первый аргумент метода, который представляет собой сам объект, на котором вызывается метод. По сути, это self: Self — сокращённая запись для параметра.

Пример для наглядности

Вернёмся к вашему коду. Давайте перепишем его с явными типами, чтобы стало понятнее:

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Здесь Self — это Point
    fn new(x: f64, y: f64) -> Self {  // Self = Point
        Self { x, y }  // Создаём экземпляр Point
    }

    // Здесь:
    //   self — это переменная типа Point (точнее &Point)
    //   other — это другой экземпляр Point
    //   Self — это тоже Point
    fn distance(&self, other: &Self) -> f64 {
        // self.x — обращаемся к полю x текущего экземпляра
        // other.x — обращаемся к полю x другого экземпляра
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}

Теперь разберём каждый случай:

1. Self — псевдоним типа

Self можно использовать везде, где ожидается имя типа: в возвращаемом значении, в параметрах, в объявлениях переменных. Это особенно удобно, когда:

  • Вы работаете с обобщёнными типами (generics) — Self автоматически подставит конкретный тип.
  • Вы переименовываете структуру — не придётся менять все вхождения в impl-блоке.
  • Вы пишете трейты — там Self обязателен, так как реализующий тип неизвестен заранее.
// Пример с трейтом
trait Area {
    fn area(&self) -> f64;
}

impl Area for Point {
    fn area(&self) -> f64 {
        0.0  // Точка не имеет площади
    }
}

Внутри impl Area for Point слово Self будет означать Point.

2. self — аргумент-экземпляр

self — это сокращённая форма записи первого параметра метода. Полная форма выглядит так:

fn distance(self: &Self, other: &Self) -> f64 { ... }

Rust позволяет писать просто &self вместо self: &Self. Есть три варианта:

  • self — забирает владение (редко, только если нужно уничтожить объект).
  • &self — неизменяемая ссылка (самый частый случай).
  • &mut self — изменяемая ссылка.

3. Почему в new нет self?

Функция new — это ассоциированная функция (associated function), а не метод. Она не принимает self, потому что вызывается на типе, а не на экземпляре:

let p = Point::new(3.0, 4.0);  // Вызов на типе, не на объекте

Поэтому внутри new нет доступа к self.x — просто потому, что self (экземпляра) ещё не существует. Self же есть — это тип Point.

Короткое правило для запоминания

  • Self — это имя типа (как Point). Используйте его везде, где нужно указать тип.
  • self — это имя переменной (как this в других языках). Используйте его как первый аргумент метода.

И да, вместо Self можно написать Point — это будет работать. Но использование Self делает код более гибким и читаемым, особенно при работе с дженериками и трейтами.

<<>>

Практический совет: как не путаться

Когда я только начинал писать на Rust, я использовал такой ментальный трюк: заменял Self на слово "Тип", а self на "Я". Это помогает сразу понять контекст.

impl Point {
    // "Тип" new возвращает новый экземпляр "Типа"
    fn new(x: f64, y: f64) -> Self { ... }

    // "Я" (self) сравниваюсь с другим "Типом"
    fn distance(&self, other: &Self) -> f64 { ... }
}

Ещё один пример для закрепления

Рассмотрим ситуацию, когда у нас есть перечисление (enum):

enum Color {
    Red,
    Green,
    Blue,
}

impl Color {
    // Self здесь — это Color
    fn random() -> Self {
        // Генерация случайного цвета
        todo!()
    }

    // self — это конкретный экземпляр Color
    fn is_primary(&self) -> bool {
        matches!(self, Color::Red | Color::Green | Color::Blue)
    }
}

Как видите, Self работает с любыми типами: структурами, перечислениями, даже с примитивами (хотя для них редко пишут impl).

Важно: Self всегда пишется с заглавной буквы, а self — со строчной. Это строгое соглашение в Rust, и компилятор выдаст ошибку, если вы перепутаете регистр.

Со временем вы привыкнете, и различие станет интуитивно понятным. Главное — запомнить: Self = тип, self = экземпляр.