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

5.14. Операторы и циклы

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

Операторы и циклы

Операторы в Swift

Операторы в Swift делятся на несколько категорий по функциональному назначению: арифметические, сравнения, логические, побитовые, присваивания и специальные операторы. Каждый тип оператора имеет чётко определённое поведение и область применения.

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

Арифметические операторы выполняют базовые математические действия над числовыми значениями. Swift поддерживает сложение (+), вычитание (-), умножение (*) и деление (/). Все эти операторы работают как с целыми числами, так и с числами с плавающей точкой. Например, выражение 10 + 5 возвращает значение 15, а 7.5 * 2.0 даёт результат 15.0.

Особое внимание в Swift уделено безопасности при работе с целочисленными типами. Оператор получения остатка от деления (%) доступен только для целых чисел. Он возвращает остаток после целочисленного деления одного числа на другое. Например, 9 % 4 равно 1, потому что девять делится на четыре два раза с остатком один. Этот оператор часто используется для определения чётности числа или для циклического переключения между состояниями.

Swift также предоставляет унарные операторы + и -, которые могут применяться к одному числовому значению. Унарный плюс (+x) возвращает значение без изменений и используется в основном для симметрии с унарным минусом. Унарный минус (-x) меняет знак числа на противоположный. Например, если переменная a равна 5, то -a будет равно -5.

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

Операторы сравнения используются для проверки отношений между двумя значениями. Swift поддерживает шесть стандартных операторов: равенство (==), неравенство (!=), больше (>), меньше (<), больше или равно (>=) и меньше или равно (<=). Результатом любого сравнения является логическое значение типа Bool — либо true, либо false.

Эти операторы применимы ко всем типам, которые соответствуют протоколу Equatable (для == и !=) или Comparable (для порядковых сравнений). Встроенные типы, такие как Int, Double, String и многие другие, уже реализуют эти протоколы. Это позволяет сравнивать строки лексикографически, числа по величине и даже пользовательские структуры, если они явно объявлены как соответствующие нужным протоколам.

Сравнение строк в Swift учитывает нормализацию Юникода, что обеспечивает корректную работу с многоязычными текстами и специальными символами. Например, строка "café" будет считаться равной строке "cafe\u{301}", где последний символ — это комбинирующий акцент, даже если внутреннее представление отличается.

Логические операторы

Логические операторы работают с булевыми значениями и позволяют строить сложные условия. Swift предоставляет три основных логических оператора: логическое И (&&), логическое ИЛИ (||) и логическое НЕ (!).

Оператор && возвращает true, только если оба операнда равны true. Если первый операнд оказывается false, второй операнд не вычисляется — это называется «ленивой» или «короткозамкнутой» оценкой. Такое поведение повышает эффективность и предотвращает ошибки, например, при проверке наличия значения перед обращением к его свойствам.

Оператор || возвращает true, если хотя бы один из операндов равен true. Аналогично, если первый операнд равен true, второй не вычисляется, так как результат уже известен.

Оператор ! — унарный. Он инвертирует булево значение: !true становится false, а !falsetrue. Этот оператор часто применяется для проверки отрицательных условий, например, if !isEnabled { ... }.

Swift требует, чтобы операнды логических операторов были строго типа Bool. Это исключает неявные преобразования, характерные для некоторых других языков, где, например, ноль может интерпретироваться как false. Такой подход повышает читаемость и снижает вероятность логических ошибок.

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

Оператор присваивания (=) используется для сохранения значения в переменную или константу. В Swift присваивание не возвращает значение, в отличие от некоторых языков, где оно может использоваться внутри выражений. Это предотвращает распространённую ошибку, когда вместо оператора сравнения (==) случайно используется оператор присваивания (=).

Swift также поддерживает составные операторы присваивания, которые сочетают арифметическую или побитовую операцию с присваиванием. К ним относятся +=, -=, *=, /=, %= и другие. Например, запись counter += 1 эквивалентна counter = counter + 1, но более кратка и выразительна. Эти операторы особенно удобны в циклах и при обновлении счётчиков.

Побитовые операторы

Побитовые операторы работают на уровне отдельных битов целочисленных значений. Swift предоставляет полный набор таких операторов: побитовое И (&), побитовое ИЛИ (|), побитовое исключающее ИЛИ (^) и побитовое НЕ (~). Также доступны операторы сдвига влево (<<) и вправо (>>).

Побитовые операции часто используются в системном программировании, работе с сетевыми протоколами, шифрованием и оптимизации производительности. Например, сдвиг влево на n бит эквивалентен умножению числа на 2^n, а сдвиг вправо — целочисленному делению на 2^n. Swift гарантирует, что сдвиг за пределы разрядной сетки не вызывает неопределённого поведения: при сдвиге вправо знаковый бит сохраняется для знаковых типов, а для беззнаковых — вставляются нули.

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

Тернарный условный оператор (condition ? valueIfTrue : valueIfFalse) предоставляет компактный способ выбора между двумя значениями на основе логического условия. Он состоит из трёх частей: условия, значения при истинности условия и значения при его ложности.

Например, выражение let message = isLoggedIn ? "Добро пожаловать!" : "Пожалуйста, войдите." присваивает переменной message одно из двух строк в зависимости от значения isLoggedIn. Этот оператор особенно полезен для инициализации констант или передачи значений в функции, где требуется краткость без ущерба для ясности.

Swift требует, чтобы оба возможных значения имели совместимые типы, что обеспечивает безопасность типов даже в сокращённой форме записи.

Операторы диапазонов

Операторы диапазонов позволяют создавать последовательности значений, которые часто используются в циклах и при работе с коллекциями. Swift поддерживает два основных типа диапазонов: замкнутый (...) и полуоткрытый (..<).

Замкнутый диапазон a...b включает все значения от a до b включительно. Например, 1...5 представляет последовательность 1, 2, 3, 4, 5.

Полуоткрытый диапазон a..<b включает значения от a до b, но не включает само значение b. Например, 0..<3 даёт 0, 1, 2. Этот тип диапазона особенно удобен при работе с массивами, где индексы начинаются с нуля и заканчиваются на count - 1.

Диапазоны в Swift являются полноценными объектами, которые можно сохранять в переменные, передавать в функции и использовать в различных контекстах. Они реализуют протокол Sequence, что делает их совместимыми с циклами for-in.


Циклы в Swift

Циклы в Swift позволяют многократно выполнять блок кода до тех пор, пока соблюдается заданное условие или не завершится перебор элементов коллекции. Язык предоставляет три основных типа циклов: for-in, while и repeat-while. Каждый из них предназначен для решения определённого класса задач и обладает собственными особенностями синтаксиса и поведения.

Цикл for-in

Цикл for-in является наиболее часто используемым циклом в Swift. Он предназначен для перебора последовательностей — таких как диапазоны, массивы, строки, словари и другие коллекции, соответствующие протоколу Sequence. Синтаксис этого цикла прост и выразителен:

for item in collection {
// тело цикла
}

Здесь item — временная константа, которая на каждой итерации принимает очередное значение из collection. Эта константа доступна только внутри тела цикла и автоматически освобождается после его завершения.

Пример перебора диапазона:

for index in 1...5 {
print("Итерация \(index)")
}

Этот код выведет числа от 1 до 5 включительно. Если требуется исключить последнее значение, используется полуоткрытый диапазон:

for i in 0..<3 {
print(i)
}
// Вывод: 0, 1, 2

Цикл for-in также эффективно работает с массивами:

let colors = ["красный", "зелёный", "синий"]
for color in colors {
print("Цвет: \(color)")
}

При необходимости получить не только значение, но и индекс элемента, Swift предлагает функцию enumerated():

for (index, value) in colors.enumerated() {
print("\(index): \(value)")
}

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

Для словарей цикл for-in возвращает кортеж из ключа и значения:

let scores = ["Алиса": 95, "Боб": 87]
for (name, score) in scores {
print("\(name) набрал(а) \(score) баллов")
}

Порядок перебора элементов словаря не гарантируется, так как словари в Swift не сохраняют порядок вставки (до версии 5.7). Начиная с Swift 5.7, словари сохраняют порядок вставки, но это поведение следует использовать с осторожностью, если порядок критичен для логики программы.

Цикл for-in можно применять даже к строкам, перебирая символы:

for character in "Swift" {
print(character)
}

Это демонстрирует мощную интеграцию строк с системой коллекций Swift: строка рассматривается как последовательность расширенных графем, что обеспечивает корректную работу с многоязычными текстами и эмодзи.

Цикл while

Цикл while выполняет блок кода до тех пор, пока условие остаётся истинным. Проверка условия происходит до каждой итерации, поэтому тело цикла может не выполниться ни разу, если условие изначально ложно.

Синтаксис:

while condition {
// тело цикла
}

Пример:

var countdown = 3
while countdown > 0 {
print("Осталось: \(countdown)")
countdown -= 1
}
print("Пуск!")

Этот цикл выведет обратный отсчёт от 3 до 1, а затем сообщение «Пуск!». Если начальное значение countdown было бы равно нулю или меньше, тело цикла не выполнилось бы.

Цикл while часто используется в ситуациях, когда количество итераций заранее неизвестно — например, при чтении данных из сети, ожидании события или обработке пользовательского ввода.

Цикл repeat-while

Цикл repeat-while (аналог do-while в других языках) отличается от while тем, что проверка условия происходит после выполнения тела цикла. Это гарантирует, что тело цикла выполнится хотя бы один раз.

Синтаксис:

repeat {
// тело цикла
} while condition

Пример:

var attempts = 0
repeat {
attempts += 1
print("Попытка №\(attempts)")
} while attempts < 3

Вывод:

Попытка №1
Попытка №2
Попытка №3

Обратите внимание: цикл завершается после третьей попытки, потому что условие attempts < 3 становится ложным только после увеличения attempts до 3.

Этот тип цикла удобен, когда действие должно быть выполнено минимум один раз, а дальнейшее повторение зависит от результата этого действия — например, при запросе пароля, генерации случайного числа в допустимом диапазоне или инициализации ресурса.

Управление потоком выполнения: break и continue

Swift предоставляет два оператора для управления ходом выполнения циклов: break и continue.

Оператор break немедленно завершает выполнение текущего цикла и передаёт управление первой инструкции после цикла. Он полезен, когда дальнейшая обработка становится бессмысленной — например, при поиске первого совпадения:

let numbers = [2, 4, 6, 7, 8]
for number in numbers {
if number % 2 != 0 {
print("Найдено нечётное число: \(number)")
break
}
}

Оператор continue прерывает текущую итерацию и переходит к следующей. Он позволяет пропустить обработку определённых элементов без остановки всего цикла:

for number in 1...10 {
if number % 2 == 0 {
continue
}
print("Нечётное: \(number)")
}

Этот код выведет только нечётные числа от 1 до 9.

Метки циклов

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

Метка указывается перед циклом, за которой следует двоеточие:

outerLoop: for i in 1...3 {
for j in 1...3 {
if i * j == 4 {
break outerLoop
}
print("(\(i), \(j))")
}
}

Без метки break завершил бы только внутренний цикл. С меткой outerLoop управление передаётся сразу за пределы внешнего цикла.

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

Метки делают код более предсказуемым в сложных сценариях и избавляют от необходимости использовать флаговые переменные или дублировать условия выхода.

Бесконечные циклы

Хотя Swift не имеет специального синтаксиса для бесконечных циклов, их легко создать с помощью while true:

while true {
// выполняется вечно, пока не вызван break
}

Такие циклы применяются в серверных приложениях, игровых движках или фоновых задачах, где программа должна постоянно обрабатывать входящие события. Важно предусмотреть условие выхода с помощью break, иначе программа зависнет.

Альтернативно, можно использовать repeat-while true, но это менее распространено, так как требует хотя бы одной итерации, даже если она не нужна.


Условные конструкции и их связь с циклами и операторами

Хотя тема главы сосредоточена на операторах и циклах, полное понимание управления потоком выполнения невозможно без рассмотрения условных конструкций. В Swift они тесно интегрированы с циклами и логическими операторами, образуя единый механизм принятия решений.

Условный оператор if

Оператор if позволяет выполнять блок кода только в том случае, если заданное условие истинно. Он может включать дополнительные ветви else if для проверки альтернативных условий и финальную ветвь else для обработки всех остальных случаев.

if temperature > 30 {
print("Жарко")
} else if temperature > 20 {
print("Тепло")
} else {
print("Прохладно")
}

Условие в if должно быть выражением типа Bool. Swift не допускает неявного преобразования чисел или других типов в логические значения, что исключает распространённые ошибки, связанные с неправильной интерпретацией данных.

Оператор if часто используется внутри циклов для фильтрации элементов или изменения поведения в зависимости от состояния. Например, в цикле перебора массива можно пропускать отрицательные числа:

for number in [-3, 0, 5, -1, 7] {
if number >= 0 {
print("Положительное: \(number)")
}
}

Это сочетание демонстрирует, как логические операторы, условия и циклы работают вместе для реализации сложной логики.

Оператор guard

Оператор guard представляет собой инвертированную форму if, предназначенную для раннего выхода из функции, цикла или замыкания при невыполнении условия. Его синтаксис требует наличия ветви else, в которой обязательно должен быть оператор перехода управления — return, break, continue или throw.

func processUser(name: String?) {
guard let unwrappedName = name, !unwrappedName.isEmpty else {
print("Имя отсутствует или пустое")
return
}
print("Обработка пользователя: \(unwrappedName)")
}

Ключевое преимущество guard — сохранение области видимости распакованных опционалов. Переменная unwrappedName остаётся доступной во всём оставшемся теле функции, а не только внутри блока условия. Это улучшает читаемость и уменьшает вложенность кода.

Внутри циклов guard часто используется для пропуска недопустимых значений:

for item in optionalItems {
guard let value = item, value.isValid else { continue }
// обработка валидного значения
}

Такой подход делает логику более линейной и предотвращает «пирамиду вложенности», характерную для множественных if.

Оператор switch

Оператор switch в Swift значительно мощнее аналогов в других языках. Он поддерживает сопоставление с любыми типами данных, включая строки, кортежи, перечисления и даже пользовательские структуры. Каждый случай (case) должен быть исчерпывающим — либо покрывать все возможные значения, либо завершаться ветвью default.

let direction = "north"
switch direction {
case "north":
print("Движение на север")
case "south":
print("Движение на юг")
default:
print("Неизвестное направление")
}

Swift требует, чтобы каждый case содержал хотя бы один исполняемый оператор. Пустые ветви без явного указания fallthrough не допускаются, что предотвращает случайное «проваливание» между случаями — частую причину ошибок в C-подобных языках.

Оператор switch поддерживает сложные шаблоны сопоставления, включая интервалы, привязку значений и условия where:

let point = (1, 0)
switch point {
case (0, 0):
print("Начало координат")
case (_, 0):
print("Точка на оси X")
case (0, _):
print("Точка на оси Y")
case let (x, y) where x == y:
print("Точка на диагонали: (\(x), \(y))")
default:
print("Обычная точка")
}

Эта гибкость делает switch мощным инструментом для анализа структурированных данных, особенно в сочетании с перечислениями (enum), которые часто используются для моделирования состояний приложения.

Циклы и switch могут комбинироваться для обработки последовательностей состояний. Например, при разборе команд из очереди:

for command in commandQueue {
switch command.type {
case .start:
startEngine()
case .stop:
stopEngine()
case .pause:
pauseExecution()
}
}

Такой код остаётся читаемым даже при большом количестве команд, благодаря декларативному стилю switch.

Сочетание условий, циклов и операторов

На практике управление потоком выполнения редко ограничивается одним типом конструкции. Реальные программы используют комбинации:

  • Логические операторы формируют сложные условия для if, while и guard.
  • Циклы for-in перебирают коллекции, а внутри применяются условия для фильтрации.
  • Операторы диапазонов задают границы итераций.
  • Тернарный оператор позволяет компактно выбирать значения в инициализации или передаче аргументов.
  • break и continue с метками дают точный контроль над вложенными циклами.

Пример комплексного использования:

outer: for row in matrix {
var sum = 0
for element in row {
guard element > 0 else { continue }
sum += element
if sum > 100 {
print("Сумма превышена в строке")
break outer
}
}
print("Сумма строки: \(sum)")
}

Этот фрагмент демонстрирует:

  • Вложенные циклы с меткой outer.
  • Использование guard для пропуска недопустимых значений.
  • Накопление суммы с помощью составного оператора +=.
  • Ранний выход из внешнего цикла при достижении порога.

Такой стиль кода соответствует философии Swift: безопасность через строгую типизацию, выразительность через лаконичный синтаксис и предотвращение ошибок через явные конструкции.

Рекомендации по стилю и надёжности

При написании кода с операторами и циклами в Swift стоит придерживаться следующих принципов:

  • Предпочитать for-in явным счётчикам, когда цель — перебор коллекции.
  • Использовать guard для проверки предусловий и раннего выхода, особенно в функциях.
  • Избегать глубокой вложенности, вынося логику в отдельные функции или используя continue/break.
  • Применять тернарный оператор только для простых, легко читаемых выражений.
  • Не использовать побитовые операторы без крайней необходимости — они снижают читаемость.
  • Всегда обеспечивать завершение циклов, особенно бесконечных, с помощью явных условий выхода.

Эти практики способствуют созданию кода, который легко читать, тестировать и поддерживать.