% Подтипы и вариантность
Хотя в Rust не входят никакие средства структурного наследования, он включает
в себя выделение подтипов. В Rust оно полностью вытекает из
времен жизни. Благодаря временам жизни и областям видимости мы можем частично
упорядочить выделение подтипов в виде отношений содержания ('a
переживает
'b
). Мы даже можем выразить это в виде обобщенной границы.
Выделение подтипов по временам жизни в терминах таких отношений будет
следующим: если 'a: 'b
("a содержит b" или "a живет дольше, чем b"), то 'a
является подтипом 'b
. Здесь существует огромная вероятность ошибиться, потому
что интуитивно кажется, что должно быть наоборот: большая область видимости
является подтипом меньшей.
Хотя на самом деле в этом есть смысл. Интуитивная причина этого - если вы
ожидаете &'a u8
, то вполне нормально передать вам &'static u8
, так же как
если бы вы ожидали Животного в Java, то было бы нормально передать вам Кошку.
Кошка - это Животное и кое-что еще, также как 'static
это просто 'a
и кое-
что еще.
(Помните, что отношения подтипов и типизация времен жизни - это довольно произвольная конструкция, с которой некоторые не соглашаются. Однако она прилично упрощает нам жизнь в части анализа времен жизни.)
Высокоуровневые времена жизни - это тоже подтипы каждого конкретного времени жизни. Так происходит, потому что произвольное время жизни, строго говоря, больше, чем какое-либо конкретное время жизни.
Вариантность
Вот где вещи становятся немного сложнее.
Вариантность - это то, как конструкторы типа относятся к своим аргументам.
Конструктор типа в Rust - это обобщённый тип без ограничений на аргументы.
Например Vec
- это конструктор типа, который принимает на входе T
и
возвращает Vec<T>
. &
и &mut
- это конструкторы типа, которые принимают 2
аргумента на входе: время жизни и тип, на который указывать.
Вариантность конструктора типа - это то, как выделение подтипов из аргументов на входе влияет на выделение подтипов на выходе. В Rust присутствуют два типа вариантности:
- Если из того, что
T
является подтипомU
, следует, чтоF<T>
является подтипомF<U>
, то F вариантна надT
. (выделение подтипов "проходит насквозь") - F инвариантна над
T
в противном случае (нельзя выделить отношений подтипов)
(Для тех, кто сталкивался с вариантностью в других языках - то, что мы относим
к "просто" вариантности, на самом деле является ковариантностью. У Rust есть
контрвариантность для функций. Будущее контрвариантности еще не определено, и
она может быть удалена. На данный момент fn(T)
контрвариантна над T
, и
используется при поиске реализации, подходящей под определение типажа.
У типажей нельзя вывести вариантность, поэтому Fn(T)
инвариантна к T
).
Некоторые важные вариантности:
&'a T
вариантна над'a
иT
(по аналогии,*const T
ведёт себя также)&'a mut T
вариантна над'a
, но инвариантна надT
Fn(T) -> U
инвариантна надT
, но вариантна надU
Box
,Vec
и другие коллекции вариантны над типами их содержимогоUnsafeCell<T>
,Cell<T>
,RefCell<T>
,Mutex<T>
и другие типы с внутренней изменяемостью инвариантны над T (по аналогии,*mut T
ведёт себя также)
Чтобы понять, почему эти вариантности правильны и важны, рассмотрим несколько примеров.
Мы уже рассматривали, почему &'a T
должна быть вариантна над 'a
, когда
представляли выделение подтипов: желательно иметь возможность передавать что-то
с большим временем жизни туда, где ожидается что-то с более коротким временем
жизни.
По похожей причине &'a T
должна быть вариантна над T. Разумно иметь
возможность передавать &&'static str
где ожидается &&'a str
. Дополнительный
уровень косвенности не влияет на передачу чего-то с большим временем жизни туда,
где ожидается что-то с более коротким временем жизни.
Однако, эта логика не применима к &mut
. Для того, чтобы понять почему &mut
должна быть инвариантна над T, возьмем следующий код:
fn overwrite<T: Copy>(input: &mut T, new: &mut T) {
*input = *new;
}
fn main() {
let mut forever_str: &'static str = "hello";
{
let string = String::from("world");
overwrite(&mut forever_str, &mut &*string);
}
// Упс, вывод освобожденной памяти
println!("{}", forever_str);
}
Сигнатура overwrite
абсолютна правильна: она берет изменяемую ссылку на два
значения одного типа и переписывает одно в другое. Если &mut T
была бы
вариантна над T, то &mut &'static str
была бы подтипом &mut &'a str
из-за
того, что &'static str
является подтипом &'a str
. Таким образом, время жизни
forever_str
успешно "усохло" бы до более короткого времени жизни string
и
overwrite
бы успешно вызвалось. string
впоследствии бы уничтожилось и
forever_str
указывало бы на освобожденную память, когда мы вызываем печать!
Данный пример показывает, почему &mut
должна быть инвариантна.
Это основная тема в вариантности против инвариантности: если вариантность позволяет хранить коротко живущее значение в долго живущей ячейке памяти, то надо использовать инвариантность.
В то же время &'a mut T
вариантна над 'a
. Основное отличие между 'a
и T -
'a
является свойством самой ссылки, в то время как T - это что-то, что ссылка
захватила. Если вы поменяете тип T, то источник будет все еще помнить
оригинальный тип. Но если вы поменяете время жизни типа, никто кроме ссылки не
помнит эту информацию, поэтому все в порядке. Говоря по другому: &'a mut T
владеет 'a
, но только заимствует T.
Интересными случаями являются Box
и Vec
, потому что они вариантны, но вы
можете хранить значения в них! Вот именно тут Rust становится особенно умным:
они вариантны, потому что вы можете хранить значения в них только посредством
изменяемой ссылки! Изменяемая ссылка делает весь тип инвариантным и поэтому не
позволяет перевезти коротко-живущие значения контрабандой в них.
Вариантность позволяет Box
и Vec
ослаблять условия общей изменяемости.
Поэтому вы можете передать &Box<&'static str>
туда , где ожидается &Box<&'a str>
.
Если идет передача по значению, то все гораздо менее очевидно. Оказывается, да, вы можете выделять подтипы при передаче по значению. Вот как это работает:
#![allow(unused)] fn main() { fn get_box<'a>(str: &'a str) -> Box<&'a str> { // строковые литералы являются `&'static str` Box::new("hello") } }
Ослабление при передаче по значению нормально проходит, потому что нет никого,
кто бы "помнил" старое время жизни в Box. Вариантность &mut
была проблемой,
потому что всегда был настоящий владелец, который помнил оригинальный под-тип.
Инвариантность типов ячеек можно объяснить так: &
похожа на &mut
для ячеек,
потому что вы можете менять значение в них по средствам &
. Поэтому ячейки
должны быть инвариантны, чтобы избежать незаконного ввоза времени жизни.
Fn
- это самый тонкий случай, потому что у него смешанная вариантность. Чтобы
понять почему Fn(T) -> U
должна быть инвариантна над T, создадим следующую
сигнатуру функции:
// 'a получается из родительской области видимости
fn foo(&'a str) -> usize;
Эта сигнатура утверждает, что может принять любую &str
, которая живет по крайней
мере 'a
. Теперь если бы эта сигнатура была вариантна над &'a str
, это означало
бы, что
fn foo(&'static str) -> usize;
можно подставить в это место, так как она является под-типом. Но у этой функции
требования строже: она утверждает, что может принимать только &'static str
и
ничего кроме. Невозможно было бы дать ей &'a str
, потому что она свободно
могла бы предположить, что ей дали то, что должно жить вечно. Поэтому функции
инвариантны над своими аргументами.
Чтобы понять, почему Fn(T) -> U
должна быть вариантна над U, создадим следующую
сигнатуру функции:
// 'a получается из родительской области видимости
fn foo(usize) -> &'a str;
Эта сигнатура утверждает, что вернет что-то, что будет жить дольше, чем 'a
.
Поэтому абсолютно разумно подставить
fn foo(usize) -> &'static str;
на ее место. Поэтому функции вариантны над своим возвращаемым типом.
У *const
такая же семантика как и у &
, а, следовательно, и вариантность. С
другой стороны *mut
можно разыменовать в &mut
, поэтому она инвариантна,
также как типы ячеек.
Все это хорошо для типов из стандартной библиотеки, но как вариантность
вычисляется для типов, которые определили вы? Структуры, говоря неформально,
наследуют вариантность своих полей. Если у структуры Foo
есть обобщенный
аргумент A
, который используется в поле a
, то вариантность Foo над A
будет
совпадать с вариантностью a
. Это усложняется, если A
используется в
нескольких полях.
- Если все использования A вариантны, то Foo вариантна над A
- Иначе, Foo инвариантна над A
#![allow(unused)] fn main() { use std::cell::Cell; struct Foo<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H> { a: &'a A, // вариантна над 'a и A b: &'b mut B, // вариантна над 'b и инвариантна над B c: *const C, // вариантна над C d: *mut D, // инвариантна над D e: Vec<E>, // вариантна над E f: Cell<F>, // инвариантна над F g: G, // вариантна над G h1: H, // была бы вариантна над H если бы не... h2: Cell<H>, // была бы инвариантна над H, потому что инвариантность побеждает } }