Программирование RustВремена жизниСтарший разработчик Rust

Почему при подстановке типов функций время жизни аргумента ссылки проверяется контравариантно?

Почему при подстановке типов функций время жизни аргумента-ссылки проверяется контравариантно?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

В Rust времена жизни являются частью статической системы типов, которая должна гарантировать безопасность заимствований без сборщика мусора и без продления объектов во время выполнения. Для этого компилятору нужно проверять не только отдельные ссылки, но и возможность безопасной подстановки сложных типов.

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

Постановка проблемы

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

Однако длинную ссылку можно безопасно использовать как короткую: заимствование разрешено завершить раньше. Поэтому функция, принимающая короткую ссылку, обычно предъявляет более слабое требование к вызывающему коду.

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

Подробное решение

Пусть 'long означает время жизни, которое длится дольше 'short. Ссылка &'long T может быть укорочена до &'short T, но &'short T нельзя безопасно превратить в &'long T.

Для функции направление меняется:

  • функция типа fn(&'short T) может принять &'long T, сократив его использование до 'short;
  • поэтому её можно подставить туда, где требуется функция типа fn(&'long T);
  • функция типа fn(&'long T) не может заменить fn(&'short T), поскольку короткая ссылка не удовлетворяет требованию 'long.

Минимальная иллюстрация:

type AcceptsAny = for<'a> fn(&'a str); type AcceptsStatic = fn(&'static str); fn any_lifetime(_: &str) {} fn only_static(_: &'static str) {} let _: AcceptsStatic = any_lifetime; // let _: AcceptsAny = only_static; // ошибка

&str в параметре any_lifetime означает, что функция может работать с заимствованием любого подходящего времени жизни. Поэтому она подходит для вызова с 'static. Обратная подстановка невозможна: функция only_static не обязана корректно работать с временной ссылкой.

Это правило касается именно позиции параметра функции. Для результата функции направление обычно противоположное: возвращаемое значение является производимым функцией результатом, поэтому время жизни результата проверяется ковариантно. При анализе реальных типов также нужно учитывать другие свойства, например изменяемость ссылки и ограничения самих типов.

Аннотация времени жизни не меняет длительность объекта и не заставляет функцию хранить ссылку дольше. Она только описывает допустимую связь между временем жизни аргумента и требованиями типа функции.

Ситуация из практики

Представим обработчик входного буфера. Буфер создаётся вызывающим кодом, передаётся обработчику только на время вызова и после этого может быть изменён или уничтожен.

Возможны три подхода:

  1. Требовать обработчик, работающий только со ссылкой 'static. Это слишком жёстко: временный буфер нельзя будет передать без копирования.
  2. Передавать обработчику принадлежащий тип, например String или Vec<u8>. Это упрощает управление временем жизни, но может потребовать ненужного выделения памяти и передачи владения.
  3. Описать обработчик как функцию, способную принять ссылку с выбранным вызывающим кодом временем жизни. Это сохраняет отсутствие копирования и не позволяет обработчику использовать буфер после вызова.

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

Что кандидаты часто упускают

1. Дополнительный вопрос: означает ли контравариантность, что функция может сама продлить время жизни аргумента?

Нет. Контравариантность разрешает только безопасно использовать более длинную ссылку в контексте более короткого заимствования. Функция не получает права вернуть ссылку с продлённым временем жизни, сохранить её после разрешённого срока или сделать её 'static.

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

2. Дополнительный вопрос: почему обратная подстановка может привести к висячей ссылке?

Предположим, вызывающий код располагает ссылкой, действительной только до конца текущего блока, но передаёт её функции, которая по типу может хранить ссылку до 'long. Если разрешить такую подстановку, функция получила бы право использовать ссылку после уничтожения исходного значения.

Именно поэтому функция, требующая 'long, не может притворяться функцией, принимающей 'short. Контравариантность запрещает подмену, которая расширила бы допустимый срок использования входной ссылки.

3. Дополнительный вопрос: распространяется ли это правило на весь тип функции без учёта других параметров?

Нет. Совместимость проверяется по всем компонентам типа функции. Времена жизни аргументов анализируются с учётом контравариантности, но типы значений, изменяемость ссылок, количество параметров и тип результата также должны соответствовать правилам Rust.

Например, подходящее время жизни не сделает совместимыми функции, различающиеся типом аргумента String и Vec<u8>. Кроме того, универсальная функция, способная принять ссылку любого времени жизни, может быть совместима с более узким требованием, но функция, рассчитанная на один конкретный срок, не становится универсальной автоматически.