Особенности объектно-ориентированных языков программирования

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Особенности объектно-ориентированных языков программирования. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе мы исследуем особенности объектно-ориентированных языков, включая объекты, инкапсуляцию и наследование, и проверяем, поддерживает ли Rust эти функции.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/floating_types("Floating-point Types") rust/DataTypesGroup -.-> rust/type_casting("Type Conversion and Casting") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/integer_types -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/floating_types -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/type_casting -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/function_syntax -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/expressions_statements -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/method_syntax -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/traits -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} rust/operator_overloading -.-> lab-100441{{"Особенности объектно-ориентированных языков программирования"}} end

Особенности объектно-ориентированных языков программирования

В сообществе программистов не существует единого мнения о том, какие свойства должен иметь язык, чтобы считаться объектно-ориентированным. Rust受到许多编程范式的影响,包括面向对象编程(OOP);例如,我们在第13章中探讨了来自函数式编程的特性。可以说,面向对象编程语言具有某些共同特征,即对象、封装和继承。让我们看看这些特征各自的含义以及Rust是否支持它们。

Объекты содержат данные и поведение

Книга Design Patterns: Elements of Reusable Object-Oriented Software Эрика Гаммы, Ричарда Гельма, Раффаэль Джонсона и Джон Влиссайда (Addison-Wesley, 1994), более простым языком называемая The Gang of Four («Четвёрка»), представляет собой каталог паттернов объектно-ориентированного проектирования. Она определяет ООП следующим образом:

Объектно-ориентированные программы состоят из объектов. Объект объединяет как данные, так и процедуры, которые работают с этими данными. Процедуры обычно называются методами или операциями.

Согласно этому определению, Rust является объектно-ориентированным языком: структуры и перечисления содержат данные, а блоки impl предоставляют методы для структур и перечислений. Даже если структуры и перечисления с методами не называются объектами, они обеспечивают ту же функциональность, что и объекты по определению Четвёрки.

Инкапсуляция, скрывающая детали реализации

Ещё одним аспектом, обычно связанным с ООП, является понятие инкапсуляции, которое означает, что детали реализации объекта недоступны для кода, использующего этот объект. Поэтому единственным способом взаимодействия с объектом является через его публичный API; код, использующий объект, не должен иметь доступа к внутренним частям объекта и напрямую изменять данные или поведение. Это позволяет программисту изменять и рефакторить внутренние части объекта, не меняя при этом код, использующий объект.

Мы обсуждали, как контролировать инкапсуляцию в главе 7: мы можем использовать ключевое слово pub, чтобы определить, какие модули, типы, функции и методы в нашем коде должны быть публичными, и по умолчанию все остальное является приватным. Например, мы можем определить структуру AveragedCollection, которая имеет поле, содержащее вектор значений типа i32. Структура также может иметь поле, содержащее среднее значение элементов в векторе, что означает, что среднее значение не нужно вычислять при каждом запросе. Другими словами, AveragedCollection будет кешировать вычисленное среднее значение для нас. В листинге 17-1 приведено определение структуры AveragedCollection.

Filename: src/lib.rs

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Листинг 17-1: Структура AveragedCollection, которая хранит список целых чисел и среднее значение элементов в коллекции

Структура помечена pub, чтобы другой код мог использовать её, но поля внутри структуры остаются приватными. Это важно в этом случае, потому что мы хотим гарантировать, что при добавлении или удалении значения из списка среднее значение также обновляется. Мы это делаем, реализуя методы add, remove и average на структуре, как показано в листинге 17-2.

Filename: src/lib.rs

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

Листинг 17-2: Реализации публичных методов add, remove и average на AveragedCollection

Публичные методы add, remove и average — единственный способ доступа к или модификации данных в экземпляре AveragedCollection. Когда элемент добавляется в list с использованием метода add или удаляется с использованием метода remove, реализации каждого вызывают приватный метод update_average, который также обрабатывает обновление поля average.

Мы оставляем поля list и average приватными, чтобы внешний код не имел возможности напрямую добавлять или удалять элементы из поля list; в противном случае поле average может быть устаревшим при изменении list. Метод average возвращает значение из поля average, позволяя внешнему коду читать average, но не модифицировать его.

Поскольку мы инкапсулировали детали реализации структуры AveragedCollection, мы можем легко изменить аспекты, такие как структура данных, в будущем. Например, мы могли бы использовать HashSet<i32> вместо Vec<i32> для поля list. Пока сигнатуры публичных методов add, remove и average остаются такими же, код, использующий AveragedCollection, не потребуется изменять. Если мы сделаем list публичным, это не обязательно будет так: HashSet<i32> и Vec<i32> имеют разные методы для добавления и удаления элементов, поэтому внешний код, вероятно, должен был бы измениться, если он напрямую модифицировал list.

Если инкапсуляция является обязательным аспектом для того, чтобы язык считался объектно-ориентированным, то Rust соответствует этому требованию. Возможность использовать pub или нет для разных частей кода позволяет инкапсулировать детали реализации.

Наследование как система типов и как совместное использование кода

Наследование — это механизм, при котором объект может наследовать элементы из определения другого объекта, тем самым получая данные и поведение родительского объекта, не нужно ли их определять заново.

Если для того, чтобы язык считался объектно-ориентированным, должен быть механизм наследования, то Rust не является таким языком. Нет способа определить структуру, которая бы наследовала поля и реализации методов родительской структуры, не используя макрос.

Однако, если вы привыкли использовать наследование в своей программировочной.toolbox, в Rust есть другие решения, в зависимости от того, зачем вы использовали наследование в первую очередь.

Вы выбираете наследование в основном по двум причинам. Одна из них — повторное использование кода: вы можете реализовать определённое поведение для одного типа, а наследование позволяет вам повторно использовать эту реализацию для другого типа. Вы можете сделать это в ограниченном виде в коде Rust с использованием реализаций методов по умолчанию для трейтов, которые вы видели в листинге 10-14, когда мы добавили реализацию метода summarize по умолчанию для трейта Summary. Любой тип, реализующий трейт Summary, будет иметь метод summarize без дополнительного кода. Это похоже на то, когда родительский класс имеет реализацию метода, а наследующий дочерний класс также имеет реализацию этого метода. Мы также можем переопределить реализацию метода summarize по умолчанию, когда реализуем трейт Summary, что аналогично тому, как дочерний класс переопределяет реализацию метода, унаследованного от родительского класса.

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

Полиморфизм

Для многих людей полиморфизм является синонимом наследования. На самом деле это более общий概念, который относится к коду, который может работать с данными разных типов. Для наследования эти типы обычно являются подклассами.

Вместо этого Rust использует обобщения для абстрагирования над разными возможными типами и ограничения трейтов для наложения ограничений на то, что эти типы должны предоставлять. Это иногда называется ограниченным параметрическим полиморфизмом.

Наследование в последнее время потеряло популярность в качестве решения для проектирования программного обеспечения в многих языках программирования, потому что часто рискует поделиться большим количеством кода, чем необходимо. Подклассы не всегда должны делиться всеми характеристиками своего родительского класса, но это происходит при наследовании. Это может сделать дизайн программы менее гибким. Кроме того, это открывает возможность вызвать методы у подклассов, которые не имеют смысла или которые вызывают ошибки, потому что методы не применимы к подклассу. Кроме того, некоторые языки позволяют только одиночное наследование (то есть подкласс может наследоваться только от одного класса), что ещё более ограничивает гибкость в дизайне программы.

По этим причинам Rust использует другой подход — использование объектов трейтов вместо наследования. Посмотрим, как объекты трейтов обеспечивают полиморфизм в Rust.

Резюме

Поздравляем! Вы завершили лабораторную работу по Особенностям объектно-ориентированных языков программирования. Вы можете практиковаться в более многих лабораторных работах в LabEx, чтобы улучшить свои навыки.