JavaRush /Курси /Swift SELF /Зрізи — ArraySlice ...

Зрізи — ArraySlice та Array(slice)

Swift SELF
Рівень 12 , Лекція 1
Відкрита

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 Що важливо пам’ятати
startIndex
зазвичай 0 може бути не 0 у зрізі початок часто збігається з початком вихідного масиву
endIndex
зазвичай дорівнює count «після останнього» endIndex не можна використовувати для читання елемента
for-in
безпечний безпечний переходить по елементах, а не по «нульовому індексу»
Array(slice)
рідко потрібно часто потрібно створює самостійні дані, але може копіювати елементи

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) і зберігати вже маленький самостійний масив.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ