Этот пост начинался как многословный ответ на вопрос 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 List friends = 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”