Проблема: проверки в начале функции превращаются в вложенность
Представьте охранника на входе в клуб: сначала он отсекает тех, кому нельзя, — нет документа, младше 18, нет билета. Тех, кто прошёл все проверки, он просто пропускает внутрь, и дальше о них можно не думать. Если бы вместо этого охранник заводил каждого в отдельную комнату и уже там решал, что делать дальше, — с каждой новой проверкой таких комнат становилось бы всё больше, одна внутри другой. Ровно то же самое происходит с кодом, если все проверки на входе в функцию писать через if: с каждым новым условием появляется ещё один уровень вложенности.
func priceLabel(for price: Int?) -> String {
if let price = price {
if price >= 0 {
return "Цена: \(price)"
} else {
return "Некорректная цена"
}
} else {
return "Цена не указана"
}
}
Код рабочий, но читать его неудобно: чтобы понять, что вообще происходит в «счастливом» случае — когда цена есть и она неотрицательная, — нужно продраться через два уровня вложенности. А ведь это всего две проверки; на практике их бывает пять-шесть.
guard: сначала отсекаем, потом работаем
В Swift для таких предварительных проверок есть специальная конструкция — guard (буквально «страж» — та же идея, что и с охранником в клубе). Она проверяет условие и, если оно ложно, сразу выполняет блок else. Синтаксис:
guard условие else {
// что делать, если условие ложно
}
Перепишем предыдущую функцию через guard:
func priceLabel(for price: Int?) -> String {
guard let price else {
return "Цена не указана"
}
guard price >= 0 else {
return "Некорректная цена"
}
return "Цена: \(price)"
}
Вложенности не осталось: каждая проверка стоит на своём месте отдельной строкой, а последняя строка функции — это как раз тот самый «счастливый путь», ради которого функция и написана. Такой приём называется ранним выходом (early return, early exit): вы сразу выходите из функции при любой проблеме, а всё, что написано после последнего guard, гарантированно работает с уже проверенными, корректными данными.
Обязательный выход из else — это требование компилятора
У guard есть жёсткое правило, за которым следит компилятор: блок else обязан завершить выполнение той области видимости, в которой написан guard. Для функции это чаще всего return. Если guard написан внутри цикла — вместо return допустимы continue (перейти к следующему шагу цикла) или break (выйти из цикла); если функция объявлена как выбрасывающая ошибку, можно выбросить её через throw. С циклами и обработкой ошибок подробно разберёмся отдельно — здесь достаточно знать, что это тоже допустимые способы выйти.
func half(of number: Int) -> Int {
guard number % 2 == 0 else {
return -1 // без выхода компилятор откажется собирать код
}
return number / 2
}
Если убрать return -1 и оставить блок else пустым, компилятор не соберёт программу. Смысл guard в том, чтобы гарантировать: если исполнение дошло до строки после guard, значит условие точно истинно. Компилятор не может дать такую гарантию, если после проверки, оказавшейся ложной, работа функции спокойно продолжится, — поэтому он и требует явного выхода.
guard let: значение живёт до конца функции
С разворачиванием опционалов вы уже знакомы: if let достаёт значение из опционала, но эта константа существует только внутри блока if. У guard let — принципиально другая область видимости: развёрнутое значение доступно от точки объявления и до конца функции (или блока, в котором написан guard), а не только внутри какого-то вложенного блока.
func describe(nickname: String?) {
if let nickname {
print("Внутри if: \(nickname)")
}
// print(nickname) — так нельзя, за пределами if let значение недоступно
guard let nickname else {
return
}
print("После guard: \(nickname)") // работает
print("И тут тоже: \(nickname.count) символов") // и здесь тоже работает
}
Это и есть ключевое отличие двух конструкций. if let удобна, когда развёрнутое значение нужно только для одного конкретного действия внутри ветки. guard let удобна, когда развёрнутое значение нужно во всём остальном коде функции, — тогда не приходится заворачивать всю оставшуюся логику в блок if.
Несколько guard подряд
guard прекрасно выстраивается цепочкой: каждая следующая проверка полагается на то, что все предыдущие уже прошли успешно.
func validatePassword(_ password: String) -> String {
guard !password.isEmpty else {
return "Пароль не может быть пустым"
}
guard password.count >= 6 else {
return "Пароль должен быть не короче 6 символов"
}
guard password.count <= 20 else {
return "Пароль слишком длинный"
}
return "Пароль принят"
}
Каждая строка отвечает ровно за одну проверку и ровно за одно сообщение об ошибке. Представьте эту же функцию с вложенными if: три проверки — это уже три уровня вложенности, и добавить четвёртую проверку станет заметно неудобнее, чем дописать ещё один guard.
guard или if: когда что выбирать
guard и if внешне похожи, но решают разные задачи, и путать их не стоит.
guard — для предварительных условий. Он отвечает на вопрос «можно ли вообще продолжать?». Если условие не выполнено — это не один из нормальных исходов, а проблема, из-за которой нет смысла выполнять остальной код: неверные входные данные, отсутствующее значение, недопустимое состояние.
if (и else if) — для выбора между равноправными вариантами. Если у вас есть несколько путей, каждый из которых — нормальный, ожидаемый исход, а не ошибка, guard не подойдёт: у него по смыслу только один «правильный» путь — тот, что продолжается после него, а всё остальное — досрочный выход. Например, если функция должна вести себя по-разному для трёх типов пользователей и все три варианта одинаково законны, это классический if/else if, а не цепочка guard.
| Ситуация | Что использовать |
|---|---|
| Отсеять некорректные данные в начале функции | guard |
| Выбрать одно из нескольких равноправных действий | if / else if |
| Развернуть опционал и использовать значение до конца функции | guard let |
| Развернуть опционал только для одного действия внутри ветки | if let |
Частые ошибки
- Забыть выход из блока else — компилятор укажет на ошибку, но начинающие иногда пытаются исправить её, убирая guard целиком, вместо того чтобы добавить return.
- Пытаться использовать значение из guard let за пределами той функции или блока, где написан guard, — область видимости привязана к тому уровню кода, где стоит guard, а не расширяется на весь файл.
- Строить через guard выбор между несколькими равноправными вариантами — для этого предназначен if/else if, а не цепочка проверок с выходом.
- Писать guard там, где условие и так уже гарантированно истинно, например сразу после проверки того же самого условия, — лишняя проверка не ошибка, но и не нужна.
Резюме
guard условие else { ... }проверяет условие и выполняет блок else, если оно ложно.- Блок else у guard обязан завершить область видимости: return, continue, break или throw — компилятор не соберёт код без явного выхода.
- Приём «ранний выход»: сначала отсекаем все некорректные случаи через guard, дальше пишем основной код без вложенности.
guard letразворачивает опционал и делает значение доступным до конца функции;if let— только внутри своего блока.- guard — для предварительных условий и отсева ошибочных случаев; if/else if — для выбора между несколькими равноправными вариантами.
Проверьте себя
3 вопроса
Проверка возраста
Реализуйте функцию checkAge(_ age: Int) -> String. Если age меньше 0 или больше 120 — верните строку "Некорректный возраст". В остальных случаях верните строку "Возраст принят: ЧИСЛО", подставив значение возраста через интерполяцию. Используйте guard с ранним выходом, а не вложенный if.
Проверка пароля по правилам
Реализуйте функцию validatePassword(_ password: String) -> String, которая проверяет пароль цепочкой guard, по порядку:
- если пароль пустой — верните
"Пароль не может быть пустым"; - если короче 6 символов — верните
"Пароль должен быть не короче 6 символов"; - если длиннее 20 символов — верните
"Пароль слишком длинный"; - если пароль равен ровно
"123456"— верните"Слишком простой пароль"; - если ни одна проверка не сработала — верните
"Пароль принят".
Оформите каждую проверку отдельным guard, чтобы дальнейший код полагался на то, что все предыдущие проверки уже пройдены.
Цена билета в кино
Реализуйте функцию ticketPrice(age: Int, discountCode: String?) -> Int, которая считает стоимость билета в кино. Полная цена — 500. Правила применяются по порядку:
- если
ageотрицательный — верните-1(некорректный возраст); - детям младше 7 лет (то есть
ageот 0 до 6 включительно) билет бесплатный — верните0, промокод в этом случае не важен; - если промокод (
discountCode) не передан, то есть равенnil, — верните полную цену500; - если промокод передан, но это не строка
"STUDENT"(в любом другом регистре или виде) — верните полную цену500; - если промокод — ровно
"STUDENT"— верните400.
Используйте guard там, где нужно отсечь недопустимые или отсутствующие значения.