Connect with us

Программирование

Циклы в Swift: скрытые трюки для повышения производительности, которые сделают ваш код в 10 раз быстрее

Помните: сначала пишите понятный код, а оптимизируйте, когда это действительно необходимо. В будущем вы (и ваши товарищи по команде) будете благодарны вам за читабельные циклы вместо преждевременно оптимизированных.

Опубликовано

/

     
     

В начале своего пути в программировании я думал, что циклы — это просто… циклы. Знаете, те самые базовые 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 }

Заключение: контрольный список для циклов

Итак, давайте подведем итоги с помощью практичного контрольного списка, который пригодится вам при написании циклов:

Прежде чем писать любой цикл, спросите себя:

  1. Знаю ли я количество итераций? → Рассмотрите циклы for-in или основанные на диапазоне
  2. Сложное ли условие? → Возможно, while чище
  3. Нужно ли мне хотя бы одно выполнение? → repeat-while может быть идеальным вариантом
  4. Изменяю ли я коллекцию? → Будьте особенно осторожны или используйте альтернативные подходы
  5. Критично ли качество выполнения? → Профилируйте и оптимизируйте соответствующим образом

Красные флаги, на которые стоит обратить внимание:

  • Принудительное развертывание опциональных переменных в циклах
  • Создание ненужных объектов внутри циклов
  • Изменение коллекций во время итерации
  • Вложенные циклы без учёта алгоритмической сложности
  • Отсутствие условий останова в циклах while/repeat-while

Приоритеты оптимизации производительности:

  1. Алгоритмическая сложность (O(n) против O(n²)) — это важнее всего
  2. Выделение памяти — повторное использование объектов, когда это возможно
  3. Накладные расходы на вызов функций — кеширование дорогостоящих вычислений
  4. Выбор цикла — использование правильного инструмента для решения задачи

По правде говоря, большинство проблем с производительностью циклов связаны не с выбором for-in или while. Они связаны с проектированием алгоритма и предотвращением ненужной работы внутри тела цикла. Но когда вам действительно нужно дополнительное преимущество в производительности, знание этих шаблонов может иметь решающее значение.

Помните: сначала пишите понятный код, а оптимизируйте, когда это действительно необходимо. В будущем вы (и ваши товарищи по команде) будете благодарны вам за читабельные циклы вместо преждевременно оптимизированных.

А теперь вперёд и делайте циклы эффективными!

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: