Этот пост начинался как многословный ответ на вопрос stackoverflow. Этот вопрос заставил меня понять, что существует довольно универсальный способ мышления о изменчивости, исходящий из популярных языков, таких как C # и Java, и что приход к rust с таким мышлением часто приводит к путанице и разочарованию.
Я буду сравнивать и противопоставлять решения общих проблем, связанных с изменчивостью, таким образом, чтобы (надеюсь) интуитивно выражать различия. Для примеров, не связанных с rust, я буду использовать Java, но эти примеры могут быть выражены аналогичным образом на других объектно-ориентированных языках.
Мы начнем с простой цели — определим тип Dog
с двумя полями:
- Имя, которое будет неизменяемым
- Возраст, который будет изменчивым
В первых нескольких примерах мы будем избегать распространенных идиом, таких как геттеры и сеттеры, чтобы все было просто.
Вот как мы определим наш тип в Java:
class Dog { public final String name; public int age; public Dog(String name) { this.name = name; this.age = 0; } }
Эквивалентный тип в rust будет выглядеть следующим образом:
struct Dog { pub name: &'static str, pub age: usize, } impl Dog { pub fn new(name: &'static str) -> Dog { return Dog { name, age: 0 }; } }
В примере rust вы можете заметить, что в нашем поле name
нет модификатора, который указывал бы на то, что мы каким-либо образом контролируем изменчивость. Это связано с тем, что rust не имеет понятия о изменчивости на уровне поля.
Rust придерживается принципиально иного подхода. Если мы хотим изменить какое-либо поле нашего типа, нам нужно объявить изменяемую переменную, указывающую на него. По умолчанию все переменные являются неизменяемыми, и вы должны добавить модификатор mut
, если вы собираетесь его изменить:
// Declare an immutable variable let harris = Dog::new("Harris"); harris.age += 1; // Compile error! // Declare a mutable variable let mut harris = Dog::new("Harris"); harris.age += 1; // No problem!
На первый взгляд может показаться, что Java обеспечивает более детальный контроль над изменчивостью, чем rust – если переменная изменчива, мы можем изменять все ее поля; если она неизменяема, мы не можем изменять ни одно из них.
Изменчивость и собственные параметры
Давайте обновим наши примеры, чтобы сделать их более идиоматичными. Мы сделаем наши поля закрытыми и предоставим методы для доступа к ним или их изменения.
Первый в Java:
class Dog { private final String name; private int age; public Dog(String name) { this.name = name; this.age = 0; } public String getName() { return this.name; } public int getAge() { return this.age; } public void incrementAge() { this.age += 1; } }
И наш пример ржавчины:
struct Dog { name: &'static str, age: usize } impl Dog { pub fn new(name: &'static str) -> Dog { return Dog { name, age: 0 }; } pub fn get_name(&self) -> &'static str { return self.name; } pub fn get_age(&self) -> usize { return self.age; } pub fn increment_age(&mut self) { self.age += 1 } }
В примере rust get_name
, get_age
и increment_age
все методы включают параметр self
. Как и переменные, параметры могут быть либо изменяемыми, либо неизменяемыми. self
отличается от других параметров тем, как он передается методу: если у нас есть переменная с именем dog
и мы вызываем dog.increment_age()
, переменная dog
неявно передается методу increment_age
в качестве параметра self
и может получить доступ к закрытым полям, объявленным в типе.
Это становится еще более интересным, когда мы рассматриваем, как это работает в сочетании с переменными. Если у нас есть неизменяемая переменная, и мы вызываем для нее метод, который принимает &mut self
, мы столкнемся с ошибкой компиляции, потому что метод требует изменяемого доступа к нашему значению!
// Declare an immutable dog variable let dog = Dog::new("Harris"); dog.increment_age(); // Compile error! // Declare a mutable dog variable let mut dog = Dog::new("Harris"); dog.increment_age(); // This works!
Итак, что произойдет, если мы объявим переменную как изменяемую и вызовем метод, который принимает неизменяемый self
параметр? В этом случае вызов действителен, но методу по-прежнему предоставляется только неизменяемый доступ к параметру self
.
Повторюсь: для вызова метода, который требует изменяемого доступа к self
, переменная должна быть объявлена как изменяемая, но методы, которые объявляют неизменяемый параметр self
, могут вызываться как через изменяемые, так и неизменяемые переменные.
Изменчивость кодирования в возвращаемых типах
Мы собираемся ввести новое поле, чтобы проиллюстрировать, как изменчивость работает с более сложными типами. Мы добавим Список
( Vec
в rust) к нашему примеру с именем друзья
:
class Dog { private final Listfriends = new ArrayList<>(); ... public List getFriends() { return this.friends; } public void addFriend(Dog friend) { this.friends.add(friend); } }
В примере java наше новое поле friends
объявлено final
, что означает, что мы никогда не сможем изменить то, на что указывает поле, однако , мы по-прежнему можем добавлять и удалять элементы из списка с помощью нашего метода получения, как показано ниже:
Dog harris = new Dog("Harris"); Dog buck = new Dog("Buck"); harris.getFriends().add(buck);
Так как же это будет работать в rust? Давайте обновим наш пример:
struct Dog { ... friends: Vec, } impl Dog { ... pub fn get_friends(&self) -> &Vec { return &self.friends } }
В нашем первом примере мы фактически кодируем изменяемость в возвращаемый тип нашего метода get_friends
! Возвращаемый тип &Vec
представляет собой неизменяемую ссылку, поскольку в нем отсутствует most
ключевое слово. Это была бы ошибка компиляции, если бы мы попытались добавить элемент в возвращаемый friends
список:
let mut harris = Dog::new("Harris"); let buck = Dog::new("Buck"); harris.get_friends().push(buck); // Compile error!
Давайте разберемся с этим:
- Мы объявили изменяемую переменную
harris
- Метод
get_friends
возвращает неизменяемую ссылку на нашедрузей
поле - Тип Vec имеет метод с именем
push
, который требует изменяемой ссылки наself
- Наш код не компилируется, потому что неизменяемая ссылка, возвращаемая
get_friends
нельзя использовать для вызова метода, который требует изменяемого доступа ксам
Итак, даже несмотря на то, что наша исходная переменная была объявлена как изменяемая, мы используем метод get_friends
, чтобы скрыть доступ к нашему списку за неизменяемой ссылкой. Следовательно, это ошибка компиляции, если мы попытаемся ее изменить.
В качестве упражнения предположим, что мы хотели, чтобы метод get_friends
предоставлял изменяемый доступ к возвращаемой ссылке friends
. Найдите минутку, чтобы подумать, что нам нужно было бы изменить.
Есть идея? Вот вам и ответ:
pub fn get_friends(&mut self) -> &mut Vec{ &mut self.friends }
Подводя итог, мы должны были:
- Объявляем наш параметр
self
изменяемым, - Объявите возвращаемый тип как изменяемый и
- Возвращает изменяемую ссылку на наш список
Теперь мы можем изменить список друзей
с помощью нашего метода get_friends
! Захотим ли мы когда-нибудь сделать это на практике – это другой вопрос.
Вывод
Изменчивость в rust сильно отличается от того, к чему привыкло большинство людей, и может потребоваться некоторое время, чтобы она усвоилась. Однако, как только это происходит, это становится невероятно мощным способом обеспечения гарантий, которые невозможны в большинстве языков. Это также фундаментальная часть языка, работающая совместно с другими языковыми функциями для достижения уникального сочетания безопасности и производительности.
Краткое замечание об идиомах rust
Примеры rust не особенно идиоматичны, но это было сделано ради ясности и простоты. Возможно, я коснусь интересных идиом rust в отдельном посте!
Оригинал: “https://dev.to/dubyabrian/mutability-in-rust-and-how-it-differs-from-object-oriented-languages-32e”