Трейты Copy возникли в раннем проектировании Rust как маркер для типов, которые могут быть дублированы с помощью простого побитового копирования без учета управления ресурсами. Drop был введен для обработки детерминированной очистки ресурсов для типов, управляющих внешними ресурсами, такими как файловые дескрипторы или память кучи. Конфликт между неявным дублированием и уникальным владением стал очевидным, когда проектировщики поняли, что побитовые копии будут делить неделимые дескрипторы ресурсов. В результате компилятор был спроектирован так, чтобы отклонять любые типы, пытающиеся одновременно реализовать оба трейта.
Если тип, реализующий Drop (например, управляющий файловым дескриптором), также был бы Copy, присвоение значения новой переменной создало бы два побитово идентичных копии. Когда обе копии выходят из области видимости, пользовательская реализация Drop выполняется дважды на одном и том же основном ресурсе. Это приводит к уязвимости double-free или use-after-free, если ресурс становится недействительным из-за первого вызова drop, но затем к нему происходит доступ со стороны второго, что нарушает безопасность памяти.
Компилятор Rust включает проверку согласованности в системе трейтов, которая явно запрещает типам реализовать как Copy, так и Drop. Это ограничение заставляет разработчиков использовать Clone (явное дублирование) для типов, требующих пользовательского уничтожения, позволяя реализации корректно увеличивать счетчики ссылок или выполнять глубокие копии. Обеспечивая, чтобы каждое логическое сущность имела соответствующий уникальный drop, система типов поддерживает абстракции без затрат, не жертвуя гарантиями безопасности.
Рассмотрим структуру DatabaseHandle, оборачивающую сырой указатель на объект соединения во внешней библиотеке C. Приложение требует передачи дескрипторов по значению в несколько замыканий для ведения логов, при этом каждый дескриптор должен закрывать свое уникальное соединение через вызов FFI, когда он уничтожается. Если бы дескриптор был Copy, неявное дублирование создало бы несколько дескрипторов, претендующих на владение одним и тем же основным ресурсом C, что неизбежно привело бы к двойным закрытиям или use-after-free, когда область выходит.
Один из подходов заключался в разрешении Copy и реализации Drop с внутренним подсчетом ссылок, используя Arc. Это добавило бы накладные расходы на синхронизацию для каждого дескриптора, увеличивая размер двоичного файла и стоимость выполнения всех операций. Это также усложняет границу FFI, где сырой указатель должен быть атомарно извлечен из Arc, вводя потенциальные взаимные блокировки, если логика drop сама вызывает код Rust.
Другой подход заключался в использовании Copy, но с документацией, что пользователи должны вручную вызывать метод close перед уничтожением значения. Это полностью перекладывает бремя обеспечения безопасности памяти на программиста, нарушая основополагающий принцип Rust предотвращать ошибки во время компиляции. Это неизбежно приводит к утечкам ресурсов, когда разработчики забывают вызвать close, или к двойным закрытиям, когда они случайно копируют дескриптор и пытаются закрыть обе копии.
Выбранным решением было убрать Copy и реализовать Clone вручную, вместе с Drop. Clone выполняет глубокое копирование, открывая новое соединение с базой данных, обеспечивая, чтобы каждый экземпляр владел своим уникальным ресурсом и предотвращал алиасинг основного указателя C. Drop закрывает только свое соединение, в то время как компилятор предотвращает случайные побитовые копии, поддерживая безопасность без накладных расходов во время выполнения.
Теперь система типов предотвращает случайное копирование на этапе компиляции, заставляя разработчиков явно вызывать clone и делая приобретение ресурсов видимым в исходном коде. Программа избегает ошибок double-free, когда дескрипторы передаются в потоки или замыкания, а гарантии детерминированного уничтожения остаются в силе без необходимости в атомарных операциях или ручном управлении памятью.
Почему я не могу произвести Copy для структуры, содержащей Vec?
Vec владеет памятью на куче и реализует Drop для освобождения этой памяти, когда вектор выходит из области видимости. Если бы структура, содержащая Vec, была бы Copy, побитовое дублирование создало бы две структуры, указывающие на один и тот же буфер кучи в стеке, но обе содержали бы один и тот же указатель на кучу. Когда первая структура уничтожается, память освобождается; когда вторая уничтожается, она пытается снова освободить ту же память, вызывая неопределенное поведение. Rust предотвращает это, требуя, чтобы все поля типа Copy также были Copy, рекурсивно обеспечивая отсутствие вложенных реализаций Drop.
Устраняет ли mem::forget проблемы с Copy и Drop?
std::mem::forget потребляет значение без вызова его деструктора, но это касается только одного конкретного владением значением, а не всех его копий. Если Copy и Drop были бы разрешены, забвение одной копии не помешает другим побитовым копиям выполнять свои реализации Drop, когда они выходят из области видимости. Оставшиеся вызовы drop все равно попытаются освободить один и тот же основной ресурс, что приведет к use-after-free или double-free независимо от забытого экземпляра.
Могу ли я использовать ManuallyDrop для безопасной реализации Copy?
Оборачивание поля в ManuallyDrop предотвращает автоматическое вызов Drop, что технически позволяет внешней структуре использовать Copy. Однако это переносит ответственность за вызов ManuallyDrop::drop на пользователя для каждого созданного копирования, фактически создавая сценарий ручного управления памятью. Если пользователь забудет освободить даже одну копию, ресурс навсегда утечет; Rust запрещает этот шаблон для типов, обладающих ресурсами, поскольку это подрывает гарантию безопасности детерминированной, автоматической очистки.