Найти тему
Ржавый код

Работа с типами

Types, Traits, Generics

Типы и трейты

-2
  • Набор значений с заданной семантикой, компоновкой, ...
-3

Эквивалентность типов и преобразования

-4
  • Это может быть не очевидно, но 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) просто не определена для предотвращения несчастных случаев.

-5

Тем не менее, 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 { }

-6
-7
  • Типы обычно поставляются с собственными реализациями, например, impl Port {}, связавается с типом:

-- связанная функция `Port::new(80)`.
-- метод `port.close()`

То, что считается родственным, является более философским, чем техническим, ничто (кроме хорошего вкуса) не помешает сделать u8::play_sound().

Трейты — trait T { }

-8
  • Трейты ...

-- являются способом «абстрактного» поведения.
-- автор трейта заявляет семантически, что этот трейт означает X.
-- другие могут реализовать («подписаться на») поведение для своего типа.

  • Подумайте о трейтах как «списке методов» для типов:
Трейты как таблицы характеристик Self относится к типу.
Трейты как таблицы характеристик Self относится к типу.
  • Тот, кто входит в этот список, будет придерживаться поведения списка.
  • Трейты могут также включать в себя связанные методы, функции, ...
-10
-11
-12
  • Трейты без методов часто называют маркерными трейтами.
  • Copy - это пример трейта маркера, означающий, что память может быть скопирована побитово.
-13
  • Некоторые черты трейты вне явного контроля.
  • Sized, предоставляемый компилятором для типов с известным размером, либо это есть, либо его нет.

Реализация трейтов для типов - impl T for S { }

-14
  • Трейты реализуются для типов.
  • Реализация предполагает, что A для B добавляет тип B в список принадлежности трейта:
-15
  • Визуально вы можете подумать о типе, получающем «значок» за свое членство:
-16

Трейты против интерфейсов

-17

Интерфейсы

  • В Java Алиса создает интерфейс Eat.
  • Когда Иван создаёт Venison, он должен решить, реализует ли Venison Eat или нет.
  • Другими словами, все члены должны быть исчерпывающе объявлены во время определения типа.
  • При использовании Venison Лена может использовать поведение, предоставляемое Eat:
-18
-19

Трейты

  • В Rust Алиса создаёт трейт «Eat».
  • Иван создаёт тип Venison и решает не реализовывать Eat (он может даже не знать о Eat).
  • Кто-то[*] позже решит добавить Eat в Venison, это было бы действительно хорошей идеей.
  • При использовании Venison Лена должна импортировать Eat отдельно:
-20

[*] Чтобы помешать двум лицам реализовать Eat по-разному Rust ограничивает этот выбор либо Алисой, либо Иваном, то есть impl Eat for Venison может произойти только в трейте Venison или в трейте Eat. Подробности см. в разделе Согласованность.

Дженерики

Type Constructors — Vec<>

-21
  • `Vec<u8>` - тип «вектор байтов», `Vec<char>` - тип «вектор символов», но что такое `Vec < >`?
-22
  • Vec<> не является типом, не занимает память, даже не может быть преобразовано в код.
  • Vec<> - конструктор типов, шаблон или рецепт для создания типов.

-- позволяет третьей стороне строить конкретный тип через параметр.
-- только тогда этот `Vec<UserType>` сам станет реальным типом.

Параметры дженериков — <T>

-23
  • Параметр для Vec<> часто называется T, поэтому `Vec<T>`.
  • T «имя переменной для типа» для подключения чего-либо определенного, `Vec<f32>`, `S<u8>`,...
-24

Тип против конструкторов типов.

-25

Константы дженерики — [T; N] и S<const N: usize>

-26
  • Некоторые конструкторы типов принимают не только определенный тип, но и определенную константу.
  • [Т; n] конструирует тип массива, содержащий n раз T-тип.
  • Для пользовательских типов, объявленных как MyArray<T, const N: usize>.
-27

Конструкторы типов на основе константы.

-28

Границы (простые) — where T: X

-29
  • Если T может быть любым типом, то мы можем по рассуждать (писать код) для `Num<T>`?
  • Границы параметров:

-- ограничить допустимые типы (границы трейта) или значения (границы константы?)
-- теперь мы можем использовать эти ограничения!

-30
-31

Здесь мы добавляем границы к структуре. На практике вместо этого лучше добавлять границы к соответствующим блокам impl, см. далее этот раздел.

Границы (составные) — where T: X + Y

-32
-33
  • Длинный трейт может выглядеть устрашающе.
  • На практике каждое дополнение + X к ограничению просто сокращает пространство подходящих типов.

Реализация - impl<>

Когда мы пишем:

-34

Когда мы пишем:

  • вот рецепт реализации для любого типа T (`impl <T>`).
  • этот тип должен быть членом признаков Absolute + Dim + Mul.
  • вы можете добавить блок реализации в семейство типов S<>.
  • содержащиеся методы ...

Можно считать, что такой код `impl<T>... {}` абстрактно реализует семейство моделей поведения. В частности, они позволяют третьим сторонам прозрачно материализовать реализации аналогично тому, как конструкторы типов материализуют типы:

-35

Реализации общего покрытия - impl<T> X for T {...}

Можно также написать реализации, чтобы они применяли трейт ко многим типам:

-36

Это называется общими реализациями.

-37

Что бы ни было в верхнем списке, может быть добавлено к нижнему списку, основываясь на следующем рецепте (impl).

-38

Трейты могут быть безупречным способом обеспечения функциональность чужих типов модульным способом, если они просто реализуют другой интерфейс.

Расширенные концепции

Параметры трейтов — Trait<In> { type Out; }

Обратите внимание, как некоторые трейты могут быть использованы несколько раз, а другие только один раз.

-39

Это почему?

  • Сами трейты могут быть общими для двух видов параметров:

-- `trait From<I> {}`
-- `trait Deref { type O; }`

  • Помните, мы говорили, что трейты являются «списками членства» для типов и называются списком Self?
  • Параметры I (для ввода) и O (для вывода)— это просто больше столбцов в списке этого трейта:
-40
Входные и выходные параметры.
Входные и выходные параметры.

Теперь вот в чем дело:

  • любые выходные параметры O должны быть однозначно определены входными параметрами I.
  • (так же, как отношение X Y будет представлять функцию).
  • Self в качестве входных данных.

Более сложный пример:

-42
  • создается связь типов с именем Complex.
  • с 3 входными параметрами (Self всегда один) и 2 выходными, и он содержит (Self, I1, I2) => (O1, O2).
-43

Различные реализации трейта. Последняя из них недопустима, (NiceMonster, u16, String) т.к. имеет одинаковую сигнатуру входных данных.

Рекомендации по разработке трейтов (аннотация)

-44
  • Выбор параметра (ввод и вывод) также определяет, кому может быть разрешено добавлять элементы:

-- Параметры I позволяют пересылать реализации пользователю (Леной).
-- Параметры O должны определяться программистом трейта (Алисой или Иваном).

-45
-46

Лена может добавить больше членов, предоставив свой собственный тип для T.

-47

Для заданного набора входных данных (здесь Self) программист обязан предварительно выбрать O.

Рекомендации по разработке трейтов (пример)

-48

Выбор параметров сопровождается заполнением трейта назначения.

Нет дополнительных параметров

-49
-50

Автор трейта предполагает:

  • ни реализатор, ни пользователь не должны настраивать API.

Входные параметры

-51
-52

Автор трейта предполагает:

  • реализатор настраивает API для типа Self (но только одним способом).
  • пользователям не нужно или не должно быть возможность влиять на настройку для конкретного Self.

Как вы можете видеть здесь, термин вход или выход не имеет (обязательно) никакого отношения к тому, являются ли I или O входами или выходами для фактической функции!

Несколько входных и выходных параметров

-53
-54

Как и примеры выше, автор трейта предполагает:

  • пользователи могут захотеть иметь возможность решать, для каких типов I должны быть возможны.
  • для входных данных, реализатор должен определить результирующий тип выходных данных.

Динамические типы / Типы нулевого размера

-55
  • Тип T имеет значение размера, если во время компиляции известно, сколько байт он занимает например u8, и &[u8], [u8] не имеют размера.
  • Размером подразумевается `impl Sized for T {}`. Это происходит автоматически и не может быть вызвано пользователем.
  • Типы без размера называются динамически размерными типами (DST), иногда неразмерными.
  • Типы без данных называются типами нулевого размера (ZST), не занимают места.
-56

?Sized

-57
-58
  • T может быть любого типа.
  • Тем не менее, существует невидимая привязка по умолчанию `T: Size`, поэтому `S<str>` невозможно из коробки.
  • Вместо этого мы должны добавить T : ?Sized, чтобы отказаться от этой привязки:
-59
-60

Дженерики и время жизни — <'a>

-61

Время жизни действует[*] как параметры типа:

-- пользователь должен указать определенный тип 'a (компилятор поможет в методах).
-- как `Vec<f32>` и `Vec<u8>` являются разными типами, так и `S<'p>` и `S<'q>` разные.
-- это означает, что вы не можете просто присвоить значение типа `S<'a>` переменной, ожидающей `S<'b>` (исключение: отношение «подтип» для жизненных периодов жизни, например, 'a переживет 'b).

-62
  • 'static — это только именуемый экземпляр времени жизни пространства типов.
-63

[*] Есть тонкие различия, например, вы можете создать явный экземпляр 0 типа u32, но за исключением «статических, вы не можете действительно создать время жизни», компилятор сделает это за вас.

Примечание для себя и TODO: эта аналогия кажется несколько ошибочной, как будто `S<'a>` относится к `S<'static>` как `S<T>` к `S<u32>`, статический будет типом, но тогда каково значение этого типа?

Внешние типы и трейты

Визуальный обзор типов и трейтов в вашем крейте.

-64

Примеры трейтов и типов, и какие трейты можно реализовать для какого типа.

Преобразования типов

Как получить B, когда у вас есть A?

Введение

-65
-66

[1] В то время как оба преобразуют `A` в `B`, принуждение, обычно ссылается на несвязанный `B` (тип «можно разумно ожидать, что у него разные методы»), в то время как подтипизация ссылок на `B` отличается только временем жизни.

Вычисления (трейты)

-67

Сахар, чтобы получить B от A. Некоторые трейты обеспечивают канонические, вычислимые пользователем отношения типов:

-68

Приведение типа

-69

Преобразование типов с ключевым словом, как будто преобразование относительно очевидно, но может вызвать проблемы.

-70

Где Ptr, Integer, Number просто используются для краткости и фактически означают:

  • Ptr любой `*const T` или `*mut T`.
  • Целое число любого типа u8 ... i128.
  • Вещественное число (f32, f64).

Мнение — Приведение, особенно Число — Число, может легко пойти не так. Если вы обеспокоены правильностью, рассмотрите более явные методы.

Принуждение

-71

Автоматическое ослабление типа от А до В; Типы могут быть существенно разными. [1]

-72

[1] По существу, можно ожидать, что результат принуждения `В` будет совершенно другого типа (т.е. иметь совершенно другие методы), чем исходный тип `А`.

[2] Не вполне работает в примере выше, так как неразмерные не могут быть в стеке; представьте себе `f(x: &A) - > &B` вместо этого. Отмена размера работает по умолчанию для:

  • `[T; n]` для `[T]`.
  • `T` для `dyn Trait` если `impl Trait for T {}`.
  • `Foo<…, T, …>` для `Foo<…, U, …>` при загадочных обстоятельствах.

Подтипизация

-73

Автоматически преобразует A в B для типов, отличающихся только временем жизни - примеры подтипизации:

-74

🛑 это не примеры подтипизации:

-75

Различные

-76

Автоматически преобразует A в B для типов, отличающихся только временем жизни - правила дисперсии подтипов:

  • Более длинная продолжительность жизни 'a, которая переживает более короткий 'b, является подтипом 'b.
  • Подразумевает, что `'static` является подтипом всех других жизней `'a`.
  • Независимо от того, являются ли типы с параметрами (например, &'a T) подтипами друг друга, используется следующая таблица дисперсии:
-77

Ковариант означает, что если `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.