InlineArray та Span у Swift 6.2: нові типи для продуктивності та безпеки пам'яті

Swift 6.2 представив InlineArray та Span — два типи для високопродуктивного коду. InlineArray зберігає елементи на стеку без heap-алокацій, а Span надає безпечний доступ до суміжної пам'яті. Синтаксис, практичні приклади та сценарії використання.

Вступ

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 — циклах, що виконуються мільйони разів на секунду (рендеринг, аудіо, фізичні розрахунки). Для великих колекцій або рідкісних операцій перевага буде мінімальною.

Про Автора Editorial Team

Our team of expert writers and editors.