PhantomData и подтипизация времён жизни (Branding 2/4)
Идея:
- Используем время жизни как уникальную метку для каждого токена.
- Сделаем времена жизни достаточно различными, чтобы они не преобразовывались друг в друга неявно.
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
use std::marker::PhantomData;
#[derive(Default)]
struct InvariantLifetime<'id>(PhantomData<&'id ()>); // Основное внимание
struct Wrapper<'a> { value: u8, invariant: InvariantLifetime<'a> }
fn lifetime_separator<T>(value: u8, f: impl for<'a> FnOnce(Wrapper<'a>) -> T) -> T {
f(Wrapper { value, invariant: InvariantLifetime::default() })
}
fn try_coerce_lifetimes<'a>(left: Wrapper<'a>, right: Wrapper<'a>) {}
fn main() {
lifetime_separator(1, |wrapped_1| {
lifetime_separator(2, |wrapped_2| {
// Мы хотим, чтобы это НЕ компилировалось
try_coerce_lifetimes(wrapped_1, wrapped_2);
});
});
}
-
В Rust между временами жизни могут существовать отношения подтипизации.
Такое отношение позволяет компилятору определить, переживает ли одно время жизни другое.
Определение того, что одно время жизни переживает другое, также позволяет утверждать: наименьшее общее время жизни — это то, которое заканчивается первым.
Это полезно во многих случаях: два разных времени жизни можно рассматривать как одинаковые в тех областях, где они перекрываются.
Обычно это именно то, что нужно. Но здесь мы хотим использовать времена жизни, чтобы различать значения, и добиться того, чтобы токен относился только к одной переменной, не создавая newtype для каждой объявленной переменной.
-
Цель: нам нужны два времени жизни, для которых компилятор Rust не может определить, что одно переживает другое.
Мы используем
try_coerce_lifetimesкак проверку на этапе компиляции: есть ли у времён жизни общее более короткое время жизни (иначе говоря, можно ли их подтипизировать). -
Примечание: этот слайд компилируется; к концу слайда он должен компилироваться только тогда, когда
try_coerce_lifetimesзакомментирован. -
В этом коде есть две важные части:
- Ограничение
impl for<'a>на замыкание, передаваемое вlifetime_separator. - То, как времена жизни используются в параметре для
PhantomData.
- Ограничение
Ограничение for<'a> на замыкание
-
Мы используем
for<'a>, чтобы ввести обобщённый параметр времени жизни в тип функции и потребовать, чтобы тело функции работало для всех возможных времён жизни.Это также лишает компилятор возможности делать предположения о конкретном времени жизни аргумента функции: тело должно соответствовать правилам анализатора заимствований независимо от «настоящего» времени жизни, которое будут иметь аргументы. Конкретное время жизни подставляет вызывающий код, сама функция сделать это не может.
Это аналог квантора всеобщности (Ɐ) в математике или того, как мы вводим
<T>как переменные типов, только для времён жизни в ограничениях трейтов.Когда мы пишем функцию, обобщённую по типу
T, мы не можем определить этот тип изнутри самой функции. Даже если вызвать функциюfn foo<T, U>(first: T, second: U)с двумя аргументами одного типа, тело функции не может определить, одинаковы лиTиU.Это также не позволяет потребителю API самому определить время жизни, что позволило бы обойти ограничения, которые мы хотим ввести.
PhantomData и вариантность времён жизни
-
Мы уже знаем
PhantomData: он создаёт формальное «пустое» использование типа или параметра времени жизни, которые иначе не использовались бы. -
Спросите: что ещё можно делать с
PhantomData?Ожидаемые ответы: паттерн typestate, связывание времён жизни значений, которыми владеют.
-
Спросите: что такое подтипизация в других языках?
Ожидаемые ответы: наследование, возможность использовать значение типа
Bтам, где ожидалось значение типаA, потому чтоB— «подтип»A. -
В Rust подтипизация есть! Но только для времён жизни.
Спросите: что может означать, что одно время жизни является подтипом другого?
Время жизни является «подтипом» другого времени жизни, когда оно переживает это другое время жизни.
-
Поведение времён жизни, используемых в
PhantomData, зависит не только от того, откуда берётся время жизни, но и от того, как определена ссылка.Причина, по которой это компилируется, в том, что вариантность времени жизни внутри
InvariantLifetimeслишком мягкая.Примечание: не ждите, что слушатели полностью поймут вариантность. Просто рассматривайте её как лестницу ограничений на то, насколько времена жизни могут устанавливать отношения подтипизации.
-
Спросите: как сделать это ограничение строже? Как сделать тип ссылки строже в Rust?
Ожидаемый ответ или демонстрация: заменить на
&'id mut (). Этого будет недостаточно!Нам нужна вариантность для времён жизни, при которой подтипизацию нельзя вывести, кроме как для одинаковых времён жизни. То есть единственный подтип
'a, который компилятор может знать, — это сам'a.Примечание: опять же, не пытайтесь заставить весь класс понять вариантность. Пока просто считайте её лестницей ограничений.
Продемонстрируйте переход от
&'id ()(ковариантно по времени жизни и типу),&'id mut ()(ковариантно по времени жизни, инвариантно по типу),*mut &'id mut ()(инвариантно по времени жизни и типу) к*mut &'id ()(инвариантно по времени жизни, но не по типу).Последние два не должны компилироваться — значит, мы наконец нашли варианты того, как связать времена жизни с
PhantomData, чтобы их нельзя было сравнивать друг с другом в этом контексте.Причина:
*mutозначает изменяемый сырой указатель. В Rust есть изменяемые указатели! Но рассуждать о них в безопасном Rust нельзя. Если сделать это изменяемым сырым указателем на ссылку с временем жизни, это усложняет способность компилятора к подтипизации, потому что анализатор заимствований не может рассуждать об изменяемых сырых указателях. -
Итог: мы показали способы не дать компилятору решить, что времена жизни «достаточно похожи», выбрав для времени жизни в
PhantomDataвариантность, достаточно строгую, чтобы этот слайд не компилировался.То есть теперь мы можем создавать переменные, которые существуют в одной области видимости, но чьи типы автоматически различаются для каждой переменной без лишнего шаблонного кода.
Для дальнейшего изучения
- Квантор
for<'a>используется не только для типов функций. Это трейт-ограничение высшего ранга (higher-ranked trait bound).