Пульт на четыре команды через Serial я собрал на if-лестнице: if (cmd == '1') … else if (cmd == '2') … и так четыре раза, по восемь строк со сравнением одной и той же переменной. На третьей ветке в одну из скобок уехала лишняя строка, и режим «выкл» тихо отработал как «мигнуть». Код работал «почти как надо» — худший из возможных статусов для скетча.
Для случая «одна переменная, много значений» в языке есть switch: каждая ветка — case с меткой, сверху не требуется повторять имя переменной, и глаз сразу видит карту команд. Для выбора значения в одну строку есть тернарный оператор cond ? a : b. Оба — не замена if, а инструменты под конкретную форму ветвления, и перепутать их сферы легко именно на первых проектах.
Базовое ветвление if/else мы проходили в уроке об условиях, а готовые скелеты на switch уже есть: режимы устройства и пульт по Serial. Здесь — как выбирать между if, switch и тернарником, и куда стреляет забытый break.
Оба инструмента старые: switch пришёл из чистого Си, тернарник — тоже, и компилятор превращает оба в одни и те же машинные ветвления. Экономии тактов они не дают — дают читаемость, которая на третьем режиме уже расходится с if-лестницей в разы. Ниже — границы каждого и грабли, которые у них общие.
switch: одна переменная — много case
switch (cmd) { case '1': … break; case '2': … break; default: … } — сравнение одной переменной со списком меток. Работа идёт с целыми и с char: режимы 0-1-2, коды клавиш, принятые символы команд. Строки switch не сравнивает — для текстов остаются if с strcmp или String.equals. Метки case — константы: числа, символы, имена из enum и #define.
Каждая ветка заканчивается break: он выходит из switch и не даёт выполнению упасть в следующий case. Ветка default ловит всё, что не совпало ни с одной меткой — «неизвестная команда», «безопасный режим»; ставлю её последней и с break, хотя формально последней ветке он не нужен: при добавлении нового case снизу привычка спасает от сюрпризов.
Когда switch читабельнее if-лестницы — вопрос формы условий, а не вкуса: три и больше веток по одной переменной с константами — территория switch. Если же условия разные по смыслу (svet < 350, knopka == LOW, flag) — switch не подходит в принципе, это работа для if. Сравнение одной переменной на 3+ значений в if превращается в простыню повторов, где промах со скобкой дешевле, чем кажется.
Диапазоны switch не берёт: case от 1 до 5 одной меткой не записывается, и для «при температурах ниже X делай Y» остаётся if. Метки обязаны быть константами времени компиляции — числа, символы, имена из enum; переменная в качестве метки не скомпилируется. По скорости на AVR большой разницы между switch и цепочкой if нет: при плотных метках компилятор строит таблицу переходов, при редких — ту же цепочку сравнений, так что выбирать стоит по читаемости.
const int PIN_LED = 12; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("1 = svet, 2 = vspyshka, 0 = vykl");} void loop() { if (Serial.available() > 0) { char cmd = Serial.read(); switch (cmd) { case '1': digitalWrite(PIN_LED, HIGH); Serial.println("svet vklyuchen"); break; case '2': digitalWrite(PIN_LED, HIGH); delay(120); digitalWrite(PIN_LED, LOW); Serial.println("odna vspyshka"); break; case '0': digitalWrite(PIN_LED, LOW); Serial.println("vykl"); break; default: Serial.println("neizvestnaya komanda"); break; } }}В скетче выше команды — одиночные символы: Serial.read() отдаёт байт, и сравнивать его с '1' или '0' удобно прямо в метках case. char здесь не причуда: команды из монитора порта приходят посимвольно, и switch по байту — самый короткий маршрут от ввода к действию. Для многосимвольных команд («led on») сначала придётся собрать строку — это уже техника из уроков о чтении порта.
Обратите внимание на default: без него чужой символ из порта просто молча игнорировался бы, и отладка превратилась бы в гадание. Ветка печатает, что команда не распознана, — и пульт становится самодиагностируемым. Тот же приём стоит в каждом switch по данным извне: печать в default — ваша страховка от «почему-то не срабатывает».
Читаемость на switch видна и в правках: добавить пятую команду — одна пара «метка-тело», скобки не трогаются, else-цепочка не перещёлкивается. Ниже — тот же пульт на if-лестнице: посчитайте строки и сравните, где промах со скобкой вероятнее.
Сравните два кода и по числу повторов переменной: на switch cmd упомянута один раз в заголовке, на лестнице — четыре раза, в каждой ветке. Это не эстетика: чем больше повторов, тем больше мест, где легко промахнуться символом сравнения или потерять скобку. При четырёх ветках и выше я считаю это техническим решением, а не вкусовщиной; ниже трёх лестница даже честнее switch — меньше церемоний ради одной проверки.
void loop() { if (Serial.available() > 0) { char cmd = Serial.read(); if (cmd == '1') { digitalWrite(PIN_LED, HIGH); Serial.println("svet vklyuchen"); } else if (cmd == '2') { digitalWrite(PIN_LED, HIGH); delay(120); digitalWrite(PIN_LED, LOW); Serial.println("odna vspyshka"); } else if (cmd == '0') { digitalWrite(PIN_LED, LOW); Serial.println("vykl"); } else { Serial.println("neizvestnaya komanda"); } }}Тернарный оператор: выбор значения в одну строку
Запись uslovie ? znachenie1 : znachenie2 — выражение, у которого есть значение: при истинном условии всё выражение равно первому, при ложном — второму. В этом его отличие от if: if — оператор действия, он выполняет блок; тернарник — выбирает значение, и потому влезает туда, где блок не встроишь. digitalWrite(PIN_LED, gori ? HIGH : LOW) — выбор аргумента, int porog = den ? 620 : 350 — выбор начального значения.
Удобство имеет узкие границы. Мой предел — один тернарник на строку и никакого вложенного: uslovie1 ? a : uslovie2 ? b : c читается со словарём и диаграммой. Выбор действия с побочкой — две-три команды в ветке — не тернарник, а if: попытка упаковать это в одну строку приносит читателю боли больше, чем экономит строк. Про совместную работу тернарника и флагов состояний подробнее разобрано в уроке о тернарном операторе.
По машинному коду разницы между if и тернарником нет: компилятор генерирует одно и то же, выбор — про читаемость, а не про скорость. Внутри выражения тернарник уместен в аргументе функции, в инициализации константы, в печати; вылезание за эти рамки — сигнал, что пора писать if.
Печать — излюбленное место тернарника: Serial.print(gori ? «on» : «off») заменяет три строки с if и делает лог читаемым построчно. Второй любимый слот — константы режима в начале витка: const int porog = den ? 620 : 350; — одна строка, в которой видны оба порога сразу. Всё, что длиннее этих двух шаблонов, у меня честно перерастает в if.
| Свойство | if / else | switch | Тернарник ? : |
|---|---|---|---|
| Что проверяет | любые условия, разные по смыслу | одну переменную против констант | одно условие |
| Сколько веток читабельно | 2–3, дальше простыня | 4–10 без потери формы | ровно одна |
| Возвращает значение | нет — оператор действия | нет — оператор действия | да — выражение |
| Провал в соседнюю ветку | невозможен | без break — да | невозможен |
| Место в моих скетчах | условия-диапазоны, флаги | меню, режимы, команды | выбор аргумента в строку |
| Диапазоны (svet < 350) | да — родная территория | нет, только константы | нет, одно условие |
Забытый break — самая частая грабля конструкции: выполнение не выходит из switch, а проваливается в следующий case и выполняет его код. В моём пультовом скетче отсутствие break после case '1' означало: свет включился, затем отработала ветка «вспышка», затем «выкл» — три режима подряд за один прогон. Диагноз ставится по одному симптому: «последняя ветка всегда срабатывает» — значит, где-то выше дырка в break.
Провал бывает и полезен, но редко: пустые case '1': case '2': подряд означают общую ветку для двух значений — это законно и читается, если пометить комментарием. Всё остальное — непреднамеренный провал, и в новых версиях компиляторы даже предупреждают о case, которые падают в соседний без комментария. Правило простое: провал пишется явно (пустой case), всё остальное — break в каждой ветке.
На коде меню разница подходов видна в строках: if-лестница на четыре команды — восемь строк со сравнениями и промежуточными else, switch — четыре пары «метка-тело» и default. Добавление пятой команды: в switch — новый case, в лестнице — ещё один else if и проверка, что цепочка не разорвалась. По-прежнему помню меню на двух кнопках и конечный автомат на светофоре — оба каркаса собраны на этом же выборе веток, и оба пережили добавление режимов без перестройки.
Меню в симуляторе собрано ровно на разборе из этого урока: переменная rezhim, четыре case и default, тернарник в финальной строке. ШИМ-режим отделён условием if (rezhim != 2) — analogWrite и digitalWrite делят один пин, и смешивать их в одном проходе не стоит: последний записанный уровень побеждает. Такое разделение — обычная плата за общий выход, и обёртка решает её в одну строку.
Отдельный случай — switch внутри цикла: break в switch не выходит из цикла, а только из ветки. Классический промах — for по массиву режимов, где break задумывался как выход из цикла, а останавливает лишь выбор ветки. Для выхода из цикла на глубине switch оставляют флаг или выносят виток в функцию с return — там return уходит из всей функции сразу.
- 1. Что произойдёт, если убрать break из ветки case?
- 2. С какими значениями switch работает, а с какими нет?
- 3. Что даст выражение uroven > 128 ? HIGH : LOW при uroven = 200?
- 4. Зачем в switch нужна ветка default?
- 5. Чем тернарный оператор отличается от if?
- switch сравнивает одну переменную с константами: целые и char, строки не умеет — для них if и strcmp.
- break завершает ветку; без него выполнение проваливается в следующий case — «включился и тут же выключился».
- default ловит всё непойманное: ставлю последней и с break — на случай добавления новых case.
- Тернарник uslovie ? a : b — выражение со значением: выбор аргумента или инициализации в одну строку.
- Рубеж выбора: 3+ ветки по одной переменной — switch; разные по смыслу условия — if; выбор из двух значений — тернарник, один на строку.
Частые вопросы по теме урока
- Что произойдёт, если убрать break из ветки case?
- Без break выполнение падает в следующий case и идёт до ближайшего break или конца switch. Легальный провал пишется пустыми case подряд, всё остальное — грабля. выполнение провалится в следующий case и выполнит его тоже
- С какими значениями switch работает, а с какими нет?
- Метки case — целые константы и символы. Тексты switch не сравнивает: для строк остаются if со strcmp или String.equals. с целыми и char, со строками — нет
- Что даст выражение uroven > 128 ? HIGH : LOW при uroven = 200?
- 200 > 128 истинно, выражение принимает первое значение — HIGH. При ложном условии тернарник отдаёт значение после двоеточия. HIGH
- Зачем в switch нужна ветка default?
- default — ветка «всё остальное»: неизвестная команда, мусор из порта, невалидный режим. Ставлю её последней и с break — на случай добавления case снизу. ловит значения, не совпавшие ни с одной меткой
- Чем тернарный оператор отличается от if?
- Тернарник возвращает значение, поэтому влезает в выражения: int porog = den ? 620 : 350. if — оператор действия. По машинному коду разницы нет. это выражение со значением — его можно вставить в присваивание или аргумент