Comments 12
"Гайд на полиморфизм" - это что-то новенькое. Ладно, "обзор на видео" - к этому все уже давно привыкли, хотя это тоже, безусловно, неправильно.
"Гайд по полиморфизму". Так и только так.
в Rust нет наследования.
зависит от того что вы им называете. реализация трейтов по сути - наследование интерфейса. Там даже виртуальные таблицы в определённых ситуациях появляются.
вместо этого они будут встраиваться напрямую в вызывающий код,
тут кстати не обязательно. может отработать девиртуализация и если возникает перегрузка фукции, то оно может быть девиртуализировано. Ну и продакшн бывает разный, поэтому есть вариант с оптимизацией по размеру, когда инлайнинг используется меньше.
зависит от того что вы им называете. реализация трейтов по сути - наследование интерфейса. Там даже виртуальные таблицы в определённых ситуациях появляются.
Согласен что это вопрос терминологии но все же в Rust нет того что привычно называют наследование в других языках а именно иерархической системы наследования типов.
Вот допустим есть у нас несколько трейтов и структура:
trait Identifiable {
fn id(&self) -> i32;
}
trait User: Identifiable {
fn name(&self) -> &str;
}
struct Player {
id: i32,
name: String,
}
Теперь мы хотим реализовать User для Player и подразумеваем что User наследует Identifiable и соответственно расширяется всеми его методами и должно быть что-то наподобии:
impl User for Player {
fn name(&self) -> &str {
&self.name
}
fn id(&self) -> i32 {
self.id
}
}
error[E0277]: the trait bound `Player: Identifiable` is not satisfied
--> src/main.rs:89:15
|
89 | impl User for Player {
| ^^^^^^ the trait `Identifiable` is not implemented for `Player`
|
help: this trait has no implementations, consider adding one
--> src/main.rs:76:1
|
76 | trait Identifiable {
| ^^^^^^^^^^^^^^^^^^
note: required by a bound in `User`
--> src/main.rs:80:13
|
80 | trait User: Identifiable {
| ^^^^^^^^^^^^ required by this bound in `User`
Нас наругали, потому что несмотря на то что вроде бы User должен расширяться от Identifiable, это не так, он перенял только ограничения а не его методы. Ну оке премудрости Rust:
impl User for Player {
fn name(&self) -> &str {
&self.name
}
}
impl Identifiable for Player {
fn id(&self) -> i32 {
self.id
}
}
Окей, все работает. Попробуем сделать текст на наследование типов. Опустим момент что структуры никак не могут наследоваться друг от друга, ну допустим у нас только наследование от трейтов работает. Если Player наследуется от User значит вместо любого User можно подставить Player. Проверяем:
fn main() {
let player = Player {
id: 1,
name: "Mike".to_string(),
};
let user: User = player;
}
error[E0782]: expected a type, found a trait
--> src/main.rs:107:15
|
107 | let user: User = player;
|
Нет нельзя. Но мало того такой код не работает, так еще и компилятор пишет прямо что: нужен тип а найден трейт. Т.е. с точки зрения Rust трейты это вообще не типы.
В сухом остатке имеем:
трейты не наследуются друг от друга а только аккумулируют ограничение
в между трейтом и структурой нет отношения наследования типов
трейт в принципе не является типом
Все встает на свои места если смотреть на это под другим углом. В той же Java или C# интерфейсы - это типы и даже более того классы. Между всеми классами есть отношение наследования благодаря которому наследник перенимает природу родителя как в части данных так и в части поведения. Наследник может быть использован вместо родителя.
Ничего подобного нет в Rust. Есть структуры и есть трейты выступающие контрактами - задача компилятора проверить что типы соответствуют контрактам.
Т.е. Rust решает похожие задачи похожими на вид инструментами но в действительности иным подходом. Можно конечно пытаться натягивать наследование на глобус но мне кажется, в практическом плане, проще смотреть через призму контрактов.
трейты не наследуются друг от друга а только аккумулируют ограничение
В этом суть наследования. Немного контринтуитивно, но верно. Только поэтому приведение к общему типу имеет смысл - мы снимаем ограничения.
в между трейтом и структурой нет отношения наследования типов
Не понятно, что это должно значить.
трейт в принципе не является типом
Но impl X им является, в чем проблема? Это не проблема раста, что в других языках impl для спокойствия и удобства пользователя неявный.
То, что есть в Java/C# - это не 10 заповедей, которые всем нужно соблюдать. Некоторые их даже не считают ООП языками, потому что они устроены не как Smalltalk.
Но impl X им является, в чем проблема?
В том что impl X
- это не тип а выражение вида какой-то тип реализующий ограничение X
. Это не имеет никакого отношения к языкам с наследованием типов где передача типа туда где нужен X
означала бы приведение дочернего типа к родительскому. Здесь никакого приведения к X
нет, потому что это даже не тип.
То, что есть в Java/C# - это не 10 заповедей, которые всем нужно соблюдать.
Могут ответить аналогичным образом - не нужно везде видеть ООП. Трейты в Rust это по сути классы типов вдохновленные Haskell, синтаксическая схожесть с интерфейсами не делает их таковыми.
В этом суть наследования. Немного контринтуитивно, но верно.
На эти вопросы давно даны ответы как со стороны сообщества:
Why do we need a separate impl for a supertrait
A trait that declares supertraits is merely a trait with constraints, not an extension of its supertraits’ interface.
Conceptually, by declaring
trait Tr2: Tr1
, you only say that if a typeT
implementsTr2
, it must implementTr1
as well, but traitsTr1
andTr2
are otherwise completely independent, and each creates a separate namespace for its associated items (methods, associated types, constants, etc.). Any code usingT
may import and make use ofTr1
, orTr2
, both, or neither; but if it uses only one of them, it should not even have to think about the other existing.More concretely, by making each trait a separate namespace, Rust avoids the well-known fragile base class problem. Each trait can define its own operations that are guaranteed not to clash with any other if it has not been specifically imported into the namespace.
Так и с официальной стороны:
If a language must have inheritance to be object oriented, then Rust is not such a language. ...
Rust doesn't have "inheritance", but you can define a trait as being a superset of another trait.
В официальных руководствах прописано - нет в Rust нет наследования. Не нужно пытаться его там видеть.
If a language must have inheritance to be object oriented, then Rust is not such a language.
Ну то есть по их описанию если нельзя наследовать данные, но можно поведение, то это либо не ООП, либо не наследование.
Rust avoids the well-known fragile base class problem.
собственно это вся причина, почему наследование поведения сделано таким образом. В контексте наследования данных у нас получается, что вместо виртуальных таблиц мы производим явную композицию с необходимыми данными. В языках типа Odin/Jai для этого есть сахар, а в раст его завозить не стали.
struct Base {/**/};
impl Foo for Base {
fn do_foo(&self) {/**/}
}
struct Derive {
vptr: Base // указатель на данные "родителя"
}
impl Foo for Derive { // вместо плюсового virtual do_foo() override
fn do_foo(&self) {/**/}
}
В общем большая декомпозиция этих сущностей раста - данных и поведения - на мой взгляд просто позволяет избежать проблем классического наследования, но по сути те же яйца, только сбоку.
синтаксическая схожесть с интерфейсами не делает их таковыми.
а в чем оно расходится? Duck typing подсказывает, что это и есть интерфейсы.
а в чем оно расходится? Duck typing подсказывает, что это и есть интерфейсы.
Трейты позволяют описывать ассоциированные типы. Пример - Iterator из стандартной библиотеки:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// другие методы
}
У трейтов могут быть методы с реализацией по умолчанию. Пример - трейт Iterator
выше: у него 76 методов, но для реализации достаточно лишь одного: next
. Все остальные реализованы в терминах next
.
Трейты позволяют ссылаться на тип, который его реализовывает, через Self
. Пример - PartialEq:
trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
fn eq(&self, other: &Rhs) -> bool;
}
В ООП-языках аналогичные интерфейсы или принимают верхний объект из иерархии типов (Java), или вынуждены принимать сравниваемый тип обобщённым параметром (C#) и заставляют повторять тип в описании ограничений.
Трейты могут быть реализованы не безусловно, а только при соблюдении каких-то условий. Например, трейт Serialize из популярной библиотеки serde. Он реализован для Vec только в том случае, если Serialize
реализован для его элементов:
impl<T> Serialize for Vec<T>
where
T: Serialize,
{
// ...
}
Автору типа не обязательно знать о трейте для его реализации. Пример выше: Vec
определён в стандартной библиотеке, но трейт Serialize
и его реализация для Vec
определены в serde.
Трейты могут быть реализованы для многих типов сразу. Пример: в стандартной библиотеке есть два трейта для конвертации между типами: From и Into. Реализация первого автоматически даёт реализацию второго:
impl<T, U> Into<U> for T
where
U: From<T>,
{
// ...
}
Как видите, отличий предостаточно.
В первой части был упомянут спорный
Algebraic data types(closed type)
Пунктуация сохранена
А тут его нет. Как же так?
Между тем, Rust тут умеет побольше чем другие языки.
То есть претензия так же самая: если вы пишете обзор / гайд то нужно описать полный обзор, а не какие-то куски
А тут его нет. Как же так?
Ну на самом деле есть, просто в контексте решения проблемы возвращаемого значения неопределенного размера. Решил что примеры уже были в первой статье, и не стал растягивать эту. Но если считаете что не хватает, не проблема, добавлю.
То есть претензия так же самая: если вы пишете обзор / гайд то нужно описать полный обзор, а не какие-то куски
В идеале да, но в реальности получаются большие статьи, которые читают не так чтобы много кто.
Гайд на полиморфизм. Rust