Рубрики
Без рубрики

Изменчивость в Rust и чем она отличается от объектно-ориентированных языков

Этот пост начинался как многословный ответ на вопрос stackoverflow. Этот вопрос заставил меня осознать… Помеченный как rust, java, изменчивость.

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