1. Вступ
Коли ви тільки звикаєте до масивів, легко подумати: «Ну, масив — це просто набір елементів, тож я можу взяти будь-який за індексом». І це правда, але лише в ідеальному світі, де індекс завжди відомий заздалегідь. У реальності так майже не буває. Частіше ви знаєте значення або умову, а задача стає схожою на пошук шкарпетки після прання: вона десь у купі, тож доводиться переглядати речі по одній.
Пошук у масиві — це набір стандартних інструментів для різних запитань: «Чи є такий елемент?», «Який перший підходить?», «Де він лежить, тобто який у нього індекс?». Swift дає готові методи, а наша мета — навчитися обирати правильний інструмент під задачу, а не той, що першим спаде на думку.
Для прикладів ми візьмемо невеликий консольний застосунок ReadingList — список книг або тем, які ви хочете прочитати. Там дуже природно виникають запитання: «Чи є вже така книга?», «Знайди першу книгу про Swift», «Знайди позицію елемента, щоб видалити його».
2. contains(_:): відповідь «так/ні»
Метод contains(_:) — це найпрямолінійніший інструмент пошуку. Він відповідає на запитання: чи є в масиві саме такий елемент. Він не повідомляє, де саме елемент міститься, не повертає сам елемент (він і так відомий — ви його передали), а просто повертає Bool: true або false. У реальних застосунках це дуже часта перевірка перед тим, як додавати щось у список повторно.
Практичне правило просте: contains добре підходить, коли вам потрібен саме факт наявності. Якщо потім вам усе одно доведеться шукати індекс, краще одразу використовувати метод, який повертає індекс, інакше вийде «шукаємо двічі». Але для початківців contains — чудовий старт, тому що він читається майже буквально як англійська фраза: «масив містить елемент?».
Приклад: перевірка на числах
Давайте почнемо з максимально простого прикладу, де все передбачувано: чисел. На них легко відчути, що contains — це просто обхід масиву та порівняння елементів, без прихованої телепатії.
import Foundation
let scores = [10, 20, 30, 40]
print(scores.contains(20)) // true
print(scores.contains(25)) // false
Тут усе прозоро: 20 є, 25 немає. І так, усередині contains не «вгадує», а чесно перевіряє елементи один за одним.
Приклад: рядки та маленька пастка з регістром
З рядками все так само, але є одна тонкість: рядки порівнюються точно, включно з регістром. Для людини “Swift” і “swift” можуть бути «приблизно одним і тим самим», а для комп’ютера це два різні значення. Тому для введення користувача часто роблять нормалізацію, наприклад через lowercased(). Але спочатку корисно побачити проблему в чистому вигляді.
import Foundation
let readingList = ["Swift", "Algorithms", "Git"]
print(readingList.contains("Swift")) // true
print(readingList.contains("swift")) // false (важливий регістр)
Якщо ви хочете «людський» пошук, доведеться домовитися про правила: або зберігати все в одному регістрі, або порівнювати нормалізовані версії.
3. first(where:): пошук за правилом
Іноді точний збіг не потрібен. Припустімо, ви хочете знайти «першу парну оцінку», «першу книгу, де в назві є слово Swift», «перше число більше 100». У таких задачах ви не знаєте точне значення елемента наперед. Ви знаєте умову, якій елемент має відповідати. Для цього існує first(where:).
first(where:) проходить по масиву зліва направо та повертає перший елемент, який підходить під умову. Якщо відповідного елемента немає, він поверне nil. Це одна з груп методів стандартної бібліотеки, і вона спеціально створена для того, щоб ви писали менше ручного коду з прапорцями та break.
Що таке правило для first(where:)
Слово «правило» звучить трохи абстрактно, тому перекладемо його на людську мову: правило — це функція, яка отримує один елемент і повертає true або false. Тобто вона каже: «Цей елемент підходить?» Так — true, ні — false. А first(where:) просто шукає перший елемент, для якого правило повернуло true.
Сигнатура, якщо говорити просто, така:
first(where: правило) -> Element?
правило: (Element) -> Bool
Є кілька способів задати правило для пошуку або вибору відповідних елементів. Найпростіший із них — записати його у вигляді звичайної функції.
Така функція на вхід отримує елемент масиву і повертає Bool: підходить він нам чи ні.
Щоб передати цю функцію в where, достатньо написати її імʼя без дужок — так само, як імʼя змінної. Це простіше, ніж здається, хоча спочатку виглядає трохи дивно.
Приклад: знайти перше парне число
На прикладі парних чисел особливо добре видно сенс first(where:): нам не важливо, яке саме парне число буде знайдено, нам важливо, щоб воно було першим відповідним. Парність перевіряється просто: остача від ділення на 2 дорівнює нулю.
import Foundation
func isEven(_ x: Int) -> Bool { // Функція повертає true для парних чисел
return x % 2 == 0
}
let numbers = [1, 3, 7, 10, 12]
print(numbers.first(where: isEven) ?? -1) // 10. Передаємо функцію як значення. Дужок немає — отже, функція не викликається
Зверніть увагу: результат — Int?, тому ми підставили значення за замовчуванням -1 через ??, щоб надрукувати число без if let. У реальній логіці застосунку ви частіше використовуватимете if let, але для демонстрації виводу так простіше.
Приклад: знайти першу тему «про Swift» за підрядком
Повернімося до нашого ReadingList. Нехай список книг і тем у нас зберігається рядками. Ми хочемо знайти першу тему, де трапляється слово «swift» у будь-якому регістрі. Це типовий «людський» пошук: користувач не зобов’язаний пам’ятати точну назву, але хоче швидко знайти потрібне.
import Foundation
func isAboutSwift(_ title: String) -> Bool { // Функція повертає true, якщо рядок містить Swift
return title.lowercased().contains("swift")
}
let readingList = ["Git basics", "Swift strings", "Algorithms"]
if let firstSwift = readingList.first(where: isAboutSwift) { // Передаємо функцію як значення. Функцію не викликаємо
print(firstSwift) // Swift strings
} else {
print("У списку немає тем про Swift")
}
Тут видно головну відмінність від contains: ми не шукаємо точний рядок, а шукаємо за умовою «у назві є Swift».
4. firstIndex(where:): пошук індексу
Буває, що вам потрібен не сам елемент, а його індекс, бо далі ви хочете діяти «на місці»: замінити, видалити, змінити сусідні елементи або вивести номер у списку. Для цього є firstIndex(where:). Він працює майже так само, як first(where:), тільки повертає не Element?, а Int? (точніше, Index?, але для звичайного масиву це Int).
І знову: якщо нічого не знайдено, повертається nil. У Swift це нормально: відсутність результату позначають nil, а не «магічним числом».
Приклад: знайти індекс першого числа більше 100
Цей приклад добре показує, навіщо потрібен індекс. Припустімо, ви хочете не просто дізнатися число, а, наприклад, видалити його або замінити. Тоді позиція важливіша за значення. І так, firstIndex(where:) теж проходить зліва направо, тому «перший» означає «з найменшим індексом».
import Foundation
func isBig(_ x: Int) -> Bool {
return x > 100
}
let values = [10, 55, 130, 200]
if let index = values.firstIndex(where: isBig) {
print(index) // 2
} else {
print("Немає чисел > 100")
}
Приклад: знайти індекс книги, щоб видалити її зі списку
У ReadingList дуже природна операція — «прибрати тему зі списку», тому що ви її вже пройшли або передумали. Щоб видалити елемент, нам потрібен індекс. Отже, нам підходить firstIndex(where:). А саме правило казатиме: «Це та сама книга?» — наприклад, порівняємо нормалізовані рядки.
Зробімо просту нормалізацію: обріжемо пробіли та переведемо рядок у нижній регістр. Це не робить пошук «ідеальним», але вже сильно зменшує кількість ситуацій «ну я ж майже так само написав».
import Foundation
var targetTitle = " swift strings "
func normalized(_ s: String) -> String {
return s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
func isSameTitleAsTarget(_ title: String) -> Bool {
return normalized(title) == normalized(targetTitle)
}
var readingList = ["Git basics", "Swift strings", "Algorithms"]
if let i = readingList.firstIndex(where: isSameTitleAsTarget) {
readingList.remove(at: i)
}
print(readingList) // ["Git basics", "Algorithms"]
Так, тут використовується змінна targetTitle ззовні функції-предиката. Це не ідеальне рішення, але на цьому рівні — чесний компроміс: ми уникаємо нового синтаксису замикань і водночас бачимо, як firstIndex(where:) працює з правилом.
Як обирати метод пошуку
Коли у вас з’являються три схожі інструменти, дуже легко почати використовувати їх навмання. Тому корисно навчитися формулювати запитання словами, а потім підбирати метод. Це схоже на вибір ключа: якщо ви намагаєтеся відкрити двері ключем від домофона, винен зазвичай не домофон, а невдалий вибір інструмента.
Нижче — проста табличка, яка допомагає не плутатися. Її можна тримати в голові як мінішпаргалку: «що я хочу отримати в результаті пошуку».
| Що ви хочете дізнатися | Метод | Що повертає | Якщо не знайдено |
|---|---|---|---|
| «Чи є такий елемент?» | |
|
|
| «Який перший елемент підходить?» | |
|
|
| «Де, тобто на якій позиції, розташований перший відповідний елемент?» | |
|
|
І ще одна маленька схема, щоб «зловити» інтуїцію:
flowchart TD
A[Потрібно щось знайти в масиві] --> B{Потрібен лише факт?}
B -->|Так| C[contains]
B -->|Ні| D{Потрібне значення чи індекс?}
D -->|Значення елемента| E["first(where:)"]
D -->|"Позиція (індекс)"| F["firstIndex(where:)"]
5. Мінізастосунок ReadingList: пошук на практиці
Тепер зберемо все в одну маленьку історію: у нас є список тем для читання, і користувач вводить нову тему. Ми хочемо додавати її лише тоді, коли вона ще не зустрічалася в «людському» сенсі, а також уміти знаходити першу тему за ключовим словом.
Цей приклад не претендує на повноцінний інтерфейс чи набір команд — ми просто тренуємося шукати правильно й спокійно обробляти Optional.
Перевірка: не додавати дублікати
Дублікати в списках — класика: «Я вже додавав цю тему, чи мені здається?» Якщо не перевіряти, список швидко перетворюється на хаос. Але й перевірка не повинна бути надто складною: на цьому рівні достатньо нормалізувати рядок і пройтися масивом у пошуку «за правилом».
Зверніть увагу на прийом: first(where:) != nil — це майже contains за умовою, тільки ми перевіряємо не значення, а факт знаходження.
import Foundation
var newTitle = " swift STRINGS "
func normalized(_ s: String) -> String {
return s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
func isDuplicate(_ title: String) -> Bool {
return normalized(title) == normalized(newTitle)
}
var readingList = ["Git basics", "Swift strings"]
let alreadyExists = readingList.first(where: isDuplicate) != nil
print(alreadyExists) // true
Знайти індекс і замінити елемент «на місці»
Іноді ви хочете не видалити, а виправити. Наприклад, ви домовилися зберігати назви у вигляді з великої літери, а користувач додав абияк. Тоді зручно знайти індекс і замінити елемент за індексом.
import Foundation
var target = "swift strings"
func normalized(_ s: String) -> String {
return s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
func isTarget(_ title: String) -> Bool {
return normalized(title) == normalized(target)
}
var readingList = ["Git basics", "swift strings"]
if let i = readingList.firstIndex(where: isTarget) {
readingList[i] = "Swift Strings"
}
print(readingList) // ["Git basics", "Swift Strings"]
6. Типові помилки під час пошуку в масиві
Помилка №1: використовувати contains там, де потрібен індекс.
Дуже частий сценарій: спочатку пишуть if array.contains(x), а потім усе одно потрібно видалити елемент, і починається другий пошук, уже вручну. Це не катастрофа, але це зайва робота і зайвий код. Якщо вам далі потрібен індекс, краще одразу шукати індекс.
Помилка №2: очікувати, що «не знайдено» поверне -1.
У деяких мовах так роблять, але Swift зазвичай повертає nil. І це добре: nil не можна випадково переплутати з «валідним індексом». Тому замість if index != -1 ви пишете if let index = ... — і це набагато безпечніше.
Помилка №3: плутати first(where:) і firstIndex(where:).
Вони звучать схоже, але повертають різні речі: один повертає елемент, інший — індекс. У результаті новачки іноді отримують Int?, думають, що це «значення елемента», і починають друкувати індекс так, ніби це знайдений елемент. Лікується просто: перед вибором методу сформулюйте запитання словами — «мені потрібен елемент чи позиція?».
Помилка №4: забувати обробити nil і використовувати !.
Пошук може нічого не знайти, і це нормальна ситуація. Якщо написати array.first(where: ...)!, то застосунок упаде, коли «нічого не знайдено». Іноді падіння доречне в навчальних прикладах, але у звичайному коді це майже завжди погана угода: ви міняєте «нормальну ситуацію» на падіння застосунку.
Помилка №5: порівнювати рядки «як є», не домовившись про правила.
Користувач вводить Swift, ви зберігаєте swift, і раптом пошук «не працює». Насправді він працює ідеально — просто за надто суворими правилами. Якщо ви хочете пошук «як у людини», уведіть нормалізацію: хоча б lowercased() і обрізання пробілів, а вже потім порівнюйте.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ