1. ArraySlice: тип, що виглядає як масив
Іноді масив — це як буханець хліба: вам не завжди потрібен увесь батон, щоб зробити бутерброд. Часто хочеться взяти лише середину, перші 10 елементів або останні 3, і зробити з ними щось корисне: показати сторінку списку, обробити «хвіст», видати «топ‑N».
Наївний підхід — щоразу створювати новий масив і копіювати туди елементи. Swift уміє розумніше: замість «копіювати все» він часто повертає вам зріз — віртуальний масив, який насправді посилається на дані оригіналу.
Найпростіший спосіб отримати зріз — це взяти підмножину масиву. Уявіть, що у вас є список книжок, і ви хочете показати лише елементи з 5-го по 9-й. Логіка проста, але важливо зрозуміти: Swift може повернути не [String], а інший тип.
Якщо у квадратних дужках указати не один індекс, а діапазон, наприклад numbers[1...3], то Swift повертає не елемент і навіть не Array, а ArraySlice. Це зроблено для ефективності: зріз зберігає посилання на вихідне сховище і межі «вікна», а не копіює кожен елемент у новий контейнер. У результаті зріз швидко створюється і майже не витрачає памʼять.
Водночас є одна психологічна пастка: ArraySlice друкується і виглядає майже як масив. Через це дуже легко почати звертатися до нього як до масиву з індексацією від нуля — і саме тут Swift підготував вам мініквест.
Важливий принцип: ArraySlice — це колекція, індекси якої збігаються з індексами вихідного масиву. Тобто «0» не зобов’язаний означати перший елемент зрізу.
2. Як отримати ArraySlice: діапазони ... і ..<
Зріз створюється звичайним доступом через діапазон. Найважливіше — не переплутати ці два види діапазонів.
Закритий діапазон a...b
Тут права межа включається: беремо елементи з індексами a, a+1, ..., b.
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3] // ArraySlice<Int>
print(middle) // [20, 30, 40]
Напіввідкритий діапазон a..<b
Тут права межа не включається: беремо a, a+1, ..., b-1. Саме цей варіант найчастіше зручний у циклах і розрахунках.
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1..<4] // ArraySlice<Int>
print(middle) // [20, 30, 40]
Поки що все виглядає як звичайний масив, і саме тому мозок розслабляється. Але зараз ми його акуратно повернемо в робочий стан.
3. Головна пастка: індекси ArraySlice можуть починатися не з 0
Коли ви берете зріз, Swift зазвичай зберігає індекси вихідного масиву. Це означає таке:
- middle.startIndex може бути 1, а не 0;
- middle.endIndex може бути 4, а не 3;
- middle[0] може завершитися помилкою, навіть якщо «візуально» зріз починається з першого елемента.
Подивімося на індекси прямо:
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3] // індекси 1, 2, 3
print(middle.startIndex) // 1
print(middle.endIndex) // 4
Зверніть увагу на endIndex: він не є індексом останнього елемента, а позначає позицію після нього. Це базове правило для всіх Collection у Swift: endIndex — це межа за останнім елементом. І читати slice[slice.endIndex] не можна — це гарантована помилка.
Чому це важливо? Бо багато новачків, та й багато досвідчених, але втомлених, думають: якщо endIndex = 4, то останній елемент теж на 4. Ні: останній елемент тут має індекс endIndex - 1, а endIndex — це лише межа.
4. Безпечна робота з індексами startIndex і endIndex
Коли вам потрібен перший елемент зрізу, не пишіть slice[0]. Пишіть через startIndex.
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3]
let first = middle[middle.startIndex]
print(first) // 20
Якщо вам потрібен останній елемент, можна взяти індекс перед endIndex:
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3]
let lastIndex = middle.index(before: middle.endIndex)
print(middle[lastIndex]) // 40
Тут ви вперше бачите метод index(before:). Це не складна магія, а просто коректний спосіб працювати з межами колекції. Програмісти люблять усе ускладнювати.
Перебір ArraySlice: for-in і обережно з enumerated()
Добра новина: найчастіше вам узагалі не потрібні індекси. Якщо завдання — пройтися по елементах і щось вивести, то for element in slice працює ідеально.
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3]
for x in middle {
print(x)
}
// 20
// 30
// 40
Якщо ж вам потрібні і індекс, і значення, то запам’ятайте тонкість: enumerated() дає зсуви, а не «справжні індекси колекції». Для ArraySlice це особливо незручно, тому що enumerated() почне з 0, хоча реальний startIndex може бути 15.
Правило на практиці:
- якщо потрібен просто порядковий номер для виводу, enumerated() підходить;
- якщо потрібен саме реальний індекс колекції, дивіться в бік slice.indices.
Мініпам’ятка: startIndex / endIndex
Коли в голові все перемішалося, корисно мати маленький орієнтир.
| Властивість | Для Array | Для ArraySlice | Що важливо пам’ятати |
|---|---|---|---|
|
зазвичай 0 | може бути не 0 | у зрізі початок часто збігається з початком вихідного масиву |
|
зазвичай дорівнює count | «після останнього» | endIndex не можна використовувати для читання елемента |
|
безпечний | безпечний | переходить по елементах, а не по «нульовому індексу» |
|
рідко потрібно | часто потрібно | створює самостійні дані, але може копіювати елементи |
5. Коли потрібен Array(slice) і скільки це коштує
Якщо ArraySlice такий швидкий і зручний, навіщо взагалі Array(slice)?
Тому що інколи вам потрібен самостійний масив:
- з індексами від 0;
- який не залежить від вихідного масиву;
- який зручно зберігати, передавати далі й не думати про спільні індекси.
Найпростіший спосіб: Array(middle).
let numbers = [10, 20, 30, 40, 50]
let middle = numbers[1...3] // ArraySlice<Int>
let asArray = Array(middle) // Array<Int>
print(asArray[0]) // 20
Тут asArray[0] безпечний і означає перший елемент, тому що це вже повноцінний масив.
Важлива думка: Array(slice) може копіювати
Коли ви робите Array(slice), Swift створює новий масив, а отже потенційно копіює елементи. Це не «погано» і не «добре» — це нормально. Але робити так про всяк випадок не завжди розумно.
Є ще один практичний момент: зріз — це «вікно» на вихідні дані. Якщо вихідний масив величезний, а ви зберегли десь маленький зріз «на потім», то є ризик, що вихідний масив не зможе звільнити памʼять, бо на нього все ще є посилання через зріз. У таких випадках якраз доречно зробити Array(slice) і зберігати вже маленький самостійний масив.
6. Приклад: пагінація через ArraySlice
Щоб не залишатися у світі абстрактних чисел, давайте зробимо навчальний ескіз «бібліотеки»: зберігатимемо назви книжок у масиві рядків і показуватимемо їх «сторінками».
Ідея така: «сторінка» — це зріз масиву.
Функція, що повертає зріз сторінки
func pageSlice(of items: [String], page: Int, pageSize: Int) -> ArraySlice<String> {
let start = page * pageSize
let end = min(start + pageSize, items.count)
if start >= end { return [] } // порожній зріз
return items[start..<end] // ArraySlice<String>
}
Тут важливо, що ми повертаємо саме ArraySlice<String>. Ми не копіюємо елементи, а просто беремо «вікно».
Використовуємо цю функцію
let books = [
"Dune", "1984", "The Hobbit", "Fahrenheit 451",
"Brave New World", "Foundation", "Neuromancer"
]
let firstPage = pageSlice(of: books, page: 0, pageSize: 3)
print(firstPage) // ["Dune", "1984", "The Hobbit"]
Демонстрація проблеми: slice[0] — це пастка
let books = ["Dune", "1984", "The Hobbit", "Fahrenheit 451"]
let secondPage = books[2...3] // елементи з індексами 2 і 3
// print(secondPage[0]) // ❌ може завершитися помилкою: індексу 0 не існує
print(secondPage.startIndex) // 2
Усе чесно: у secondPage перший індекс — 2. Тому secondPage[0] — це помилка: такого індексу немає.
Правильний спосіб: startIndex
let books = ["Dune", "1984", "The Hobbit", "Fahrenheit 451"]
let secondPage = books[2...3]
let firstBookOnPage = secondPage[secondPage.startIndex]
print(firstBookOnPage) // The Hobbit
Коли нам потрібна «сторінка як масив»: робимо Array(slice)
Припустімо, ми хочемо передати сторінку у функцію, яка очікує [String] — звичайний масив, наприклад для відображення списку.
func printList(_ items: [String]) {
for (i, title) in items.enumerated() {
print("\(i + 1). \(title)")
}
}
let books = ["Dune", "1984", "The Hobbit", "Fahrenheit 451"]
let page = books[1...2] // ArraySlice<String>
printList(Array(page)) // перетворюємо на [String]
Тут Array(page) робить копію даних сторінки в новий масив, і далі всередині printList можна спокійно працювати з індексами від 0.
7. Типові помилки під час роботи зі зрізами ArraySlice
Помилка № 1: звертатися до зрізу як до масиву з індексом 0.
Це найчастіша пастка: зріз виглядає як масив, друкується як масив, «пахне» як масив, але його індекси можуть починатися не з нуля. Правильне рішення залежить від мети: якщо ви хочете саме перший елемент зрізу, беріть його через slice[slice.startIndex]. Якщо вам потрібно, щоб індексація знову починалася з нуля, зробіть Array(slice).
Помилка № 2: намагатися читати елемент за endIndex.
endIndex — це позиція «після останнього елемента», а не індекс останнього елемента. Тому slice[slice.endIndex] — це вихід за межі. Якщо потрібен останній елемент, використовуйте slice.index(before:)(slice.endIndex) і вже за цим індексом читайте значення.
Помилка № 3: плутати «зсув» із enumerated() з реальним індексом ArraySlice.
enumerated() повертає пари (offset, element), де offset починається з 0, навіть якщо startIndex дорівнює 100. На ArraySlice це особливо підступно: enumerated() дає не індекс колекції, а зсув. Якщо вам потрібен справжній індекс, краще ітеруватися по slice.indices, а якщо потрібен просто порядковий номер, то enumerated() цілком підходить, але сприймайте це число як номер у виводі, а не як індекс масиву.
Помилка № 4: робити Array(slice) завжди про всяк випадок.
Іноді хочеться одразу перетворити все на масив, щоб не думати. Це робочий підхід, але він може призводити до зайвого копіювання. Добрий стиль — копіювати лише тоді, коли ви справді хочете автономні дані або індексацію з нуля.
Помилка № 5: зберігати маленький ArraySlice, забувши, що він може утримувати великий вихідний масив.
Зріз — це «вікно» на вихідні дані. Якщо вихідний масив величезний, а ви зберегли десь маленький зріз «на потім», то є ризик, що вихідний масив не зможе звільнити пам’ять, бо на нього все ще є посилання через зріз. У таких випадках доречно зробити Array(slice) і зберігати вже маленький самостійний масив.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ