Вступ
Swift 6.2 привніс два нові типи, які серйозно змінюють підхід до роботи з пам'яттю в продуктивному коді: InlineArray та Span. Якщо ви хоч раз ловили себе на думці, що звичайний Array додає зайві heap-алокації там, де вони зовсім не потрібні, а перехід на UnsafeBufferPointer перетворює код на мінне поле — ці типи саме для вас.
Отже, коротко. InlineArray — масив фіксованого розміру, що живе прямо на стеку, без жодного звернення до heap. Span — безпечне, не-власницьке «вікно» у суміжну пам'ять, яке прийшло на заміну небезпечним вказівникам. Разом вони дають Swift-розробникам інструменти системного рівня, але зі збереженням повної безпеки пам'яті.
У цій статті ми розберемо обидва типи від синтаксису до реальних сценаріїв: розробка ігор, обробка графіки, взаємодія з C/C++. Усі приклади — робочий код для Xcode 26.
InlineArray: масив фіксованого розміру на стеку
InlineArray — спеціалізований тип масиву, елементи якого зберігаються inline, тобто безпосередньо всередині пам'яті типу-контейнера. Стандартний Array виділяє окремий блок у heap і використовує copy-on-write. InlineArray обходиться без цього повністю.
Формальне оголошення виглядає так:
@frozen struct InlineArray<let count: Int, Element> where Element: ~Copyable
Зверніть увагу на let count: Int. Це так звані value generics — нова фіча Swift, що дозволяє передавати значення (а не тільки типи) як параметри дженериків. Розмір масиву стає частиною типу і відомий уже на етапі компіляції.
Ключові характеристики InlineArray
- Фіксований розмір — визначається на етапі компіляції і не змінюється
- Stack-алокація — елементи живуть прямо на стеку (якщо масив не є властивістю класу)
- Без copy-on-write — копіювання відбувається одразу та цілком
- Підтримка некопійованих типів — може зберігати
~Copyableелементи - Передбачуване розміщення в пам'яті — ідеально для інтеропу з C API
Створення InlineArray: синтаксис та ініціалізація
Swift 6.2 дає кілька способів створити InlineArray, зокрема новий скорочений синтаксис [count of Element]:
// Повний синтаксис з явним типом
let explicit: InlineArray<3, Int> = [10, 20, 30]
// Скорочений синтаксис з ключовим словом of
let shorthand: [3 of Int] = [10, 20, 30]
// Автоматичне визначення розміру
let inferred: [_ of Int] = [10, 20, 30] // count = 3
// Повне автоматичне визначення
let fullyInferred: InlineArray = [10, 20, 30] // InlineArray<3, Int>
// Ініціалізація однаковими значеннями
let zeros: [5 of Double] = .init(repeating: 0.0)
Синтаксис [count of Element] читається майже як звичайна мова — «масив із 3 елементів типу Int». Чесно кажучи, це одне з найелегантніших API-рішень у Swift за останні роки. Коротко, зрозуміло, без зайвого шуму.
Рядки та складні типи
InlineArray працює не лише з числами, звісно:
// Масив рядків
let colors: [3 of String] = ["червоний", "зелений", "синій"]
// Масив кортежів
let points: [2 of (x: Double, y: Double)] = [
(x: 1.0, y: 2.0),
(x: 3.0, y: 4.0)
]
// Масив опціоналів
let maybeValues: [4 of Int?] = [1, nil, 3, nil]
Доступ до елементів та обмеження
Тут все знайоме — subscript з перевіркою меж:
var values: [3 of Double] = [1.0, 1.5, 2.0]
// Читання
print(values[0]) // 1.0
// Запис
values[1] = 99.9
print(values[1]) // 99.9
// Ітерація через indices
for i in values.indices {
print("Елемент \(i): \(values[i])")
}
// Небезпечний subscript без перевірки меж
let fast = values[unchecked: 2] // Лише коли ви абсолютно впевнені в індексі
Чого InlineArray НЕ підтримує
А ось тут — важливий момент, на якому багато хто спотикається. InlineArray не відповідає протоколам Sequence та Collection. Тобто забудьте про:
for element in array— пряма ітерація черезfor-in.map(),.filter(),.reduce()— функціональні трансформації.append(),.remove()— динамічна зміна розміру.contains(),.first,.last— пошук елементів
І це не баг — це свідомий вибір. InlineArray задуманий як мінімальний, продуктивний примітив. Якщо потрібна функціональна обробка, просто конвертуйте в звичайний Array:
let inline: [4 of Int] = [1, 2, 3, 4]
// Конвертація у звичайний Array для функціональної обробки
let regular = Array(unsafeUninitializedCapacity: inline.count) { buffer, count in
for i in inline.indices {
buffer[i] = inline[i]
}
count = inline.count
}
let doubled = regular.map { $0 * 2 } // [2, 4, 6, 8]
Розміщення в пам'яті: чому InlineArray швидший
Щоб зрозуміти, звідки береться перевага в продуктивності, достатньо подивитися на розміщення в пам'яті:
import Foundation
struct Pixel {
let r: UInt8
let g: UInt8
let b: UInt8
let a: UInt8
}
// InlineArray: елементи безпосередньо в пам'яті структури
print(MemoryLayout<[4 of UInt8]>.size) // 4 байти
print(MemoryLayout<[4 of UInt8]>.stride) // 4 байти
print(MemoryLayout<[4 of UInt8]>.alignment) // 1
// Для порівняння: кортеж
print(MemoryLayout<(UInt8, UInt8, UInt8, UInt8)>.size) // 4 байти
// Звичайний Array (heap + вказівник + count + capacity)
print(MemoryLayout<Array<UInt8>>.size) // 8 байт (вказівник на heap)
Бачите різницю? Звичайний Array тримає на стеку лише вказівник — 8 байт. А самі дані десь далеко, в heap. InlineArray зберігає все на місці: 4 елементи UInt8 — рівно 4 байти. Жодних алокацій, жодного підрахунку посилань, жодних перевірок copy-on-write.
Span: безпечне вікно в пам'ять
Якщо InlineArray відповідає за зберігання даних, то Span — за безпечний доступ до них. Span<Element> — це не-власницький, не-escape'абельний view на суміжний блок пам'яті. Він не копіює дані, а лише «позичає» доступ з гарантіями від компілятора.
Раніше для таких задач доводилося використовувати UnsafeBufferPointer (і молитися, що не буде use-after-free чи виходу за межі буфера). Span усуває ці ризики, зберігаючи ту саму продуктивність.
Отримання Span з колекцій
Стандартні типи Swift тепер мають властивість .span:
let numbers: [Int] = [10, 20, 30, 40, 50]
// Отримуємо Span з Array
let span: Span<Int> = numbers.span
// Span підтримує subscript з перевіркою меж
print(span[0]) // 10
print(span.count) // 5
// Ітерація по Span
for i in 0..<span.count {
print(span[i])
}
Span як параметр функції
І ось тут починається найцікавіше. Span дозволяє писати функції, які працюють з будь-яким суміжним сховищем, незалежно від конкретного типу контейнера:
// Ця функція працює з Array, ContiguousArray, InlineArray тощо
func sum(_ data: Span<Int>) -> Int {
var result = 0
for i in 0..<data.count {
result += data[i]
}
return result
}
// Виклик з Array
let array = [1, 2, 3, 4, 5]
print(sum(array.span)) // 15
// Виклик з InlineArray
let inline: [5 of Int] = [1, 2, 3, 4, 5]
print(sum(inline.span)) // 15
Один код — різні контейнери. Без копіювання, без накладних витрат на протоколи. Красиво, правда?
Сімейство Span: MutableSpan, RawSpan та інші
Swift 6.2 вводить цілу родину Span-типів. Кожен — для свого сценарію:
Span<Element>— лише для читання типізованих елементівMutableSpan<Element>— читання та запис типізованих елементівRawSpan— читання сирих байтів без типізаціїMutableRawSpan— читання та запис сирих байтівUTF8Span— спеціалізований тип для безпечної обробки Unicode
MutableSpan: безпечна модифікація пам'яті
var data = [1, 2, 3, 4, 5]
// Отримуємо мутабельний доступ
var mutableSpan: MutableSpan<Int> = data.mutableSpan
// Модифікуємо елементи на місці
for i in 0..<mutableSpan.count {
mutableSpan[i] *= 10
}
print(data) // [10, 20, 30, 40, 50]
RawSpan: робота з сирими байтами
let bytes: [UInt8] = [0xFF, 0x00, 0xAB, 0xCD]
let rawSpan: RawSpan = bytes.span.rawSpan
// Безпечне зчитування байтів
print(rawSpan.byteCount) // 4
Безпека Span: що гарантує компілятор
Span є non-escapable типом. Що це означає на практиці? Компілятор суворо контролює його час життя. Ви не можете:
- Повернути
Spanз функції (він не «втече» за межі scope) - Зберегти
Spanяк властивість структури чи класу - Захопити
Spanу closure
Звучить обмежувально? Можливо. Але саме ці обмеження гарантують, що Span завжди вказує на валідну пам'ять. Ніяких runtime-крешів через звислі вказівники — компілятор зловить проблему ще до запуску.
// ❌ Це НЕ скомпілюється — Span не може втекти
func dangerousSpan() -> Span<Int> {
let local = [1, 2, 3]
return local.span // Помилка компіляції: lifetime dependency
}
// ✅ Правильний підхід: обробка на місці
func processLocally() -> Int {
let local = [1, 2, 3]
let span = local.span
var sum = 0
for i in 0..<span.count {
sum += span[i]
}
return sum // Повертаємо результат, не Span
}
Взаємодія з C/C++
Для багатьох це буде найважливіший розділ. Span забезпечує безшовну інтероперабельність із C та C++. Коли C/C++ API анотовані відповідними атрибутами, Swift автоматично перетворює вказівники на безпечні Span:
// C-функція з анотацією __counted_by:
// void process_buffer(const int *data __counted_by(count), int count);
// Swift бачить це як:
// func process_buffer(_ data: Span<CInt>)
let values: [CInt] = [1, 2, 3, 4, 5]
process_buffer(values.span) // Безпечний виклик без ручної роботи з вказівниками
А для C++ типу std::span<T> Swift автоматично створює відповідний Span<T> (або MutableSpan<T> для non-const). Це реально спрощує міграцію існуючого C/C++ коду на безпечний Swift.
Практичний приклад: обробка пікселів зображення
Давайте розглянемо реальний сценарій, де InlineArray та Span працюють у парі — обробка RGBA-пікселів:
// Піксель як InlineArray — рівно 4 байти на стеку
struct RGBAPixel {
var channels: [4 of UInt8]
var r: UInt8 { channels[0] }
var g: UInt8 { channels[1] }
var b: UInt8 { channels[2] }
var a: UInt8 { channels[3] }
init(r: UInt8, g: UInt8, b: UInt8, a: UInt8) {
channels = [r, g, b, a]
}
}
// Застосування фільтра яскравості через Span
func adjustBrightness(
_ pixels: MutableSpan<RGBAPixel>,
factor: Double
) {
for i in 0..<pixels.count {
var pixel = pixels[i]
let newR = min(255, Double(pixel.r) * factor)
let newG = min(255, Double(pixel.g) * factor)
let newB = min(255, Double(pixel.b) * factor)
pixels[i] = RGBAPixel(
r: UInt8(newR),
g: UInt8(newG),
b: UInt8(newB),
a: pixel.a // Альфа-канал не чіпаємо
)
}
}
// Використання
var imageBuffer: [RGBAPixel] = [
RGBAPixel(r: 100, g: 150, b: 200, a: 255),
RGBAPixel(r: 50, g: 75, b: 100, a: 255),
]
adjustBrightness(&imageBuffer.mutableSpan, factor: 1.5)
RGBAPixel займає рівно 4 байти — жодного padding, жодних heap-алокацій. А adjustBrightness приймає MutableSpan, тому працюватиме з будь-яким контейнером пікселів. Саме такий код хочеться писати для обробки зображень.
Практичний приклад: матриця трансформації для ігор
// 4x4 матриця трансформації — класичний випадок для InlineArray
struct Matrix4x4 {
var elements: [16 of Float]
static var identity: Matrix4x4 {
var m: [16 of Float] = .init(repeating: 0.0)
m[0] = 1.0 // m[0][0]
m[5] = 1.0 // m[1][1]
m[10] = 1.0 // m[2][2]
m[15] = 1.0 // m[3][3]
return Matrix4x4(elements: m)
}
// Доступ до елементу за рядком і стовпцем
subscript(row: Int, col: Int) -> Float {
get { elements[row * 4 + col] }
set { elements[row * 4 + col] = newValue }
}
}
// Множення матриці на вектор
func multiply(
matrix: Matrix4x4,
vector: [4 of Float]
) -> [4 of Float] {
var result: [4 of Float] = .init(repeating: 0.0)
for row in 0..<4 {
var sum: Float = 0.0
for col in 0..<4 {
sum += matrix[row, col] * vector[col]
}
result[row] = sum
}
return result
}
let transform = Matrix4x4.identity
let point: [4 of Float] = [1.0, 2.0, 3.0, 1.0]
let result = multiply(matrix: transform, vector: point)
// result == [1.0, 2.0, 3.0, 1.0]
Ця Matrix4x4 займає рівно 64 байти (16 × 4) на стеку — ідентично представленню матриць у GPU-пам'яті. Для ігрового движка, де матриці створюються мільйони разів за кадр, різниця між stack- і heap-алокацією — це буквально різниця між стабільними 60 FPS та помітними заїканнями. Не перебільшую.
Порівняльна таблиця: Array vs InlineArray vs Span
| Характеристика | Array | InlineArray | Span |
|---|---|---|---|
| Розмір | Динамічний | Фіксований (compile-time) | Визначається джерелом |
| Алокація пам'яті | Heap | Stack | Немає (view) |
| Володіє пам'яттю | Так | Так | Ні |
| Copy-on-Write | Так | Ні | Ні |
| Sequence/Collection | Так | Ні | Ні |
| Зміна розміру | Так | Ні | Ні |
| Некопійовані елементи | Ні | Так | Так |
| C/C++ інтероп | Через вказівники | Пряме відображення | Автоматичний bridge |
| Найкращий для | Загальне використання | Продуктивний код, фіксовані буфери | Безпечний доступ до суміжної пам'яті |
Коли використовувати кожен тип
Ось проста схема, яка допоможе з вибором:
- Розмір відомий на етапі компіляції і продуктивність критична? →
InlineArray - Потрібно передати дані у функцію без копіювання? →
Span - Потрібні динамічні операції (append, filter, map)? → Звичайний
Array - Працюєте з C/C++ API і хочете безпеку? →
Span+ анотації - Обробка пікселів, аудіо, вершин? →
InlineArrayдля зберігання +Spanдля обробки
І головне — не кидайтесь замінювати кожен Array на InlineArray. Використовуйте нові типи там, де профайлер показує проблеми з алокаціями або де структури даних справді фіксовані. Для більшості звичайного коду стандартний Array як і раніше — найкращий вибір.
Часті запитання (FAQ)
Чи може InlineArray замінити кортежі (tuples)?
Частково — так. InlineArray дає subscript-доступ, ітерацію через indices та передбачуване розміщення в пам'яті. Кортежам цього бракує. Але є нюанс: кортежі підтримують різнотипні елементи ((String, Int)), а InlineArray вимагає однорідності. Тож якщо всі елементи одного типу і потрібен subscript — беріть InlineArray.
Чи можна використовувати ці типи в iOS-додатках для App Store?
Так, без проблем. Обидва типи доступні з Swift 6.2 та Xcode 26. InlineArray працює на всіх платформах Apple. Що стосується Span — сам тип доступний з iOS 12.2+, але зручні властивості на кшталт .span для Array потребують iOS 26+.
Яка різниця між Span і UnsafeBufferPointer?
По суті — ті самі можливості доступу до суміжної пам'яті, але з повною безпекою. Компілятор гарантує, що Span не переживе своє джерело (lifetime dependency) і не втече за межі scope (non-escapable). У UnsafeBufferPointer таких гарантій немає — там усе на вашій совісті.
Чи підтримує InlineArray протокол Codable?
Поки що — ні. InlineArray не відповідає ані Codable, ані Sequence, ані Collection. Для серіалізації доведеться конвертувати в звичайний Array. Цілком ймовірно, що підтримку розширять у наступних версіях Swift, але наразі це обмеження варто тримати на увазі.
Наскільки InlineArray реально швидший за Array?
Для малих фіксованих колекцій (до ~64 елементів) InlineArray може бути помітно швидшим завдяки відсутності heap-алокацій та copy-on-write. Різниця найвідчутніша в hot loops — циклах, що виконуються мільйони разів на секунду (рендеринг, аудіо, фізичні розрахунки). Для великих колекцій або рідкісних операцій перевага буде мінімальною.