Почему обращение к элементу коллекции по индексу не позволяет переместить из неё значение, не реализующее Copy?
Индексирование коллекции обычно предоставляет доступ к элементу через заимствование, а не передаёт владение самим элементом. Поэтому попытка получить из Vec<String> значение через индекс трактуется как попытка переместить его из заимствованного содержимого и отклоняется компилятором.
Для извлечения значения с передачей владения нужно использовать операцию, которая явно меняет коллекцию: например, remove, swap_remove или потребляющий итератор.
Модель владения Rust предотвращает ошибки, характерные для ручного управления памятью: использование освобождённых объектов, двойное освобождение и несогласованный доступ к изменяемым данным. Индексирование при этом должно оставаться безопасным даже для контейнеров, элементы которых нельзя копировать.
Если бы обычное чтение по индексу автоматически перемещало элемент, контейнер мог бы остаться в невалидном состоянии или потерять элемент без явного указания разработчика. Поэтому язык разделяет получение доступа к элементу и операцию его извлечения.
Рассмотрим коллекцию строк. String не реализует Copy, поскольку копирование означало бы дублирование управления принадлежащим ей буфером. Простое обращение по индексу не должно незаметно передавать владение строкой наружу, иначе после такой операции вектор содержал бы логически отсутствующий элемент.
Индексирование также должно работать через общий интерфейс Index, который предоставляет доступ к результату как к ссылочному месту. Из этого места можно читать или записывать значение, но нельзя произвольно перемещать некопируемое значение наружу.
Оператор индексации концептуально связан с реализацией трейта Index, чей метод возвращает ссылку на элемент. Поэтому выражение с индексом в контексте чтения не является операцией извлечения владельца: оно даёт доступ к месту хранения через заимствование.
Для типа, реализующего Copy, чтение элемента приводит к копированию значения. Для String или другого некопируемого типа перемещение через такую ссылку запрещено: это было бы перемещением из заимствованного содержимого.
Индексирование можно использовать как место назначения. Например, присваивание нового значения элементу разрешено через изменяемое заимствование коллекции, потому что старое значение сначала корректно уничтожается или заменяется согласно правилам присваивания. Но получение старого некопируемого значения наружу требует специальной операции.
remove возвращает владение элементом и сдвигает последующие элементы, поэтому его сложность для Vec равна O(n). swap_remove работает за O(1), но заменяет удалённый элемент последним, изменяя порядок. Если нужно забрать элементы последовательно, обычно лучше использовать потребляющий итератор: он передаёт владение всеми элементами без отдельного сдвига при каждом извлечении.
Допустим, обработчик должен удалить из очереди задач задачу с известным индексом и передать её другому компоненту. Попытка взять задачу через индекс не подходит: индекс предоставляет заимствованный доступ, а задача содержит String и другие некопируемые поля.
Вариант с clone прост, но создаёт дополнительную копию данных и оставляет исходную задачу в очереди. remove сохраняет порядок задач, однако при удалении из начала или середины сдвигает остальные элементы и может стать дорогим для большой очереди.
Если порядок задач не важен, выбирают swap_remove: операция выполняется за O(1), но последний элемент перемещается на место удалённого. Для очереди с обязательным порядком лучше применить remove, а для полного последовательного потребления — into_iter.
Таким образом, операция извлечения выбирается явно по требованию к владению, порядку и производительности. Само индексирование остаётся только способом безопасно обратиться к элементу.
Почему Copy меняет результат индексирования?
Для Copy-типа получение значения из места чтения создаёт побитовую копию, поэтому исходный элемент остаётся в коллекции. Это не означает, что индексирование передало владение: оно лишь прочитало значение и скопировало его по правилам Copy.
Почему remove может вернуть некопируемый элемент?
remove получает изменяемый доступ к самой коллекции, а не только ссылку на отдельный элемент. Коллекция может корректно извлечь значение из своего внутреннего буфера, сдвинуть или заменить элементы и вернуть владельца извлечённого значения. Операция специально описывает изменение структуры контейнера, поэтому не нарушает правила владения.
Почему swap_remove быстрее remove, но не всегда подходит?
swap_remove заменяет удаляемый элемент последним элементом и уменьшает длину вектора, не сдвигая весь хвост. Поэтому операция обычно имеет сложность O(1), но порядок элементов меняется. Если порядок является частью логики программы, например в очереди или списке приоритетов, такой компромисс неприемлем и нужен remove либо другая структура данных.