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

Сегодня рассмотрим интересную вещь из из стека 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. На странице специализации можно записаться на открытые уроки и посмотреть подробную программу.

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

Комментарии (3)


  1. yu_vl
    05.08.2025 07:41

    Странный у вас Swift. Есть оператор consume, а функции move нет.


  1. house2008
    05.08.2025 07:41

    Спасибо! Я сначала не понял зачем это всё сделано. Потом прочитал книгу по расту и там такое поведение по умолчанию и тогда сложилась картина.


  1. yu_vl
    05.08.2025 07:41

    Пример с BufferView небезопасный и компилятор не гарантирует время жизни array, именно поэтому сейчас добавили в язык ~Escapable и лайфтаймы. В стандартной библиотеке теперь будут (Raw)Span, Mutable(Raw)Span и UTF8Span.

    Передать массив без копирования можно будет так:

    
    func doSomething(_ value: Span<UInt8>) { ... }
    
    doSomething(array.span)