Введение: Одна маленькая буква, большая разница
Привет, будущий раст-разработчик! Если ты уже начал изучать Rust, то наверняка заметил, что компилятор — строгий, но справедливый учитель. Он не прощает опечаток, а уж тем более путаницы в синтаксисе. Одна из самых частых ловушек, в которую попадают новички (и не только) — это путаница между self и Self. Казалось бы, разница всего в одной букве — заглавной или строчной. Но за этой буквой скрывается фундаментальное различие между экземпляром и типом.
На Stack Overflow этот вопрос набрал тысячи просмотров, и это не случайно. Сегодня мы раз и навсегда разберемся, в чем отличие self от Self в Rust. Мы не просто прочитаем скучное определение, а пройдем пошаговый туториал с живыми примерами, написав пару небольших программ. К концу статьи ты будешь не просто знать разницу, а чувствовать её на интуитивном уровне. Поехали!
1. self: «Я» — это про экземпляр
Начнем с self. В Rust, как и во многих других языках, self используется для обозначения текущего экземпляра структуры или перечисления. Это аналог this в C++, Java, JavaScript или Python. Когда ты пишешь метод, который вызывается на конкретном объекте, ты используешь self.
У self есть три основные формы в сигнатуре метода:
self— забирает владение экземпляром (consumes the instance). После вызова метода исходный объект становится недоступен.&self— неизменяемая ссылка на экземпляр (самый частый случай). Метод может читать данные, но не может их менять.&mut self— изменяемая ссылка на экземпляр. Метод может изменять поля структуры.
Давай напишем простой пример. Представь, что мы создаем структуру Студент.
struct Студент {
имя: String,
курс: u32,
}
impl Студент {
// Метод, который берет &self (неизменяемую ссылку)
fn представиться(&self) {
println!("Привет! Я {}, учусь на {} курсе.", self.имя, self.курс);
}
// Метод, который берет &mut self (изменяемую ссылку)
fn перейти_на_следующий_курс(&mut self) {
self.курс += 1;
println!("{} теперь на {} курсе!", self.имя, self.курс);
}
// Метод, который забирает self (владение)
fn закончить_обучение(self) {
println!("{} закончил обучение на {} курсе. Прощай!", self.имя, self.курс);
// self здесь уничтожается, память освобождается
}
}
fn main() {
let mut студент = Студент {
имя: String::from("Анна"),
курс: 2,
};
студент.представиться(); // &self
студент.перейти_на_следующий_курс(); // &mut self
студент.закончить_обучение(); // self (владение)
// После вызова закончить_обучение, переменная 'студент' больше не доступна!
// Раскомментируй следующую строку, чтобы увидеть ошибку компиляции:
// студент.представиться(); // ОШИБКА: borrow of moved value
}
Обрати внимание на последний комментарий. Когда мы передаем self (без ссылки), мы отдаем владение. Это мощный инструмент, который позволяет точно контролировать время жизни объекта. В других языках (например, Python) такого контроля нет — объект живет, пока на него есть ссылки. В Rust ты сам решаешь, когда объект будет «потреблен».
2. Self: «Я» — это про тип
А теперь самое интересное. Self (с большой буквы) — это псевдоним (алиас) для типа, для которого мы реализуем методы. Проще говоря, Self внутри блока impl заменяется на имя структуры или перечисления, с которым мы работаем.
Зачем это нужно? Во-первых, это удобно. Вместо того чтобы писать полное имя структуры, ты пишешь Self. Во-вторых, это критически важно для обобщенного программирования (generics) и трейтов (traits). Когда ты пишешь трейт, ты не знаешь, какой конкретный тип будет его реализовывать. Self позволяет сослаться на этот будущий тип.
Рассмотрим пример. Мы реализуем трейт НовыйГод для нашего студента.
trait НовыйГод {
// Ассоциированная функция, которая создает новый экземпляр.
// Обрати внимание: здесь нет self! Это не метод, а функция, связанная с типом.
fn новый_год() -> Self;
// Метод, который возвращает новый экземпляр после празднования.
fn отпраздновать(self) -> Self;
}
struct Студент {
имя: String,
курс: u32,
}
impl НовыйГод for Студент {
// Self здесь заменяется на Студент
fn новый_год() -> Self {
// Мы не можем использовать self, так как это статическая функция.
// Но мы можем создать новый экземпляр Студент
Self {
имя: String::from("Новый студент"),
курс: 1,
}
}
fn отпраздновать(self) -> Self {
// self — это экземпляр, который мы получили.
// Self — это тип Студент.
println!("{} празднует Новый год! Переход на курс {}", self.имя, self.курс + 1);
Self {
имя: self.имя,
курс: self.курс + 1,
}
}
}
fn main() {
let студент = Студент {
имя: String::from("Иван"),
курс: 3,
};
let новый_студент = Студент::новый_год(); // Вызов ассоциированной функции
println!("Новый студент: {}, курс {}", новый_студент.имя, новый_студент.курс);
let студент_после_праздника = студент.отпраздновать();
println!("После праздника: {}, курс {}", студент_после_праздника.имя, студент_после_праздника.курс);
}
Видишь, как элегантно это работает? Внутри impl НовыйГод for Студент мы пишем Self, и компилятор понимает, что речь идет о типе Студент. Если бы мы захотели реализовать этот же трейт для другой структуры, например Преподаватель, код внутри трейта остался бы тем же самым, но Self бы магическим образом превратился в Преподаватель. Это и есть сила абстракции.
3. Главное отличие: Экземпляр vs. Тип
Давай подведем итог самого важного отличия в виде простой таблицы в голове:
self(строчная) — это