JavaRush /Курсы /Swift SELF /Приоритет операторов и скобки: избегаем ошибок логики

Приоритет операторов и скобки: избегаем ошибок логики

Swift SELF
3 уровень , 4 лекция
Открыта

1. Базовые правила приоритета в условиях

Когда мы пишем условие в if, кажется, что оно читается как обычная фраза: «если A или B и C…». Проблема в том, что компилятор — существо бесстрастное: он не делает паузы «по смыслу», не догадывается о ваших намерениях и не спрашивает «а вы точно это хотели?». Он просто применяет правила, по которым одни операторы связываются сильнее других.

Из‑за этого легко получить ситуацию: выражение написано без ошибок, выглядит логично, даже тест на одном-двух примерах «как будто проходит», но в реальных данных ведёт себя странно. Это и есть знаменитый баг «правильный синтаксис, неправильная логика». Хорошая новость: лечится скобками и привычкой делать условия читаемыми.

В Swift (как и в большинстве языков) операторы имеют приоритет: какие-то выполняются «раньше» и сильнее группируют выражение. Полная таблица — это справочник, который никто не носит в голове целиком (кроме людей, которые однажды поссорились со скобками и решили отомстить). Нам сейчас нужна практичная версия — чтобы не ошибаться в условиях.

Ниже — полезная «карта местности» для сегодняшнего дня. Это не все операторы Swift, но ровно те, что чаще всего участвуют в логике условий:

Группа Примеры Кто “сильнее” внутри группы Комментарий
Арифметика
*, /, %, +, -
* / %
сильнее, чем
+ -
Это вы уже частично видели на теме арифметики.
Сравнения
<, <=, >, >=, ==, !=
примерно один уровень (важнее скобки) Результат сравнения — Bool.
Логическое НЕ
!
самое сильное в логике Применяется к ближайшему Bool-фрагменту.
Логическое И
&&
сильнее, чем
||
Частая ловушка: “или” читается раньше человеком, но не компилятором.
Логическое ИЛИ
||
слабее, чем
&&
Если смешали
&&
и
||
— почти всегда просится
(...)
.

Главное правило на сегодня можно запомнить одной строкой: в логике сначала !, потом &&, потом ||.

А если в выражении одновременно есть && и ||, то скобки — не роскошь, а средство гигиены.

Ловушка: a || b && c — это не «как по‑человечески»

Представьте, что вы говорите: «пущу в клуб, если человек VIP или он взрослый и у него есть билет». По‑человечески многие читают это как «(VIP или взрослый) и билет», потому что билет кажется обязательным. Но компилятор не читает по‑человечески: он сначала «склеит» b && c, а потом уже применит ||.

Давайте посмотрим на это на чистом Bool, чтобы не мешали ввод и числа:

let a = true
let b = false
let c = false

let r1 = a || b && c
let r2 = (a || b) && c

print("r1 = \(r1)") // r1 = true
print("r2 = \(r2)") // r2 = false

Почему так? Потому что a || b && c компилятор читает как a || (b && c). Сначала считает b && c, получает false, а потом a || false даёт true.

И вот тут начинается бытовая трагедия: вы ожидали, что «без c (например, без билета) не пускаем», а по факту «если a true, то пускаем вообще без всего». Никаких ошибок компиляции не будет. Код «правильный». Логика — внезапно нет.

Отрицание ! и скобки: «не то отрицал»

Оператор ! кажется простым: «инвертировать true/false». Но как только рядом появляется && или ||, очень легко начать отрицать «не ту часть», которую вы имели в виду. Особенно если вы мысленно поставили скобки, но руками их не написали.

Сравните два выражения: «не (A и B)» и «(не A) и B». Это разные условия.

let a = true
let b = true

let x1 = !a && b
let x2 = !(a && b)

print("x1 = \(x1)") // x1 = false
print("x2 = \(x2)") // x2 = false

В этом конкретном наборе значений оба результата одинаковые, и это ещё одна ловушка: кажется, что «работает». Но давайте поменяем данные:

let a = false
let b = true

let x1 = !a && b
let x2 = !(a && b)

print("x1 = \(x1)") // x1 = true
print("x2 = \(x2)") // x2 = true

Снова одинаково. «Ну и где разница?» — спросит мозг, который хочет жить без скобок. А разница проявится на других значениях:

let a = true
let b = false

let x1 = !a && b
let x2 = !(a && b)

print("x1 = \(x1)") // x1 = false
print("x2 = \(x2)") // x2 = true

Вот теперь видно: !a && b — это «A ложь и B истина». А !(a && b) — это «не выполняется одновременно A и B», то есть «хотя бы одно из них false». В бытовой логике это часто разные смысловые вещи.

Скобки тут — способ сказать компилятору (и человеку после вас): что именно мы отрицали.

Скобки как способ показать смысл

Многие новички думают, что скобки ставят только тогда, когда без них выражение вычислится неправильно. На практике это половина правды. Вторая половина: скобки ставят, чтобы выражение было невозможно прочитать двояко.

То есть иногда результат и так будет «как надо», но без скобок код выглядит как загадка: «это a || (b && c) или (a || b) && c?». Если у читателя есть хоть одна секунда сомнения — значит, код уже просит скобки или разбиение на переменные. Потому что следующий читатель — это чаще всего вы, но через неделю, в плохом настроении.

И да, компилятор не нуждается в скобках «для красоты». А вот люди — очень даже.

Как компилятор группирует выражение

Когда условие длинное, полезно мысленно представить, как компилятор строит дерево вычислений: какие куски считаются первыми, какие — потом. Это можно понимать без сложной теории, просто как «вложенность».

Вот условная схема для a || b && c:

flowchart TD
    A[a] --> OR["||"]
    B[b] --> AND[&&]
    C[c] --> AND
    AND --> OR
    OR --> R[результат]

Смысл диаграммы простой: b && c образуют «комок» раньше, чем || соединит это с a. Скобки в коде буквально меняют структуру этого дерева.

3. Практика: условие «фейсконтроля»

Чтобы это было не абстрактным a/b/c, давайте продолжим наш «мини-CLI» стиль: программа читает ввод, вычисляет признаки (Bool), печатает решение.

Допустим, у нас есть правила входа на мероприятие:

  • Если человек в бан‑листе (isBanned == true) — не пускаем никогда.
  • Иначе пускаем, если он VIP или если он взрослый и с билетом.

Вот тут как раз живёт классический микс || и &&, где без скобок легко ошибиться.

Сначала аккуратно соберём данные из консоли. Мы пока не делаем «нормальную валидацию» через if let (это будет позже), поэтому используем знакомые дефолты через ??.

let age = Int(readLine() ?? "") ?? 0
let ticketText = readLine() ?? ""   // ожидаем "yes" или "no"
let vipText = readLine() ?? ""      // ожидаем "yes" или "no"
let banText = readLine() ?? ""      // ожидаем "yes" или "no"

let hasTicket = ticketText == "yes"
let isVIP = vipText == "yes"
let isBanned = banText == "yes"
let isAdult = age >= 18

print("isAdult=\(isAdult), hasTicket=\(hasTicket), isVIP=\(isVIP), isBanned=\(isBanned)")

«Опасная» версия: читается с усилием

let canEnterBad = !isBanned && isVIP || isAdult && hasTicket
print("canEnterBad = \(canEnterBad)")

Этот код компилируется, но давайте честно: прочитать его уверенно без паузы сложно. Где тут какая «группа»? Что важнее: бан, VIP или билет? А теперь — «как думает компилятор»?

По приоритету это читается так:

  • !isBanned сначала (потому что ! сильнее),
  • потом группируется !isBanned && isVIP,
  • отдельно группируется isAdult && hasTicket,
  • потом применяется || между этими двумя кусками.

То есть фактически:

let canEnterBad = (!isBanned && isVIP) || (isAdult && hasTicket)

Это может совпасть с вашим смыслом, а может и нет — и именно это слово «может» делает строку опасной.

Версия, которую невозможно понять двояко

let canEnter = (!isBanned) && (isVIP || (isAdult && hasTicket))
print("canEnter = \(canEnter)")

Здесь намерение максимально ясное: бан — «фильтр сверху», а внутри уже решаем «VIP или взрослый+билет».

Ещё лучше: вынести смысловые части в let

Скобки помогают, но если условие растёт, легко впасть в другой грех: «скобочный арт». Код будет правильный, но визуально тяжёлый, и вы начнёте путаться, где какие скобки закрываются.

В таких случаях более взрослый (и более читаемый) стиль — вынести смысловые части в let с нормальными именами. Это не «лишние строки», это нормальная плата за то, чтобы мозг не перегревался.

let isAllowedByStatus = isVIP || (isAdult && hasTicket)
let canEnter = (!isBanned) && isAllowedByStatus

print("isAllowedByStatus = \(isAllowedByStatus)")
print("canEnter = \(canEnter)")

Теперь условие читается почти как обычный текст, и у вас появляется бонус: вы можете отдельно распечатать isAllowedByStatus и увидеть, какая часть правила сработала. Это помогает при отладке даже на ранних темах.

4. Short-circuit и приоритет: когда «предохранитель» не спасает

Короткая проверка (short-circuit) из прошлой лекции — мощная штука: мы ставим безопасную проверку слева, и правая часть не вычисляется, если слева уже всё решено.

Но тут есть тонкость: предохранитель должен быть слева в той группе, которая реально вычисляется первой. А если вы смешали && и || без скобок, вы можете случайно сгруппировать выражение так, что «предохранитель» окажется не там, где вы думали.

Например, вы хотите: «если делитель не ноль, и либо A, либо (B делится на этот делитель)». Если написать неаккуратно, можно получить неожиданное вычисление.

Правильный и безопасный стиль:

let divisor = Int(readLine() ?? "") ?? 0
let value = Int(readLine() ?? "") ?? 0
let flagText = readLine() ?? ""          // "yes"/"no"
let isSpecial = flagText == "yes"

let canCheckDivision = divisor != 0
let condition = canCheckDivision && (isSpecial || value % divisor == 0)

print("condition = \(condition)")

Тут divisor != 0 гарантированно стоит слева от &&, а значит, если divisor == 0, правая часть в скобках вообще не будет вычисляться, и value % divisor не случится.

Если же начать «экономить скобки» и смешивать || и && на глаз, легко получить ситуацию, когда вы думаете, что divisor != 0 защищает всё выражение, а оно защищает только часть — и вторая часть всё равно вычисляется.

5. Типичные ошибки

Ошибка №1: смешали && и || без скобок и прочитали «как по‑русски».
Человек часто мысленно ставит скобки «по смыслу», а компилятор ставит их «по приоритету». В результате выражение компилируется и даже иногда работает на тестовых значениях, но ломается на реальных кейсах. Лечится просто: если в одном условии есть и &&, и ||, почти всегда стоит явно сгруппировать части скобками или вынести куски в let.

Ошибка №2: отрицание применяется не к тому, что вы хотели отрицать.
Запись !a && b выглядит похоже на !(a && b), особенно если вы быстро пробежались глазами. Но это разные условия. Если вы отрицаете «связку» — всегда пишите !(...). Скобки в отрицании — это не украшение, а точное указание смысла.

Ошибка №3: «скобочный арт» вместо читаемости.
Иногда после осознания важности скобок хочется обложить ими всё: ((((a)))) && (((b)) || ((c)))). Формально это работает, но читать тяжело, а тяжело читаемый код чаще содержит ошибки. Лучше группировать только смысловые части и дополнять это промежуточными булевыми переменными с говорящими именами.

Ошибка №4: пытаются сделать «одно условие на всё» прямо внутри if.
Когда в if попадает выражение на полэкрана, шанс ошибиться в приоритете резко растёт. Хороший стиль — заранее собрать несколько let is... и уже их склеить в финальное условие. Это не «многословность», а способ сделать логику проверяемой и объяснимой.

Ошибка №5: думают, что скобки нужны только если результат меняется.
Иногда результат действительно не меняется (например, потому что конкретные значения сейчас такие), но код становится двусмысленным для читателя. Скобки полезны не только для изменения вычисления, но и для того, чтобы намерение было очевидно сразу, без мысленного дебага в голове.

1
Задача
Swift SELF, 3 уровень, 4 лекция
Недоступна
Два расчёта
Два расчёта
1
Задача
Swift SELF, 3 уровень, 4 лекция
Недоступна
Три тумблера
Три тумблера
1
Задача
Swift SELF, 3 уровень, 4 лекция
Недоступна
Допуск на вход
Допуск на вход
1
Задача
Swift SELF, 3 уровень, 4 лекция
Недоступна
Предохранитель деления
Предохранитель деления
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ