Move-only типы и ключевое слово move в Swift

от автора

Привет, Хабр!

Сегодня рассмотрим интересную вещь из из стека Swift 6 — move‑only типы, ключевое слово move и всё, что с ними связано.

Зачем вообще нужны move-only значения

Управление памятью ест процессорное время в любых рантаймах — будь то ARC, GC или ручное подсчёт‑освобождение. Чем меньше копий объекта болтается в коде, тем проще компилятору доказать, что ресурс можно освободить без динамических проверок. Move‑only типы именно про это: значение можно переместить, но нельзя скопировать. Один владелец — один жизненный цикл — ноль референс‑каунтов. Концепция впервые была подробно описана командой языка ещё в 2022 году на форуме самого Swift, а в виде реализуемых proposal-ов вышла в SE-0366 (move функция) и SE-0390 (@noncopyable, ныне ~Copyable). После двух лет экспериментальных флагов фича доехала в стабильный Swift 6.

Классический пример — файловый дескриптор. Закрыли файл — старое значение недействительно, копии недопустимы. Move‑only гарантирует это на этапе компиляции.

Быстро пройдемся по синтаксису

Что делаем

Синтаксис

Что происходит

Объявить тип без копирования

struct MyFD: ~Copyable { ... }

Компилятор убирает авто‐Copyable

Явно переместить значение

let y = move(x)

x больше невалиден

Передать во внешний код, завершив жизнь

func consumeFile(consuming fd: FileHandle)

fd уничтожается в конце вызова

Временно одолжить

func read(borrowing fd: FileHandle)

Только чтение, без перемещения

Эти ключевые слова встроены в язык и поддерживаются фронтендом, поэтому диагностика use-after-move прилетает как обычная compile-time ошибка.

Что такое move

move — это не оператор, а inline-функция из стандартной библиотеки. Ей можно скормить любой movable binding: локальную let, var без проперти-обёрток или аргумент функции. Вызов завершает жизнь исходного binding-а и возвращает значение без лишнего retain/release. Компилятор проверяет, что после move(x) использование x запрещено; заодно подсвечивает подозрительные ветки кода диагностикой «use after move».

struct Ticket: ~Copyable {     let id: String }  func printOnce() {     let ticket = Ticket(id: "A123")     let moved = move(ticket)   // legal     // print(ticket.id)        // ошибка: use after move     print(moved.id) }

Как объявить move-only структуру

Синтаксис ~Copyable окончательно зацементирован в принятой версии SE-0390. Он дезактивирует автодобавление протокола Copyable и разрешает дополнительные возможности:

  • deinit в структурах и enum-ах,

  • методы с модификатором consuming self,

  • хранение небезопасных указателей без reference counting.

struct FileHandle: ~Copyable {     private let fd: Int32      init(path: String) throws {         fd = open(path, O_RDONLY)         if fd == -1 { throw IOError() }     }      consuming func close() {         Darwin.close(fd)     }      deinit {         // страховочный барьер         Darwin.close(fd)     } }

Код выше компилится в Swift 6 без флагов. В более старых версиях нужен -enable-experimental-move-only.

Borrowing и consuming аргументы

Чтобы вообще пользоваться значением после создания, нужны два новых модификатора параметров:

  • borrowing — даёт временный доступ без перемещения;

  • consuming — передаёт владение и завершает жизнь у вызывающей стороны.

func readHeader(borrowing fd: FileHandle) throws -> Header { ... }  func upload(consuming fd: FileHandle, to url: URL) async throws { ... }

Borrowing вызывает ноль retain-ов. После consuming компилятор запрещает использовать исходный fd.

Generic: ~Copyable в шаблонах

С выходом SE-0427 стандартный generic-синтаксис расширен: протоколы и параметры умеют описывать ограничения «может быть копируемым, может нет». Простое правило:

func process<T: Sequence>(consuming data: consuming T) where T.Element: ~Copyable { ... }

Тогда функция работает и с массивом копируемых строк, и с массивом не-копируемых ресурсов. Под капотом specialization создаёт две версии кода: copyable и move-only. Никаких обобщённых retain/release.

Пример 1. Безопасный файловый дескриптор

struct SafeFD: ~Copyable {     private let fd: Int32     init(path: String) throws { fd = try path.withCString { open($0, O_RDONLY) } }     consuming func close() { Darwin.close(fd) } }  func cat(path: String) throws {     var file = try SafeFD(path: path)     defer { file.close() }      // legal: defer объявлен в том же scope     try readLoop(borrowing: file) } // file уничтожен; double close невозможен

SafeFD закрывается ровно один раз, двойной close исключён на этапе компиляции.

Пример 2. State machine через typestate

Move-only даёт шикарный способ выразить конечный автомат без рантайм-чеков. Один тип — одно состояние.

protocol AuthState: ~Copyable { } struct LoggedOut: AuthState { } struct InFlight: ~Copyable { private var token: String } struct LoggedIn: AuthState { let user: User }  extension LoggedOut {     consuming func login(credentials: Creds) async throws -> InFlight { ... } } extension InFlight {     consuming func confirm(code: String) async throws -> LoggedIn { ... } } 

Попытка вызвать confirm на LoggedOut даже не скомпилируется.

Пример 3. Zero-copy slice в буфере

struct BufferView<Element>: ~Copyable {     private let base: UnsafePointer<Element>     private let count: Int      init(borrowing array: borrowing [Element]) {         base  = array.withUnsafeBufferPointer { $0.baseAddress! }         count = array.count     }      subscript(index: Int) -> Element {         precondition(index < count)         return base[index]     } }

BufferView умирает до исходного array, компилятор проверяет это автоматически. Никаких UB вокруг висячих указателей.

Заключение

Move-only типы дают предсказуемое управление ресурсами и ощутимый прирост скорости, убирая скрытые копии. С помощью move, borrowing и consuming мы проектируем API, который невозможно использовать неправильно.


Изучить Swift 5.x для развития профессиональных навыков уровня Junior/Middle/Senior iOS Developer можно с нуля на специализации iOS Developer. На странице специализации можно записаться на открытые уроки и посмотреть подробную программу.

Другие обучающие программы по разработке и не только можно найти в каталоге курсов.


ссылка на оригинал статьи https://habr.com/ru/articles/933034/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *