В чём причина инвариантности изменяемой ссылки по типу значения, хотя её время жизни можно безопасно укоротить?
Изменяемая ссылка &'a mut T ковариантна по времени жизни 'a: ссылку можно использовать с более коротким временем жизни. Но по типу T она инвариантна, потому что через неё значение можно не только читать, но и записывать. Если разрешить произвольную замену T на совместимый подтип, запись могла бы поместить значение неподходящего типа и нарушить безопасность памяти.
В Rust время жизни и вариативность являются частью статической системы типов, которая должна обеспечивать безопасность памяти без сборщика мусора. Компилятору нужно определить, когда один ссылочный тип можно считать совместимым с другим, не выполняя код во время работы программы.
Для неизменяемой ссылки достаточно гарантировать, что ссылка не переживёт объект. Для изменяемой ссылки добавляется требование безопасности записи: получатель должен иметь право записывать ровно те значения, которые разрешены исходным типом.
Рассмотрим изменяемую ссылку на ссылку с долгим временем жизни. Если бы Rust разрешал считать её ссылкой на ссылку с более коротким временем жизни только на основании подтипизации, через неё можно было бы записать короткоживущую ссылку.
После завершения короткого заимствования исходный контейнер всё ещё мог бы использоваться как содержащий ссылку с долгим временем жизни. Это создало бы возможность чтения уже недействительной ссылки, поэтому такая замена типов запрещена.
Вариативность рассматривает разные части типа независимо. Для &'a mut T Rust допускает сокращение 'a, поскольку более короткое заимствование безопасно: ссылка просто перестаёт быть доступной раньше, а не начинает жить дольше объекта.
Однако параметр T инвариантен. Изменяемая ссылка является одновременно потребителем и производителем значения: она позволяет прочитать T и записать T. Для безопасной записи тип должен совпадать строго, а не быть лишь отношением подтипизации.
Здесь slot можно читать только как место, содержащее &'static str. Значение value живёт лишь 'short, поэтому запись через slot недопустима: иначе после окончания 'short в slot осталась бы ссылка, объявленная как 'static.
Важно не смешивать две оси. Укорачивание времени жизни изменяемой ссылки и замена типа значения внутри неё — разные операции: первое безопасно и поддерживается, второе запрещается инвариантностью T.
Практический компромисс состоит в том, что изменяемые ссылки менее гибки при передаче через обобщённые типы, зато компилятор сохраняет сильную гарантию корректности записей. Если нужна большая гибкость, обычно меняют интерфейс: передают значение по владению, используют неизменяемую ссылку или явно ограничивают требуемое время жизни.
Представим API конфигурации, который получает изменяемую ссылку на поле, содержащее строковую ссылку. Один из вариантов — разрешить функции принимать ссылку на строку с любой длительностью и затем записывать её в поле, объявленное как содержащее долгоживущую ссылку. Такой вариант должен быть отклонён: вызывающий код может передать временную строку.
Второй вариант — сделать поле владеющим String. Он устраняет проблему времени жизни и упрощает API, но может потребовать выделения памяти или копирования. Третий вариант — принимать только &'static str; он безопасен и прост, но существенно ограничивает вызывающий код.
Обычно выбирают владеющий тип, если объект должен хранить строку после завершения вызова. Если хранение не требуется, оставляют ссылочный API с корректно связанными временами жизни. Результат — отсутствие скрытого продления заимствований и невозможность записать короткоживущую ссылку туда, где ожидается долгоживущая.
Дополнительный вопрос 1: Ковариантна ли &'a mut T по самому времени жизни?
Да. Время жизни и тип значения — независимые параметры вариативности. Если изменяемая ссылка действительна дольше, её можно временно использовать там, где достаточно более короткого времени жизни; это не позволяет ссылке пережить исходный объект.
Дополнительный вопрос 2: Почему для &T замена типа обычно безопаснее, чем для &mut T?
Неизменяемая ссылка позволяет только читать значение. Если тип чтения заменяется на совместимый более общий тип, через ссылку нельзя записать значение, которое нарушило бы исходное ограничение. У &mut T есть операция записи, поэтому тип T должен оставаться инвариантным.
Дополнительный вопрос 3: Что именно нарушилось бы при ковариантности &mut T по T?
Появилась бы возможность передать ссылку на значение одного типа туда, где ожидается изменяемая ссылка на более общий тип, а затем записать значение, допустимое для общего типа, но недопустимое для исходного. В случае ссылок это могло бы превратить долгоживущую ссылку на объект в ссылку на временный объект. Инвариантность запрещает такую подмену ещё на этапе проверки типов.