JavaRush /Курси /Swift SELF /Брейкпоінти й покрокове налагодження

Брейкпоінти й покрокове налагодження

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

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
Виконує рядок цілком, не заходячи у виклики Ви перевіряєте, як змінюються змінні на рівні поточної функції «Провалююся у стандартну бібліотеку й гублю нитку»
Step Into
Заходить усередину викликаної функції Ви підозрюєте баг усередині цієї функції (бажано вашої) «Натиснув випадково, опинився в чужому коді»
Step 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 або «якщо що, повернемо порожній рядок» — і програма перестає падати. Але це може бути лише маскуванням. Покрокове налагодження добре тим, що показує, де саме дані стали неправильними, і дає змогу виправити перший хибний крок, а не його наслідки.

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