Types, Traits, Generics
Типы и трейты
- Набор значений с заданной семантикой, компоновкой, ...
Эквивалентность типов и преобразования
- Это может быть не очевидно, но u8, &u8, &mut u8 полностью отличаются друг от друга.
- Любой t: T принимает только значения именно из T, например:
-- f(0_u8) не может быть вызван с помощью f(&0_u8).
-- f(&mut my_u8) не может быть вызван с помощью f(&my_u8).
-- f(0_u8) не может быть вызван с помощью f(0_i8).
0 != 0 работает (в математическом смысле), когда дело доходит до типов! В языковом смысле операция == (0u8, 0u16) просто не определена для предотвращения несчастных случаев.
Тем не менее, Rust иногда может помочь конвертировать между типами [1]:
-- приводит к ручному преобразованию значений типов `0_i8 as u8`.
-- автоматическое преобразование типов, если безопасно$^2$, пусть x: `&u8 = &mut 0_u8`.
[1] Преобразуют значения из одного типа (например, u8) в другой (например, u16), возможно добавляя для этого инструкции процессора и этим отличаются от подтипов, которые подразумевают, что тип и подтип являются частью одного и того же множества (например, u8 является подтипом u16, а 0_u8 является тем же, что и 0_u16), где такое преобразование будет чисто проверкой времени компиляции. Rust не использует подтипизацию для обычных типов (и 0_u8 отличается от 0_u16), вроде как для пожизненно.
[2] Безопасность здесь заключается не только в физическом понятии (например, &u8 не может быть преобразовано к &u128), «история показала, что такое преобразование приведет к ошибкам».
Реализации — impl S { }
- Типы обычно поставляются с собственными реализациями, например, impl Port {}, связавается с типом:
-- связанная функция `Port::new(80)`.
-- метод `port.close()`
То, что считается родственным, является более философским, чем техническим, ничто (кроме хорошего вкуса) не помешает сделать u8::play_sound().
Трейты — trait T { }
- Трейты ...
-- являются способом «абстрактного» поведения.
-- автор трейта заявляет семантически, что этот трейт означает X.
-- другие могут реализовать («подписаться на») поведение для своего типа.
- Подумайте о трейтах как «списке методов» для типов:
- Тот, кто входит в этот список, будет придерживаться поведения списка.
- Трейты могут также включать в себя связанные методы, функции, ...
- Трейты без методов часто называют маркерными трейтами.
- Copy - это пример трейта маркера, означающий, что память может быть скопирована побитово.
- Некоторые черты трейты вне явного контроля.
- Sized, предоставляемый компилятором для типов с известным размером, либо это есть, либо его нет.
Реализация трейтов для типов - impl T for S { }
- Трейты реализуются для типов.
- Реализация предполагает, что A для B добавляет тип B в список принадлежности трейта:
- Визуально вы можете подумать о типе, получающем «значок» за свое членство:
Трейты против интерфейсов
Интерфейсы
- В Java Алиса создает интерфейс Eat.
- Когда Иван создаёт Venison, он должен решить, реализует ли Venison Eat или нет.
- Другими словами, все члены должны быть исчерпывающе объявлены во время определения типа.
- При использовании Venison Лена может использовать поведение, предоставляемое Eat:
Трейты
- В Rust Алиса создаёт трейт «Eat».
- Иван создаёт тип Venison и решает не реализовывать Eat (он может даже не знать о Eat).
- Кто-то[*] позже решит добавить Eat в Venison, это было бы действительно хорошей идеей.
- При использовании Venison Лена должна импортировать Eat отдельно:
[*] Чтобы помешать двум лицам реализовать Eat по-разному Rust ограничивает этот выбор либо Алисой, либо Иваном, то есть impl Eat for Venison может произойти только в трейте Venison или в трейте Eat. Подробности см. в разделе Согласованность.
Дженерики
Type Constructors — Vec<>
- `Vec<u8>` - тип «вектор байтов», `Vec<char>` - тип «вектор символов», но что такое `Vec < >`?
- Vec<> не является типом, не занимает память, даже не может быть преобразовано в код.
- Vec<> - конструктор типов, шаблон или рецепт для создания типов.
-- позволяет третьей стороне строить конкретный тип через параметр.
-- только тогда этот `Vec<UserType>` сам станет реальным типом.
Параметры дженериков — <T>
- Параметр для Vec<> часто называется T, поэтому `Vec<T>`.
- T «имя переменной для типа» для подключения чего-либо определенного, `Vec<f32>`, `S<u8>`,...
Тип против конструкторов типов.
Константы дженерики — [T; N] и S<const N: usize>
- Некоторые конструкторы типов принимают не только определенный тип, но и определенную константу.
- [Т; n] конструирует тип массива, содержащий n раз T-тип.
- Для пользовательских типов, объявленных как MyArray<T, const N: usize>.
Конструкторы типов на основе константы.
Границы (простые) — where T: X
- Если T может быть любым типом, то мы можем по рассуждать (писать код) для `Num<T>`?
- Границы параметров:
-- ограничить допустимые типы (границы трейта) или значения (границы константы?)
-- теперь мы можем использовать эти ограничения!
Здесь мы добавляем границы к структуре. На практике вместо этого лучше добавлять границы к соответствующим блокам impl, см. далее этот раздел.
Границы (составные) — where T: X + Y
- Длинный трейт может выглядеть устрашающе.
- На практике каждое дополнение + X к ограничению просто сокращает пространство подходящих типов.
Реализация - impl<>
Когда мы пишем:
Когда мы пишем:
- вот рецепт реализации для любого типа T (`impl <T>`).
- этот тип должен быть членом признаков Absolute + Dim + Mul.
- вы можете добавить блок реализации в семейство типов S<>.
- содержащиеся методы ...
Можно считать, что такой код `impl<T>... {}` абстрактно реализует семейство моделей поведения. В частности, они позволяют третьим сторонам прозрачно материализовать реализации аналогично тому, как конструкторы типов материализуют типы:
Реализации общего покрытия - impl<T> X for T {...}
Можно также написать реализации, чтобы они применяли трейт ко многим типам:
Это называется общими реализациями.
Что бы ни было в верхнем списке, может быть добавлено к нижнему списку, основываясь на следующем рецепте (impl).
Трейты могут быть безупречным способом обеспечения функциональность чужих типов модульным способом, если они просто реализуют другой интерфейс.
Расширенные концепции
Параметры трейтов — Trait<In> { type Out; }
Обратите внимание, как некоторые трейты могут быть использованы несколько раз, а другие только один раз.
Это почему?
- Сами трейты могут быть общими для двух видов параметров:
-- `trait From<I> {}`
-- `trait Deref { type O; }`
- Помните, мы говорили, что трейты являются «списками членства» для типов и называются списком Self?
- Параметры I (для ввода) и O (для вывода)— это просто больше столбцов в списке этого трейта:
Теперь вот в чем дело:
- любые выходные параметры O должны быть однозначно определены входными параметрами I.
- (так же, как отношение X Y будет представлять функцию).
- Self в качестве входных данных.
Более сложный пример:
- создается связь типов с именем Complex.
- с 3 входными параметрами (Self всегда один) и 2 выходными, и он содержит (Self, I1, I2) => (O1, O2).
Различные реализации трейта. Последняя из них недопустима, (NiceMonster, u16, String) т.к. имеет одинаковую сигнатуру входных данных.
Рекомендации по разработке трейтов (аннотация)
- Выбор параметра (ввод и вывод) также определяет, кому может быть разрешено добавлять элементы:
-- Параметры I позволяют пересылать реализации пользователю (Леной).
-- Параметры O должны определяться программистом трейта (Алисой или Иваном).
Лена может добавить больше членов, предоставив свой собственный тип для T.
Для заданного набора входных данных (здесь Self) программист обязан предварительно выбрать O.
Рекомендации по разработке трейтов (пример)
Выбор параметров сопровождается заполнением трейта назначения.
Нет дополнительных параметров
Автор трейта предполагает:
- ни реализатор, ни пользователь не должны настраивать API.
Входные параметры
Автор трейта предполагает:
- реализатор настраивает API для типа Self (но только одним способом).
- пользователям не нужно или не должно быть возможность влиять на настройку для конкретного Self.
Как вы можете видеть здесь, термин вход или выход не имеет (обязательно) никакого отношения к тому, являются ли I или O входами или выходами для фактической функции!
Несколько входных и выходных параметров
Как и примеры выше, автор трейта предполагает:
- пользователи могут захотеть иметь возможность решать, для каких типов I должны быть возможны.
- для входных данных, реализатор должен определить результирующий тип выходных данных.
Динамические типы / Типы нулевого размера
- Тип T имеет значение размера, если во время компиляции известно, сколько байт он занимает например u8, и &[u8], [u8] не имеют размера.
- Размером подразумевается `impl Sized for T {}`. Это происходит автоматически и не может быть вызвано пользователем.
- Типы без размера называются динамически размерными типами (DST), иногда неразмерными.
- Типы без данных называются типами нулевого размера (ZST), не занимают места.
?Sized
- T может быть любого типа.
- Тем не менее, существует невидимая привязка по умолчанию `T: Size`, поэтому `S<str>` невозможно из коробки.
- Вместо этого мы должны добавить T : ?Sized, чтобы отказаться от этой привязки:
Дженерики и время жизни — <'a>
Время жизни действует[*] как параметры типа:
-- пользователь должен указать определенный тип 'a (компилятор поможет в методах).
-- как `Vec<f32>` и `Vec<u8>` являются разными типами, так и `S<'p>` и `S<'q>` разные.
-- это означает, что вы не можете просто присвоить значение типа `S<'a>` переменной, ожидающей `S<'b>` (исключение: отношение «подтип» для жизненных периодов жизни, например, 'a переживет 'b).
- 'static — это только именуемый экземпляр времени жизни пространства типов.
[*] Есть тонкие различия, например, вы можете создать явный экземпляр 0 типа u32, но за исключением «статических, вы не можете действительно создать время жизни», компилятор сделает это за вас.
Примечание для себя и TODO: эта аналогия кажется несколько ошибочной, как будто `S<'a>` относится к `S<'static>` как `S<T>` к `S<u32>`, статический будет типом, но тогда каково значение этого типа?
Внешние типы и трейты
Визуальный обзор типов и трейтов в вашем крейте.
Примеры трейтов и типов, и какие трейты можно реализовать для какого типа.
Преобразования типов
Как получить B, когда у вас есть A?
Введение
[1] В то время как оба преобразуют `A` в `B`, принуждение, обычно ссылается на несвязанный `B` (тип «можно разумно ожидать, что у него разные методы»), в то время как подтипизация ссылок на `B` отличается только временем жизни.
Вычисления (трейты)
Сахар, чтобы получить B от A. Некоторые трейты обеспечивают канонические, вычислимые пользователем отношения типов:
Приведение типа
Преобразование типов с ключевым словом, как будто преобразование относительно очевидно, но может вызвать проблемы.
Где Ptr, Integer, Number просто используются для краткости и фактически означают:
- Ptr любой `*const T` или `*mut T`.
- Целое число любого типа u8 ... i128.
- Вещественное число (f32, f64).
Мнение — Приведение, особенно Число — Число, может легко пойти не так. Если вы обеспокоены правильностью, рассмотрите более явные методы.
Принуждение
Автоматическое ослабление типа от А до В; Типы могут быть существенно разными. [1]
[1] По существу, можно ожидать, что результат принуждения `В` будет совершенно другого типа (т.е. иметь совершенно другие методы), чем исходный тип `А`.
[2] Не вполне работает в примере выше, так как неразмерные не могут быть в стеке; представьте себе `f(x: &A) - > &B` вместо этого. Отмена размера работает по умолчанию для:
- `[T; n]` для `[T]`.
- `T` для `dyn Trait` если `impl Trait for T {}`.
- `Foo<…, T, …>` для `Foo<…, U, …>` при загадочных обстоятельствах.
Подтипизация
Автоматически преобразует A в B для типов, отличающихся только временем жизни - примеры подтипизации:
🛑 это не примеры подтипизации:
Различные
Автоматически преобразует A в B для типов, отличающихся только временем жизни - правила дисперсии подтипов:
- Более длинная продолжительность жизни 'a, которая переживает более короткий 'b, является подтипом 'b.
- Подразумевает, что `'static` является подтипом всех других жизней `'a`.
- Независимо от того, являются ли типы с параметрами (например, &'a T) подтипами друг друга, используется следующая таблица дисперсии:
Ковариант означает, что если `A` является подтипом `B`, то `T[A]` является подтипом `T[B]`.
Контравариант означает, что если `A` является подтипом `B`, то `T[B]` является подтипом `T[A]`.
Инвариант означает, что даже если `A` является подтипом `B`, ни `T[A]`, ни `T[B]` не будут подтипом другого.
Соединения, такие как структура `S<T> {}`, получают дисперсию через используемые ими поля, обычно становясь инвариантными, если смешиваются множественные дисперсии.
Другими словами, «регулярные» типы никогда не являются подтипами друг друга (например, u8 не является подтипом u16), и `Box<u32>` никогда не будет под- или супертипом чего-либо. Однако, как правило `Box<A>`, может быть подтипом `Box<B>` (через ковариацию), если `A` является подтипом `B`, что может произойти только в том случае, если `A` и `B` являются «своего рода одним и тем же типом, который различался только по времени жизни», например, `A` существует в `&'static u32` и `B` - `&'a u32`.
Статья на list-site.