Перейти к содержанию

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

Posted on:9 февраля 2023 г. at 16:14

Example Dynamic OG Image link

Types, Traits, Generics

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

Типы и трейты

Типы

ТипЗначение
u8{ 0$_u$$_8$, 1$_u$$_8$, …, 255$_u$$_8$ }
char{ ‘a’, ‘b’, … ’🦀’ }
struct S(u8, char){ (0$_u$$_8$, ‘a’), … (255$_u$$_8$, ’🦀’) }

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

ТипЗначение
u8{ 0$_u$$_8$, 1$_u$$_8$, …, 255$_u$$_8$ }
u16{ 0u$_u$$_1$$_6$, 1$_u$$_1$$_6$, …, 65_535$_u$$_1$$_6$ }
&u8{ 0xffaa$_&$$_u$$8$, 0xffbb$&$$_u$$_8$, … }`
&mut u8{ 0xffaa$_&$$_m$$_u$$_t$ $_u$$8$, 0xffbb$&$$_m$$_u$$_t$ $_u$$_8$, … }

Реализации — impl S { }

impl Port {
    fn f() { … }
}

Трейты — trait T { }

trait ShowHex {
    // Должны быть реализованы в соответствии с документацией.
    fn as_hex() -> String;

    // Предоставляется трейтом автора.
    fn print_hex() {}
}

trait Copy { }

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

impl ShowHex for Port { … }

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

Интерфейсы
// Лена импортирует 'Venison', чтобы создать ее, может использовать 'eat()', если она хочет.
import food.Venison;

new Venison("rudolph").eat();

Трейты
// Лена должна импортировать 'Venison', чтобы создать его, и импортировать 'Eat' для метода трейта..
use food::Venison;
use tasks::Eat;

Venison::new("Гена").eat();

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

Дженерики

Type Constructors — Vec<>

КонструкцияЗначение
Vec<u8>{ [], [1], [1, 2, 3], … }
Vec<char>{ [], [‘a’], [‘x’, ‘y’, ‘z’], … }
Vec<>-

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

Тип конструктораРеализация
struct Vec<T> {}Vec<u8>, Vec<f32>, Vec<Vec<u8>>, …
[T; 128][u8; 128], [char; 128], [Port; 128]
&T&u8, &u16, &str, …

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

// S<> является конструктором типа с параметром T, пользователь может поставить любой тип для T.
struct S<T> {
    x: T
}

// В коде для Т должен быть указан существующий тип.
fn f() {
    let x: S<f32> = S::new(0_f32);
}

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

Тип конструктораРеализация
[u8; N][u8; 0], [u8; 1], [u8; 2], …
struct S<const N: usize> {}S<1>, S<6>, S<123>, …

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

let x: [u8; 4]; // "массив из 4-х байтов"
let y: [f32; 16]; // "массив из 16 чисел с плавающей точкой"

// `MyArray` является конструктором типа, требующим типа `T` и
// размер 'N' для построения конкретного типа.
struct MyArray<T, const N: usize> {
    data: [T; N],
}

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

// Тип может быть создан только для некоторых «T», если
// T является частью «Абсолютного» списка членов.
struct Num<T> where T: Absolute {

}

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

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

struct S<T>
where
    T: Absolute + Dim + Mul + DirName + TwoD
{ … }

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

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

impl<T> S<T> where T: Absolute + Dim + Mul {
    fn f(&self, x: T) { … };
}

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

// Если компилятор столкнется с этим, он
// - проверит '0' и 'x' соответствуют требованиям членства 'T'
// - создаст две новые версии 'f', одну для 'char', другую для 'u32'.
// - на основе предоставленной реализации
s.f(0_u32);
s.f('x');

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

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

// реализует Serialize для любого типа, если этот тип уже реализует ToHex
impl<T> Serialize for T where T: ToHex { … }

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

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

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

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

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

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

Это почему?

impl From<u8> for u16 {}
impl From<u16> for u32 {}
impl Deref for Port { type O = u8; }
impl Deref for String { type O = str; }

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

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

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

trait Complex<I1, I2> {
    type O1;
    type O2;
}

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

trait A<I> { }
trait B { type O; }

// Реализатор добавляет (X, u32) к A.
impl A<u32> for X { }

// Реализатор добавляет impl. (X, ...) в A, пользователь может создавать объекты.
impl<T> A<T> for Y { }

// Конструктор обязан определить конкретную запись (X, O), добавленную к B.
impl B for X { type O = u32; }

Лена может добавить больше членов, предоставив свой собственный тип для T. Для заданного набора входных данных (здесь Self) программист обязан предварительно выбрать O.

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

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

Нет дополнительных параметров
trait Query {
    fn search(&self, needle: &str);
}

impl Query for PostgreSQL { … }
impl Query for Sled { … }

postgres.search("SELECT …");

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

Входные параметры
trait Query<I> {
    fn search(&self, needle: I);
}

impl Query<&str> for PostgreSQL { … }
impl Query<String> for PostgreSQL { … }
impl<T> Query<T> for Sled where T: ToU8Slice { … }

postgres.search("SELECT …");
postgres.search(input.to_string());
sled.search(file);

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

Выходные параметры
trait Query {
    type O;
    fn search(&self, needle: Self::O);
}

impl Query for PostgreSQL { type O = String; …}
impl Query for Sled { type O = Vec<u8>; … }

postgres.search("SELECT …".to_string());
sled.search(vec![0, 1, 2, 4]);

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

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

Несколько входных и выходных параметров
trait Query<I> {
    type O;
    fn search(&self, needle: I) -> Self::O;
}

impl Query<&str> for PostgreSQL { type O = String; … }
impl Query<CString> for PostgreSQL { type O = CString; … }
impl<T> Query<T> for Sled where T: ToU8Slice { type O = Vec<u8>; … }

postgres.search("SELECT …").to_uppercase();
sled.search(&[1, 2, 3, 4]).pop();

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

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

ПримерКомментарий
struct A { x: u8 }Тип A имеет размер, т.е. impl Size for A, это «обычный» тип.
struct B { x: [u8] }Поскольку [u8] является DST, B в свою очередь, становится DST, то есть не подразумевает размер.
struct C<T> { x: T }Параметры типа имеют неявное ограничение T: Size, например, C<A> является допустимым, C<B> - нет.
struct D<T: ?Sized> { x: T }Использование ?Sized позволяет отказаться от этой привязки, т.е <B>. D также действителен
struct E;Тип E имеет нулевой размер и не будет потреблять память.
trait F { fn f(&self); }Трейты не имеют неявной границы размера, т.е. допустимо impl F for B {}.
trait F: Sized {}Трейты однако, могут выбирать размер через супер трейты.
trait G { fn g(self); }Для ‘Self’, такие параметры как DST impl все еще может выйти из строя, так как параметры не могут идти в стеке.
?Sized

struct S<T> { … }
struct S<T> where T: ?Sized { … }
Дженерики и время жизни — <‘a>

// `'a является свободным параметром (cможет пройти любое конкретное время жизни)
struct S<'a> {
    x: &'a u32
}

// В неуниверсальном коде 'static — это единственное именируемое время жизни,
// которое мы можем явно вставить здесь.
let a: S<'static>;

// В качестве альтернативы, в необщемом коде мы можем (часто должны) опустить 'a
// и заставить Rust автоматически определить правильное значение для 'a.
let b: S;

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

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

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

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

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

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

Введение

fn f(x: A) -> B {
    // Как вы можете получить B от A?
}
МетодКомментарий
ИдентичностьТривиальный случай, B — это именно A.
ВычислениеСоздание экземпляра B и управление им путем написания кода, преобразующего данные.
ПриведениеПреобразование по требованию между типами, где рекомендуется соблюдать осторожность.
ПринуждениеАвтоматическое преобразование в рамках «ослабляющего набора правил».$^1$
ПодтипизацияАвтоматическое преобразование в наборе правил «одинаковый макет— разный срок службы».$^1$

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

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

fn f(x: A) -> B {
    x.into()
}

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

ТрейтПримерКомментарий
impl From<A> for B {}a.into()Очевидное, всегда допустимое отношение.
impl TryFrom<A> for B {}a.try_into()?Очевидное, иногда действительное отношение.
impl Deref for A {}*aA является интеллектуальным указателем несущим B, также допускает принуждение.
impl AsRef<B> for A {}a.as_ref()A можно рассматривать как B.
impl AsMut<B> for A {}a.as_mut()A можно рассматривать как мутабельное B.
impl Borrow<B> for A {}a.borrow()A позаимствовал аналог B (ведет себя так же как Eq, …).
impl ToOwned for A { … }a.to_owned()A владеет аналогом B.

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

fn f(x: A) -> B {
    x as B
}

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

ABПримерКомментарий
PtrPtrdevice_ptr as *const u8Если *A, *B имеют размер.
PtrIntegerdevice_ptr as usize
IntegerPtrmy_usize as *const Device
NumberNumbermy_u8 as u16Часто удивительное поведение.
enum w/o поляIntegerE::A as u8
boolIntegertrue as u8
charInteger'A' as u8
&[T; N]*const Tmy_ref as *const u8
fn(…)Ptrf as *const u8Если Ptr имеет Sized(размер).
fn(…)Integerf as usize

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

Принуждение

fn f(x: A) -> B {
    x
}

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

ABКомментарий
&mut T&TОслабление указателя.
&mut T*mut T-
&T*const T-
*mut T*const T-
&T&UDeref, если impl Deref<Target=U> for T.
TUUnsized, если impl CoerceUnsized<U> for T.$^2$
TVТранзитивность, если T связывается с U и U к V.
|x| x + xfn(u8) -> u8Замыкание без захвата до эквивалентного указателя fn.

$^1$ По существу, можно ожидать, что результат принуждения В будет совершенно другого типа (т.е. иметь совершенно другие методы), чем исходный тип А. $^2$ Не вполне работает в примере выше, так как неразмерные не могут быть в стеке; представьте себе f(x: &A) - > &B вместо этого. Отмена размера работает по умолчанию для:

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

fn f(x: A) -> B {
    x
}

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

ABКомментарий
&'static u8&'a u8Допустимый, вечный указатель также является переходным указателем.
&'a u8&'static u8🛑 Недействительный, преходящий не должен быть вечным.
&'a &'b u8&'a &'b u8Действительно, то же самое. Но теперь все становится интересным. Читайте дальше.
&'a &'static u8&'a &'b u8Допустимо, &'static u8 также &'b u8, ковариантный внутри &.
&'a mut &'static u8&'a mut &'b u8🛑 Недействительный и неожиданный, инвариант внутри &mut.
Box<&'static u8>Box<&'a u8>Действителен, Box вечный также Box с переходным процессом, ковариант.
Box<&'a u8>Box<&'static u8>🛑 Недействительный, Box с переходным может быть не вечной.
Box<&'a mut u8>Box<&'a u8>🛑 Неверно, см. таблицу ниже, &mut u8 никогда не был &u8.
Cell<&'static u8>Cell<&'a u8>🛑 Недействительно, Cell никогда не является чем-то другим, инвариант.
fn(&'static u8)fn(&'a u8)🛑 Если fn требуется навсегда, он может подавиться переходными процессами.
fn(&'a u8)fn(&'static u8)Питается переходными процессами может быть(!) вечной.
for<'r> fn(&'r u8)fn(&'a u8)Тип с более высоким рейтингом для <'r> fn(&'r u8) также является fn(&'a u8).

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

ABКомментарий
u16u8🛑 Явно недействителен, u16 никогда не должен автоматически быть u8.
u8u16🛑 Недействителен по замыслу, типы различные данные, даже если бы они могли преобразоваться.
&'a mut u8&'a u8🛑 Троянский конь, не подтип, а принуждение (все еще работает, только не подтипирование).

Различные

fn f(x: A) -> B {
    x
}

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

Конструкция'aTU
&'a Tковариантковариант
&'a mut Tковариантинвариант
Box<T>ковариант
Cell<T>инвариант
fn(T) -> Uконтравариантковариант
*const Tковариант
*mut 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.