1. Точки зупинки та керування виконанням
Коли застосунок поводиться дивно, перша реакція новачка — розставити print() усюди, куди дотягнеться рука. Це не гріх: це природний «ліхтарик у темряві». Але в print() є побічний ефект: ви вносите зміни в код, перезапускаєте програму, отримуєте купу шуму й усе одно не завжди розумієте, у який момент щось пішло не так. Налагоджувач же дає змогу зупинити програму в живому стані, подивитися на значення й акуратно пройти виконання крок за кроком.
Головна ідея налагоджувача проста: ви не женете програму до самого кінця на повній швидкості. Натомість ставите паузу в потрібному рядку й вирішуєте, що робити далі.
До речі, під капотом багато IDE використовують LLDB — стандартний налагоджувач в екосистемі Swift. Він уміє покроково виконувати Swift-код, ставити точки зупинки та показувати стан програми.
Брейкпоінт: що це таке і де він справді працює
Брейкпоінт (breakpoint) — це точка зупинки. Коли виконання доходить до рядка з брейкпоінтом, програма завмирає, а ви отримуєте змогу подивитися на змінні, параметри функцій і поточний рядок виконання. Важливо розуміти, що брейкпоінт ставлять не на будь-який текст, а на виконуваний рядок: там має бути оператор, вираз або виклик функції.
У більшості IDE це роблять кліком по лівому полю (gutter) поруч із номером рядка: там зʼявляється червона точка. Після запуску в режимі Debug IDE доведе виконання до цієї точки й зупинить програму.
Невеликий приклад, де брейкпоінт особливо доречний, — рядок із викликом функції. Саме тут найпростіше відчути різницю між Step Over і Step Into.
import Foundation
func subtotal(count: Int, price: Int) -> Int {
count * price
}
func totalPrice(count: Int, price: Int) -> Int {
let value = subtotal(count: count, price: price) // брейкпоінт сюди
return value
}
print(totalPrice(count: 3, price: 10)) // 30
У цьому прикладі брейкпоінт стоїть на ідеальній «розвилці»: можна або виконати виклик як одну операцію, або зайти всередину subtotal.
Continue і Resume: нехай виконується, доки не стане цікаво
Після того як ви зупинилися на брейкпоінті, у вас є вибір: пройти далі покроково або просто продовжити виконання до наступної точки зупинки. Команда Continue/Resume (у різних IDE може називатися по-різному) робить саме це: програма далі виконується на повній швидкості, доки не натрапить на наступний брейкпоінт або не завершиться.
Це базовий режим роботи з налагоджувачем: зазвичай ви ставите кілька опорних брейкпоінтів у ключових місцях — на вході у функцію, у важливій гілці if, у місці розбору введення — і переходите між ними за допомогою Continue. Покроковий режим потрібен не завжди: часто він нагадує лупу, корисну тоді, коли ви вже знаєте, куди дивитися.
Важливий психологічний момент: налагоджувач — не іспит на терпіння. Якщо ви відчуваєте, що застрягли й крок за кроком проходите очевидний код, найімовірніше, час натиснути Continue і зупинитися пізніше — ближче до місця, де проблема справді проявляється.
2. Покрокове налагодження: Step Over, Step Into і Step Out
Step Over: виконайте рядок цілком, але не відкривайте капот
Step Over — це режим «зробіть один крок по поточному рядку, але не заходьте всередину викликаних функцій». Для новачків це майже завжди найкращий старт: ви бачите, як змінюються змінні після виконання рядка, але не провалюєтеся в чужі деталі.
Чому це важливо? Тому що у Swift ви постійно викликаєте функції стандартної бібліотеки (наприклад, split, map, Int(...), append). Якщо щоразу робити Step Into, ви дуже швидко опинитеся в місцях, де все виглядає як «магія компілятора й давні руни».
Повернімося до прикладу:
import Foundation
func subtotal(count: Int, price: Int) -> Int {
count * price
}
func totalPrice(count: Int, price: Int) -> Int {
let value = subtotal(count: count, price: price) // брейкпоінт
return value
}
print(totalPrice(count: 3, price: 10)) // 30
Якщо на рядку let value = subtotal(...) натиснути Step Over, налагоджувач виконає subtotal, повернеться назад і покаже вам уже готове value. Це схоже на ситуацію, коли ви довіряєте знайомому кухарю: «просто зроби бургер», а не «давай разом вирощувати корову».
Step Into: заходимо всередину, якщо там справді підозріло
Step Into — це режим, коли налагоджувач входить усередину функції, яку ви викликаєте на поточному рядку. Це корисно, коли ви підозрюєте помилку всередині цієї функції: неправильну формулу, некоректну межу діапазону, неочікуваний return із guard або класику жанру — !, який раптом виявився не таким уже й «точно не nil».
Однак Step Into вимагає дисципліни: заходити варто у свій код, а не в стандартну бібліотеку. Якщо ви за звичкою натискаєте Step Into на рядку на кшталт let parts = input.split(...), ви ризикуєте побачити «світ, де живуть бібліотечні ельфи», а початкова проблема так і залишиться без уваги.
Невеликий приклад для Step Into — розбір числа. У реальному застосунку LibraryCLI ми постійно перетворюємо текст користувача на числа або ID, і саме тут часто зʼявляються помилки.
import Foundation
func parsePort(_ text: String?) -> Int? {
guard let text else { return nil }
guard let value = Int(text) else { return nil } // брейкпоінт сюди
return value
}
print(parsePort("8080") ?? -1) // 8080
На рядку guard let value = Int(text) Step Into іноді допомагає зрозуміти, чому ви отримуєте nil (хоча в більшості випадків це просто «рядок не є числом»). А от у код навколо цього розбору — обрізання пробілів, перевірки порожнього рядка, вибору значення за замовчуванням — зазвичай корисніше заходити, ніж усередину Int(...).
До речі, сама ідея того, що деякі помилки в колекціях та індексах вважаються невідновними логічними помилками й призводять до trap/crash, у Swift цілком концептуальна: порушили вимоги API — отримали зупинку виконання як засіб захисту.
Step Out: я зайшов надто глибоко, врятуйте
Step Out — це команда «вийти з поточної функції та повернутися туди, звідки її викликали». Вона особливо корисна, коли ви натиснули Step Into за інерцією, опинилися у функції, яка не є джерелом проблеми, і тепер не хочете натискати Step Over двадцять разів, поки вона завершиться.
Уявіть: ви зайшли всередину parseCommand(...), потім у parseBookID(...), потім у якусь допоміжну функцію нормалізації рядка — і раптом зрозуміли, що помилка не тут, а повернутися треба до місця виклику. Step Out — це як кнопка «повернутися на рівень вище в меню», тільки для виконання програми.
Хороша звичка: якщо ви не можете вголос сформулювати фразу «я зараз заходжу всередину, бо підозрюю, що помилка саме там», то Step Into вам, найімовірніше, не потрібен. А якщо вже зайшли — Step Out ваш друг.
Порівняння кроків: мінітаблиця
Коли ви вперше бачите три кнопки Step Over/Into/Out, здається, ніби це якийсь «тест на професійну придатність». Насправді це дуже проста трійка дій, і вона чудово лягає в таблицю.
| Дія | Що робить | Коли обирати | Типовий анти-кейс |
|---|---|---|---|
|
Виконує рядок цілком, не заходячи у виклики | Ви перевіряєте, як змінюються змінні на рівні поточної функції | «Провалююся у стандартну бібліотеку й гублю нитку» |
|
Заходить усередину викликаної функції | Ви підозрюєте баг усередині цієї функції (бажано вашої) | «Натиснув випадково, опинився в чужому коді» |
|
Виходить із поточної функції на рівень вище | Ви зрозуміли, що зайшли не туди або вже побачили достатньо | «Тисну Step Over 50 разів, щоб вибратися» |
Якщо запамʼятати лише одне правило: починайте зі Step Over, а всередину заходьте лише за потреби — якість налагодження різко зросте.
4. Умовні брейкпоінти
Цикли — це місце, де налагодження може перетворитися на тортури. Уявімо, що у вас for на 10 000 елементів, а помилка проявляється лише на одному значенні. Якщо проходити кожну ітерацію, ви або постарієте, або почнете ненавидіти програмування — а нам хотілося б навпаки.
Умовний брейкпоінт розвʼязує цю проблему: він спрацьовує лише тоді, коли умова істинна. Зазвичай умову записують як логічний вираз, наприклад v == 9.
import Foundation
let values = [3, 7, 2, 9, 4]
var sum = 0
for v in values {
sum += v // умовний брейкпоінт: зупинитися лише коли v == 9
}
print(sum) // 25
Сенс такої зупинки — упіймати потрібний момент. Ви не витрачаєте час на нормальні ітерації, а ловите ту, що виглядає підозрілою.
Умовні брейкпоінти корисні не лише в for, а й у сценаріях розбору: наприклад, зупинитися, коли вхідна команда дорівнює "remove" або коли tokens.count < 2.
5. Куди ставити брейкпоінти в LibraryCLI
На початку навчання здається, ніби налагодження — це суперсила. Насправді ж це передусім звичка ставити точки зупинки в місцях, де програма ухвалює рішення або змінює дані. У нашому навчальному застосунку LibraryCLI такі місця трапляються постійно: ми читаємо рядок команди, розбиваємо його на токени, обираємо гілку switch, перетворюємо рядки на числа чи ID і працюємо з колекціями.
Нижче — кілька невеликих фрагментів у стилі LibraryCLI, де брейкпоінти дають максимальну користь.
Спочатку — токенізація. Класика жанру: зайві пробіли, порожній рядок, неочікуваний ввід:
import Foundation
func tokenize(_ line: String) -> [Substring] {
let tokens = line.split(separator: " ")
return tokens // брейкпоінт: перевірити tokens і tokens.count
}
print(tokenize("add 10 Swift")) // ["add", "10", "Swift"]
Потім — вибір команди. Навіть якщо ви ще не зробили ідеальний парсер, брейкпоінт на switch миттєво показує, у яку гілку ви реально потрапили, а не в яку, як вам здається, мали потрапити:
import Foundation
func commandName(from tokens: [Substring]) -> String {
let name = tokens.first.map(String.init) ?? ""
return name // брейкпоінт: переконатися, що name не порожній
}
let tokens = "list".split(separator: " ")
print(commandName(from: tokens)) // list
І, нарешті, «улюблена помилка всіх часів»: індекси. Іноді застосунок падає, бо ви берете tokens[1], а користувач увів лише одне слово. У таких місцях брейкпоінт треба ставити до небезпечної операції, щоб побачити tokens.count.
import Foundation
func secondTokenOrNil(_ tokens: [Substring]) -> Substring? {
guard tokens.count > 1 else { return nil } // брейкпоінт: подивитися tokens
return tokens[1]
}
print(secondTokenOrNil(["remove"]) ?? "nil") // nil
Сенс цих «опорних точок» такий: ви не намагаєтеся зловити помилку десь наприкінці, ви ловите її там, де дані вперше викликають підозру. Це економить час сильніше, ніж будь-який героїзм.
6. Типові помилки під час роботи з брейкпоінтами та кроками
Помилка № 1: брейкпоінт «не спрацьовує», бо виконання туди не дійшло.
Зазвичай причина не в тому, що налагоджувач зламався, а в тому, що програма пішла іншою гілкою (if/switch), вийшла раніше через return або взагалі не дійшла до потрібної функції. У таких випадках ставте брейкпоінт ближче до входу у функцію й рухайтеся до проблеми поступово.
Помилка № 2: Step Into перетворює налагодження на екскурсію стандартною бібліотекою.
Новачок натискає Step Into на будь-якому рядку й раптом опиняється всередині Array/String/split, де все виглядає неприродно. Щоб не загрузнути, починайте зі Step Over і заходьте всередину лише свого коду, коли маєте конкретну підозру щодо функції.
Помилка № 3: нескінченні Step Over усередині циклу замість умовної зупинки.
Якщо баг проявляється на одній ітерації з тисячі, ви не зобовʼязані проходити всі ітерації разом із налагоджувачем. Умовний брейкпоінт — нормальний, дорослий інструмент, який економить години й нерви.
Помилка № 4: спроба «зрозуміти все» за одну зупинку.
Налагодження майже завжди швидше, якщо ви перевіряєте одну гіпотезу за раз: «які токени вийшли?», потім «у яку гілку switch ми зайшли?», потім «яке значення намагаємося перетворити на Int?». Коли ви намагаєтеся одночасно тримати в голові все, налагоджувач стає не помічником, а генератором стресу.
Помилка № 5: ви виправляєте симптом, а не причину, бо не подивилися на крок виконання.
Іноді хочеться поставити ?? 0 або «якщо що, повернемо порожній рядок» — і програма перестає падати. Але це може бути лише маскуванням. Покрокове налагодження добре тим, що показує, де саме дані стали неправильними, і дає змогу виправити перший хибний крок, а не його наслідки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ