В начале своего пути в программировании я думал, что циклы — это просто… циклы. Знаете, те самые базовые for и while, которые изучают на первых уроках? Как же я ошибался.
Только когда я начал отлаживать кошмарное приложение, которое тратило больше трёх секунд на отрисовку простого списка, я понял, что система циклов Swift гораздо сложнее, чем показывают большинство руководств. Тот самый невинный цикл for-in, который я везде копировал? Да, он был узким местом.
Вот в чём дело: Swift предоставляет нам три основных типа циклов, но знание того, когда использовать каждый из них, может радикально повлиять на производительность вашего приложения. Мы говорим о разнице между плавной прокруткой со скоростью 60 кадров в секунду и тем, как пользователи в ярости удаляют ваше приложение.
Цикл for-in: ваша ежедневная рабочая лошадка
Начнём со всеобщего любимца — цикла for-in. Он понятен, легко читается и выглядит естественно. Но именно в нем большинство разработчиков допускают ошибки…
Базовый синтаксис цикла for-in:
let numbers = [1, 2, 3, 4, 5]
for number in numbers {
print("Number: \(number)")
}
Достаточно просто, правда? Но погодите — на самом деле существует три разных способа написания циклов for-in, и выбор неправильного может погубить производительность.
Циклы на основе диапазонов: невоспетый герой
Большинство разработчиков по умолчанию перебирают массивы, но циклы на основе диапазонов часто быстрее:
// Instead of this (creating an array):
for i in [0, 1, 2, 3, 4] {
print("Index: \(i)")
}
// Do this (using ranges):
for i in 0...4 {
print("Index: \(i)")
}
// Or for half-open ranges:
for i in 0..<5 {
print("Index: \(i)")
}
Разница в производительности? Диапазоны не создают промежуточных коллекций в памяти. Для небольших циклов это несущественно. Для больших итераций (например, 10 000+ элементов) это разница между плавной работой и подтормаживаниями приложения.
Итерации с индексами (когда нужны и индекс, и значение)
Вот где, по моему мнению, разработчики совершают самую большую ошибку. Не делайте так:
let items = ["Apple", "Banana", "Cherry"]
var index = 0
for item in items {
print("Item \(index): \(item)")
index += 1 // This is... not great
}
Вместо этого используйте enumerated():
let items = ["Apple", "Banana", "Cherry"]
for (index, item) in items.enumerated() {
print("Item \(index): \(item)")
}
Гораздо чище, и Swift может оптимизировать этот шаблон лучше, чем ручное отслеживание индекса.
Функция Stride: для случаев, когда нужны кастомные шаги
Иногда нужно пропускать элементы или возвращаться назад. Смотрите stride:
// Counting by 2s
for i in stride(from: 0, to: 10, by: 2) {
print(i) // Prints: 0, 2, 4, 6, 8
}
// Going backwards
for i in stride(from: 10, through: 0, by: -2) {
print(i) // Prints: 10, 8, 6, 4, 2, 0
}
// Working with array indices (backwards)
let data = ["first", "second", "third", "fourth"]
for i in stride(from: data.count - 1, through: 0, by: -1) {
print("Item \(i): \(data[i])")
}
Обратите внимание на разницу между to (исключительный) и through (включительный). Я всё ещё иногда их путаю…
Циклы while: мощь условий
А вот тут-то и начинается самое интересное. Циклы while нужны не только «когда не знаешь, сколько итераций нужно». На самом деле, в определённых сценариях они являются наиболее экономичным вариантом с точки зрения производительности.
Базовый цикл while:
var countdown = 5
while countdown > 0 {
print("T-minus \(countdown)")
countdown -= 1
}
print("Blast off! ")
Пример из реальной жизни: обработка данных до достижения определённого условия
Вот практический сценарий, с которым я недавно столкнулся. Мне нужно было обрабатывать ответы сети до получения определённого результата:
import Foundation
class DataProcessor {
private var attempts = 0
private let maxAttempts = 5
func processUntilSuccess() -> Bool {
var success = false
while !success && attempts < maxAttempts {
attempts += 1
print("Attempt \(attempts)...")
// Simulate some processing
success = Bool.random() // 50% chance of success
if !success {
print("Failed, retrying...")
Thread.sleep(forTimeInterval: 0.1) // Brief delay
}
}
return success
}
}
// Usage
let processor = DataProcessor()
let result = processor.processUntilSuccess()
print("Final result: \(result ? "Success!" : "Failed after max attempts")")
Совет по производительности: while против for-in для неизвестного количества итераций
Когда вы не знаете, сколько итераций вам понадобится, циклы while часто превосходят for-in с условиями break:
// Less efficient (creates sequence, then breaks)
for i in 0...Int.max {
if someCondition(i) { break }
process(i)
}
// More efficient
var i = 0
while !someCondition(i) {
process(i)
i += 1
}
repeat-while: недооценённый цикл, гарантирующий выполнение
Ладно, перейдем к делу —repeat-while, пожалуй, самый недооценённый цикл в Swift. Но он идеально подходит для определённых сценариев, особенно для проверки и пользовательского ввода.
Базовый синтаксис repeat-while:
var userInput: String
var isValid = false
repeat {
print("Enter a number between 1 and 10:")
userInput = readLine() ?? ""
if let number = Int(userInput), 1...10 ~= number {
isValid = true
print("Valid input: \(number)")
} else {
print("Invalid input. Please try again.")
}
} while !isValid
Ключевое отличие? Тело цикла выполняется хотя бы один раз, независимо от условия.
Практический пример: игровой цикл
Вот простая структура игрового цикла, которая гарантирует как минимум один раунд:
import Foundation
class SimpleGame {
private var playerHealth = 100
private var round = 1
func startGame() {
print(" Game Started!")
repeat {
print("\n--- Round \(round) ---")
print("Health: \(playerHealth)")
// Simulate game round
let damage = Int.random(in: 10...30)
let heal = Int.random(in: 5...15)
playerHealth -= damage
print(" Took \(damage) damage!")
if playerHealth > 0 {
playerHealth += heal
print(" Healed \(heal) points!")
playerHealth = min(playerHealth, 100) // Cap at 100
}
round += 1
// Add some drama
if playerHealth <= 20 {
print("⚠️ Critical health!")
}
} while playerHealth > 0
print("\n Game Over after \(round - 1) rounds!")
}
}
// Usage
let game = SimpleGame()
game.startGame()
Управление циклами: break, continue и метки
Иногда требуется больше контроля над циклами. Swift предоставляет нам несколько мощных инструментов, в том числе один, о существовании которого большинство разработчиков даже не подозревают.
Простые break и continue
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("Even numbers only:")
for number in numbers {
if number % 2 != 0 {
continue // Skip odd numbers
}
print(number)s
}
print("\nNumbers until we hit 7:")
for number in numbers {
if number == 7 {
break // Stop when we reach 7
}
print(number)
}
Метки: секретное оружие
Вот о чём большинство разработчиков Swift не знают — о метках. Они невероятно полезны для вложенных циклов:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
outerLoop: for (rowIndex, row) in matrix.enumerated() {
for (colIndex, value) in row.enumerated() {
if value == 5 {
print("Found 5 at position (\(rowIndex), \(colIndex))")
break outerLoop // Breaks out of BOTH loops
}
}
}
Без метки break будет выходить только из внутреннего цикла. С её помощью мы можем напрямую выходить из внешнего цикла. Революционное решение для алгоритмов поиска!
Практический пример: поиск данных во вложенных структурах
struct User {
let name: String
let posts: [Post]
}
struct Post {
let title: String
let likes: Int
}
let users = [
User(name: "Alice", posts: [
Post(title: "Swift Tips", likes: 45),
Post(title: "iOS Design", likes: 67)
]),
User(name: "Bob", posts: [
Post(title: "Performance", likes: 123),
Post(title: "Architecture", likes: 89)
])
]
// Find the first post with over 100 likes
searchLoop: for user in users {
for post in user.posts {
if post.likes > 100 {
print("Found viral post: '\(post.title)' by \(user.name) with \(post.likes) likes")
break searchLoop
}
}
}
Распространённые ошибки в циклах, которые снижают производительность
Давайте поговорим об ошибках, которые я постоянно вижу в код-ревью. Это не просто проблемы со стилем — они убивают производительность.
Ошибка №1: изменение коллекций во время итерации
// ❌ Don't do this - it's undefined behavior
var numbers = [1, 2, 3, 4, 5]
for (index, number) in numbers.enumerated() {
if number % 2 == 0 {
numbers.remove(at: index) // Crash waiting to happen
}
}
// ✅ Do this instead
var numbers = [1, 2, 3, 4, 5]
numbers = numbers.filter { $0 % 2 != 0 }
// Or if you need indices:
var numbers = [1, 2, 3, 4, 5]
for i in stride(from: numbers.count - 1, through: 0, by: -1) {
if numbers[i] % 2 == 0 {
numbers.remove(at: i)
}
}
Ошибка №2: создание ненужных массивов в циклах
// ❌ Creates a new array every iteration
for i in 0..<1000 {
let data = Array(repeating: 0, count: 100) // Wasteful!
// process data...
}
// ✅ Create once, reuse
var reusableData = Array(repeating: 0, count: 100)
for i in 0..<1000 {
// Reset data if needed
for j in reusableData.indices {
reusableData[j] = 0
}
// process reusableData...
}
Ошибка №3: принудительное разворачивание в циклах
// ❌ Risky - will crash on nil
let optionalNumbers: [Int?] = [1, 2, nil, 4, 5]
for number in optionalNumbers {
print(number!) // Crash on the third iteration
}
// ✅ Safe unwrapping
for number in optionalNumbers {
if let unwrapped = number {
print(unwrapped)
}
}
// ✅ Or use compactMap for filtering
for number in optionalNumbers.compactMap({ $0 }) {
print(number) // Only prints non-nil values
}
Продвинутые шаблоны циклов для реальных приложений
Теперь, когда мы рассмотрели основы, давайте перейдем к некоторым продвинутым паттернам, которые я регулярно использую в приложениях.
Шаблон №1: пакетная обработка с отслеживанием прогресса
import Foundation
class BatchProcessor {
func processBatches<T>(data: [T], batchSize: Int = 100,
processor: (ArraySlice<T>) -> Void) {
let totalBatches = (data.count + batchSize - 1) / batchSize
var currentBatch = 1
for startIndex in stride(from: 0, to: data.count, by: batchSize) {
let endIndex = min(startIndex + batchSize, data.count)
let batch = data[startIndex..<endIndex]
print("Processing batch \(currentBatch)/\(totalBatches) (\(batch.count) items)")
processor(batch)
currentBatch += 1
// Add small delay to prevent UI blocking
if currentBatch % 10 == 0 {
Thread.sleep(forTimeInterval: 0.001)
}
}
}
}
// Usage
let processor = BatchProcessor()
let largeDataSet = Array(1...10000)
processor.processBatches(data: largeDataSet, batchSize: 500) { batch in
// Process each batch
let sum = batch.reduce(0, +)
print("Batch sum: \(sum)")
}
Шаблон №2: логика повтора с экспоненциальной задержкой
import Foundation
func performWithRetry<T>(maxAttempts: Int = 3,
operation: () throws -> T) -> T? {
var attempt = 1
while attempt <= maxAttempts {
do {
return try operation()
} catch {
print("Attempt \(attempt) failed: \(error)")
if attempt == maxAttempts {
print("All attempts failed")
return nil
}
// Exponential backoff: 1s, 2s, 4s, etc.
let delay = pow(2.0, Double(attempt - 1))
print("Waiting \(delay) seconds before retry...")
Thread.sleep(forTimeInterval: delay)
attempt += 1
}
}
return nil
}
// Example usage
let result = performWithRetry(maxAttempts: 3) {
// Simulate a network call that might fail
if Bool.random() {
return "Success!"
} else {
throw NSError(domain: "NetworkError", code: 1, userInfo: nil)
}
}
print("Final result: \(result ?? "Failed")")
Шаблон №3: бесконечный цикл с условиями прерывания
import Foundation
class StreamProcessor {
private var isRunning = false
func startProcessing() {
isRunning = true
var messageCount = 0
// Infinite loop with multiple exit conditions
while true {
// Check if we should stop
if !isRunning {
print("Graceful shutdown requested")
break
}
// Check for max messages processed
if messageCount >= 1000 {
print("Processed maximum messages, shutting down")
break
}
// Simulate processing a message
if let message = getNextMessage() {
process(message)
messageCount += 1
} else {
// No messages available, brief pause
Thread.sleep(forTimeInterval: 0.01)
}
// Periodic status update
if messageCount % 100 == 0 && messageCount > 0 {
print("Processed \(messageCount) messages so far...")
}
}
print("Stream processor stopped. Total messages: \(messageCount)")
}
func stop() {
isRunning = false
}
private func getNextMessage() -> String? {
// Simulate getting messages from a queue
return Bool.random() ? "Message \(Int.random(in: 1...1000))" : nil
}
private func process(_ message: String) {
// Simulate message processing
print("Processing: \(message)")
}
}
// Usage
let streamProcessor = StreamProcessor()
// Start processing in background
DispatchQueue.global().async {
streamProcessor.startProcessing()
}
// Simulate running for a bit, then stopping
Thread.sleep(forTimeInterval: 2.0)
streamProcessor.stop()
Когда использовать тот или иной тип цикла: матрица решения
После многих лет разработки на Swift, вот моё мысленное дерево решений для выбора типа цикла:
Используйте for-in, когда:
- Итерация по всем элементам коллекции
- Вам нужен чистый, читаемый код
- Производительность не является абсолютным приоритетом
- Работа с диапазонами или последовательностями
Используйте while, когда:
- Вы не знаете, сколько итераций вам понадобится
- Условие цикла сложное
- Максимальная производительность критически важна
- Реализация алгоритмов с различным количеством итераций
Используйте repeat-while, когда:
- Вам нужно выполнить тело цикла хотя бы один раз
- Проверка пользовательского ввода
- Реализация игровых циклов или конечных автоматов
- Механизмы повторных попыток, когда вы всегда пытаетесь выполнить один раз
Оптимизация производительности: советы профессионала
Вот методы оптимизации, которые действительно меняют ситуацию к лучшему в реальных приложениях.
Совет №1: минимизируйте вызовы функций в циклах
// ❌ Calls count property every iteration
for i in 0..<array.count {
process(array[i])
}
// ✅ Cache the count
let arrayCount = array.count
for i in 0..<arrayCount {
process(array[i])
}
// ✅ Even better - use for-in when possible
for element in array {
process(element)
}
Совет №2: используйте параметры inout для больших изменений данных
// ❌ Creates new arrays
func processNumbers(_ numbers: [Int]) -> [Int] {
var result = numbers
for i in result.indices {
result[i] *= 2
}
return result
}
// ✅ Modifies in place
func processNumbers(_ numbers: inout [Int]) {
for i in numbers.indices {
numbers[i] *= 2
}
}
Совет №3: рассмотрите возможность параллельной обработки больших наборов данных
import Foundation
// For CPU-intensive operations on large datasets
func parallelProcess<T>(_ array: [T],
operation: @escaping (T) -> T) -> [T] {
let queue = DispatchQueue.global(qos: .userInitiated)
let group = DispatchGroup()
let chunkSize = max(1, array.count / ProcessInfo.processInfo.processorCount)
var results = Array<T?>(repeating: nil, count: array.count)
for startIndex in stride(from: 0, to: array.count, by: chunkSize) {
let endIndex = min(startIndex + chunkSize, array.count)
group.enter()
queue.async {
for i in startIndex..<endIndex {
results[i] = operation(array[i])
}
group.leave()
}
}
group.wait()
return results.compactMap { $0 }
}
// Usage for computationally expensive operations
let numbers = Array(1...100000)
let squared = parallelProcess(numbers) { $0 * $0 }
Заключение: контрольный список для циклов
Итак, давайте подведем итоги с помощью практичного контрольного списка, который пригодится вам при написании циклов:
Прежде чем писать любой цикл, спросите себя:
- Знаю ли я количество итераций? → Рассмотрите циклы
for-inили основанные на диапазоне - Сложное ли условие? → Возможно,
whileчище - Нужно ли мне хотя бы одно выполнение? →
repeat-whileможет быть идеальным вариантом - Изменяю ли я коллекцию? → Будьте особенно осторожны или используйте альтернативные подходы
- Критично ли качество выполнения? → Профилируйте и оптимизируйте соответствующим образом
Красные флаги, на которые стоит обратить внимание:
- Принудительное развертывание опциональных переменных в циклах
- Создание ненужных объектов внутри циклов
- Изменение коллекций во время итерации
- Вложенные циклы без учёта алгоритмической сложности
- Отсутствие условий останова в циклах
while/repeat-while
Приоритеты оптимизации производительности:
- Алгоритмическая сложность (
O(n)противO(n²)) — это важнее всего - Выделение памяти — повторное использование объектов, когда это возможно
- Накладные расходы на вызов функций — кеширование дорогостоящих вычислений
- Выбор цикла — использование правильного инструмента для решения задачи
По правде говоря, большинство проблем с производительностью циклов связаны не с выбором for-in или while. Они связаны с проектированием алгоритма и предотвращением ненужной работы внутри тела цикла. Но когда вам действительно нужно дополнительное преимущество в производительности, знание этих шаблонов может иметь решающее значение.
Помните: сначала пишите понятный код, а оптимизируйте, когда это действительно необходимо. В будущем вы (и ваши товарищи по команде) будете благодарны вам за читабельные циклы вместо преждевременно оптимизированных.
А теперь вперёд и делайте циклы эффективными!

