Перейти к основному содержимому

Управляющие конструкции и операторы Dart

Разработчику Архитектору

О чём эта статья

if/else, циклы, switch (в т.ч. expression), null-aware операторы. Углубление паттернов — Dart 3.

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Управляющие конструкции и операторы

Play ITЗагрузка интерактивного демо…


Условные конструкции

Условные конструкции позволяют выполнять различные блоки кода в зависимости от истинности или ложности заданного выражения. В Dart основной конструкцией этого типа является ifelse.

Конструкция 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 и dowhile.

Цикл 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, тело не выполнится ни разу.

Цикл dowhile отличается тем, что тело цикла выполняется как минимум один раз, поскольку проверка условия происходит после выполнения тела.

Пример:

int attempts = 0;
do {
print('Попытка номер ${attempts + 1}');
attempts++;
} while (attempts < 3);

Разбор:

  • Тело do { ... } выполняется минимум один раз.
  • Проверка while (attempts < 3) идёт после выполнения тела.
  • $&#123;attempts + 1&#125; показывает номер попытки с учётом нулевой индексации.
  • Подходит для сценариев "сначала действие, потом проверка условия повтора".

Все циклы поддерживают операторы управления потоком выполнения: break для немедленного выхода из цикла и continue для перехода к следующей итерации без выполнения оставшейся части тела цикла.


Операторы сравнения и логические операторы

Операторы сравнения позволяют оценивать отношения между двумя значениями. В Dart к ним относятся:

  • == — равенство;
  • != — неравенство;
  • <, <=, >, >= — числовые сравнения.

Результатом любого из этих операторов является булево значение (true или false). Dart строго проверяет типы при сравнении, особенно при использовании оператора ==. Для пользовательских классов поведение оператора == можно переопределить, реализовав метод operator == и соответствующий hashCode.

Логические операторы работают с булевыми значениями и включают:

  • && — логическое И (возвращает true, только если оба операнда истинны);
  • || — логическое ИЛИ (возвращает true, если хотя бы один операнд истинен);
  • ! — логическое НЕ (инвертирует значение: !true даёт false, !falsetrue).

Dart поддерживает ленивые вычисления (short-circuit evaluation) для && и ||. Это означает, что если результат выражения можно определить по первому операнду, второй операнд не вычисляется. Например, в выражении a && b(), если a равно false, функция b() не будет вызвана. Это полезно для предотвращения ошибок, таких как обращение к null.


Арифметические и побитовые операторы

Побитовые операторы (&, |, ^, ~, &lt;&lt;, &gt;&gt;) работают с битами целых чисел (маски, флаги).

Арифметические операторы выполняют базовые математические действия:

  • +, -, *, / — сложение, вычитание, умножение, деление;
  • ~/ — целочисленное деление (возвращает 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);
  • ~ — побитовое НЕ (инверсия всех битов);
  • &lt;&lt; — сдвиг влево;
  • &gt;&gt; — сдвиг вправо.

Эти операторы полезны при работе с флагами, масками, низкоуровневыми протоколами или оптимизациях.


Операторы присваивания

Оператор присваивания = связывает переменную со значением. Dart также предоставляет составные операторы присваивания, которые объединяют арифметическую или побитовую операцию с присваиванием:

  • +=, -=, *=, /=, ~/=, %=;
  • &=, |=, ^=, &lt;&lt;=, &gt;&gt;=.

Пример:

int x = 10;
x += 5; // эквивалентно x = x + 5;

Разбор:

  • int x = 10 задаёт начальное значение.
  • x += 5 — составной оператор присваивания (сложение и запись).
  • Эквивалентная запись: x = x + 5.
  • После выполнения x станет равен 15.

Такие операторы делают код более компактным и читаемым.


Тернарный условный оператор

Тернарный оператор — это сокращённая форма условной конструкции ifelse. Он имеет вид:

условие ? выражение_если_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 задаёт точку досрочного выхода.