Управляющие конструкции и операторы Dart
О чём эта статья
if/else, циклы, switch (в т.ч. expression), null-aware операторы. Углубление паттернов — Dart 3.
Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.
Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.
Управляющие конструкции и операторы
Play ITЗагрузка интерактивного демо…
Условные конструкции
Условные конструкции позволяют выполнять различные блоки кода в зависимости от истинности или ложности заданного выражения. В Dart основной конструкцией этого типа является if–else.
Конструкция if начинается с ключевого слова if, за которым следует выражение в круглых скобках. Если результат этого выражения приводится к логическому значению true, выполняется следующий за ним блок кода. Если значение выражения равно false, управление переходит к необязательному блоку else. Конструкция может содержать цепочку else if, что позволяет реализовать множественный выбор.
Пример:
int score = 85;
if (score >= 90) {
print('Отлично');
} else if (score >= 75) {
print('Хорошо');
} else if (score >= 60) {
print('Удовлетворительно');
} else {
print('Неудовлетворительно');
}
Разбор:
scoreхранит числовой результат экзамена.- Цепочка
if / else if / elseпроверяет диапазоны от большего порога к меньшему. - Условие в
if (...)обязано быть типаbool— неявных преобразований нет. - Каждая ветка печатает текстовую оценку для соответствующего диапазона.
Dart строго типизирован и не допускает неявного приведения типов к булевому значению. Это означает, что выражение внутри if должно быть явно булевым. Попытка использовать, например, целое число в качестве условия вызовет ошибку компиляции. Такой подход повышает надежность кода и предотвращает распространенные ошибки, характерные для других языков.
Для выбора по значению переменной используют switch. В Dart запрещён fall-through между case — каждая ветка должна завершаться (break, return, throw или continue). В Dart 3 оператор break в конце ветки обычно не нужен — компилятор сам завершает ветку.
Классический оператор switch:
enum Color { red, green, blue }
void describeColor(Color color) {
switch (color) {
case Color.red:
print('Цвет — красный');
case Color.green:
print('Цвет — зелёный');
case Color.blue:
print('Цвет — синий');
}
}
Разбор:
enum Colorзадаёт фиксированный набор значений дляswitch.- Каждая ветка
case Color.redобрабатывает один вариант enum. - В Dart 3
breakв конце ветки обычно не нужен — fall-through запрещён. - Функция
describeColorпечатает подпись для переданного значения enum.
Switch expression (Dart 3) — компактная форма, результат можно присвоить переменной:
String label(Color color) => switch (color) {
Color.red => 'красный',
Color.green => 'зелёный',
Color.blue => 'синий',
};
Разбор:
- Это
switch expression: результат выражения сразу возвращается функцией. - Форма
значение => результатделает ветки компактными. - Все варианты
enumдолжны быть покрыты, иначе анализатор предупредит. - Удобно присваивать результат переменной:
final text = label(Color.red);.
Pattern matching расширяет switch: можно сопоставлять записи, списки и поля объектов. Для исчерпывающего разбора вариантов удобны sealed class и расширенные enum — компилятор предупредит, если забыли ветку. Подробнее: паттерны и switch в Dart 3.
Интерактивное демо — пошаговый цикл на примере JavaScript (
for,while). В Dart синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.
Play ITЗагрузка интерактивного демо…
Циклы
Циклы предназначены для многократного выполнения блока кода. Dart предоставляет три основных вида циклов: for, while и do–while.
Цикл for состоит из трех частей: инициализации, условия продолжения и шага обновления. Все эти части указываются в круглых скобках после ключевого слова for. Тело цикла выполняется до тех пор, пока условие продолжения возвращает true.
Пример:
for (int i = 0; i < 5; i++) {
print('Итерация $i');
}
Разбор:
int i = 0— инициализация счётчика.i < 5— условие продолжения цикла.i++— шаг на каждой итерации.$iв строке подставляет текущее значение счётчика.
Dart также поддерживает расширенную форму цикла for — так называемый цикл for-in, который используется для перебора элементов коллекций, таких как списки, множества или строки. Этот цикл автоматически извлекает каждый элемент коллекции и присваивает его переменной, объявленной в заголовке цикла.
Пример:
List<String> fruits = ['яблоко', 'банан', 'апельсин'];
for (String fruit in fruits) {
print(fruit);
}
Разбор:
for (String fruit in fruits)перебирает элементы коллекции.- На каждой итерации
fruitполучает очередное значение списка. - Тип переменной цикла (
String) должен совпадать с типом элементов. - Это идиоматичная замена индексного
forпри обходе коллекций.
Цикл while проверяет условие перед каждой итерацией. Если условие истинно, выполняется тело цикла. Если условие ложно с самого начала, тело цикла не выполнится ни разу.
Пример:
int count = 3;
while (count > 0) {
print('Осталось $count');
count--;
}
Разбор:
- Условие
count > 0проверяется перед каждой итерацией. - Тело выполняется, пока условие истинно.
count--уменьшает счётчик и приближает завершение цикла.- Если
countизначально0, тело не выполнится ни разу.
Цикл do–while отличается тем, что тело цикла выполняется как минимум один раз, поскольку проверка условия происходит после выполнения тела.
Пример:
int attempts = 0;
do {
print('Попытка номер ${attempts + 1}');
attempts++;
} while (attempts < 3);
Разбор:
- Тело
do { ... }выполняется минимум один раз. - Проверка
while (attempts < 3)идёт после выполнения тела. ${attempts + 1}показывает номер попытки с учётом нулевой индексации.- Подходит для сценариев "сначала действие, потом проверка условия повтора".
Все циклы поддерживают операторы управления потоком выполнения: break для немедленного выхода из цикла и continue для перехода к следующей итерации без выполнения оставшейся части тела цикла.
Операторы сравнения и логические операторы
Операторы сравнения позволяют оценивать отношения между двумя значениями. В Dart к ним относятся:
==— равенство;!=— неравенство;<,<=,>,>=— числовые сравнения.
Результатом любого из этих операторов является булево значение (true или false). Dart строго проверяет типы при сравнении, особенно при использовании оператора ==. Для пользовательских классов поведение оператора == можно переопределить, реализовав метод operator == и соответствующий hashCode.
Логические операторы работают с булевыми значениями и включают:
&&— логическое И (возвращаетtrue, только если оба операнда истинны);||— логическое ИЛИ (возвращаетtrue, если хотя бы один операнд истинен);!— логическое НЕ (инвертирует значение:!trueдаётfalse,!false—true).
Dart поддерживает ленивые вычисления (short-circuit evaluation) для && и ||. Это означает, что если результат выражения можно определить по первому операнду, второй операнд не вычисляется. Например, в выражении a && b(), если a равно false, функция b() не будет вызвана. Это полезно для предотвращения ошибок, таких как обращение к null.
Арифметические и побитовые операторы
Побитовые операторы (&, |, ^, ~, <<, >>) работают с битами целых чисел (маски, флаги).
Арифметические операторы выполняют базовые математические действия:
+,-,*,/— сложение, вычитание, умножение, деление;~/— целочисленное деление (возвращаетint);%— остаток от деления.
Оператор / всегда возвращает значение типа double, даже если делимое и делитель — целые числа. Для получения целого результата используется ~/.
Пример:
print(7 / 2); // 3.5
print(7 ~/ 2); // 3
print(7 % 2); // 1
Разбор:
7 / 2в Dart даёт3.5, потому что/всегда возвращаетdouble.7 ~/ 2— целочисленное деление, результат3типаint.7 % 2— остаток от деления (1).- Для целых расчётов используют
~/, а не/.
Побитовые операторы применяются к целым числам и работают на уровне двоичного представления:
&— побитовое И;|— побитовое ИЛИ;^— побитовое исключающее ИЛИ (XOR);~— побитовое НЕ (инверсия всех битов);<<— сдвиг влево;>>— сдвиг вправо.
Эти операторы полезны при работе с флагами, масками, низкоуровневыми протоколами или оптимизациях.
Операторы присваивания
Оператор присваивания = связывает переменную со значением. Dart также предоставляет составные операторы присваивания, которые объединяют арифметическую или побитовую операцию с присваиванием:
+=,-=,*=,/=,~/=,%=;&=,|=,^=,<<=,>>=.
Пример:
int x = 10;
x += 5; // эквивалентно x = x + 5;
Разбор:
int x = 10задаёт начальное значение.x += 5— составной оператор присваивания (сложение и запись).- Эквивалентная запись:
x = x + 5. - После выполнения
xстанет равен15.
Такие операторы делают код более компактным и читаемым.
Тернарный условный оператор
Тернарный оператор — это сокращённая форма условной конструкции if–else. Он имеет вид:
условие ? выражение_если_true : выражение_если_false
Разбор:
- Это шаблон тернарного оператора Dart.
- Сначала вычисляется
условиетипаbool. - Если
true— берётся левое выражение, иначе правое. - Оба выражения должны иметь совместимый тип результата.
Оба выражения должны быть совместимы по типу, так как результат тернарного оператора должен иметь однозначный тип.
Пример:
String status = score >= 60 ? 'Сдано' : 'Не сдано';
Разбор:
score >= 60— булево условие прохождения.- При
trueвstatusпопадёт'Сдано', иначе'Не сдано'. - Результат сразу присваивается переменной
String status. - Для сложной логики лучше обычный
if/elseради читаемости.
Тернарный оператор удобен для простых условных присваиваний, но не рекомендуется для сложной логики, так как это снижает читаемость.
Операторы null-aware
Dart активно работает с концепцией null и предоставляет специальные операторы для безопасной работы с потенциально пустыми значениями:
-
??— оператор объединения с нулём (null-coalescing) — возвращает левый операнд, если он неnull, иначе — правый.Пример:
String name = userName ?? 'Гость';
Разбор:
-
Если
userName != null, вnameпопадётuserName. -
Если
userName == null, подставится'Гость'. -
Оператор
??безопасно задаёт значение по умолчанию. -
??=— присваивание только если текущее значениеnull.Пример:
config.theme ??= 'light';
Разбор:
-
Присваивание выполнится только если
config.theme == null. -
Если тема уже задана, значение не перезапишется.
-
Удобно для ленивой инициализации настроек по умолчанию.
-
?.— условный оператор доступа к члену: вызывает метод или обращается к свойству только если объект неnull. Если объектnull, всё выражение возвращаетnull.Пример:
int? length = user?.name?.length;
Разбор:
user?.nameвернётnull, еслиuser == null.- Второй
?.lengthтоже безопасен: приnullна любом шаге результатnull. - Тип
int?отражает, что длина может отсутствовать.
Эти операторы значительно упрощают обработку неопределённых состояний и помогают избежать ошибок времени выполнения, связанных с null.
Операторы проверки и приведения типов
Dart — язык со статической типизацией, но в некоторых случаях требуется динамическая проверка или преобразование типов. Для этого используются:
-
is— проверяет, принадлежит ли объект указанному типу. Возвращаетtrue, если тип совпадает.Пример:
if (obj is String) {
print(obj.length);
}
Разбор:
-
obj is Stringпроверяет тип во время выполнения. -
Внутри
ifкомпилятор сужает типobjдоString. -
Можно безопасно вызывать
obj.lengthбезas. -
is!— отрицаниеis: возвращаетtrue, если объект не принадлежит указанному типу. -
as— приведение типа. Используется, когда разработчик уверен в типе объекта, но компилятор не может это гарантировать. При несоответствии типов во время выполнения возникает исключение.Пример:
(obj as String).toUpperCase();
Разбор:
as Stringпринудительно приводитobjк строке.- Если фактический тип другой, будет runtime-ошибка.
- Используют, когда тип уже проверен логикой, но компилятор этого не видит.
В большинстве случаев предпочтительнее использовать is перед обращением к свойствам, чтобы избежать необходимости в as.
Операторы управления потоком — break, continue, return
Хотя эти слова не являются "операторами" в строгом смысле, они управляют выполнением кода:
break— немедленно завершает выполнение цикла илиswitch.continue— прерывает текущую итерацию цикла и переходит к следующей.return— завершает выполнение функции и возвращает значение (если указано).
В Dart также поддерживается маркировка циклов (labeled loops), что позволяет указывать, из какого именно цикла нужно выйти при вложенных конструкциях:
outerLoop:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outerLoop;
}
print('($i, $j)');
}
}
Разбор:
- Метка
outerLoop:даёт имя внешнему циклу. - Внутренний цикл перебирает
jдля каждогоi. break outerLoopвыходит сразу из обоих циклов, а не только из внутреннего.- Условие
i == 1 && j == 1задаёт точку досрочного выхода.