Привет, Хабр!
Сегодня рассмотрим интересную вещь из из стека 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 гарантирует это на этапе компиляции.
Быстро пройдемся по синтаксису
Что делаем |
Синтаксис |
Что происходит |
---|---|---|
Объявить тип без копирования |
|
Компилятор убирает авто‐ |
Явно переместить значение |
|
|
Передать во внешний код, завершив жизнь |
|
|
Временно одолжить |
|
Только чтение, без перемещения |
Эти ключевые слова встроены в язык и поддерживаются фронтендом, поэтому диагностика 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)
house2008
05.08.2025 07:41Спасибо! Я сначала не понял зачем это всё сделано. Потом прочитал книгу по расту и там такое поведение по умолчанию и тогда сложилась картина.
yu_vl
05.08.2025 07:41Пример с
BufferView
небезопасный и компилятор не гарантирует время жизни array, именно поэтому сейчас добавили в язык ~Escapable и лайфтаймы. В стандартной библиотеке теперь будут (Raw)Span, Mutable(Raw)Span и UTF8Span.Передать массив без копирования можно будет так:
func doSomething(_ value: Span<UInt8>) { ... } doSomething(array.span)
yu_vl
Странный у вас Swift. Есть оператор
consume
, а функцииmove
нет.