🍎 Swift

Swift में ऐरे और कलेक्शन मेथड्स: एक विस्तृत गाइड

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь. У тебя есть огромный стеллаж с книгами. Каждая книга — это отдельный элемент данных (число, строка, объект). Раньше, чтобы найти книгу по жанру «фэнтези», тебе приходилось вручную перебирать каждую книгу, записывать названия на листочек, потом сортировать их по алфавиту, а затем переписывать всё в отчёт. Это долго, скучно и легко ошибиться.

Методы массивов и коллекций в Swift — это как набор волшебных инструментов для библиотекаря. Вместо того чтобы делать всё руками, ты говоришь: «Массив, найди мне все книги жанра "фэнтези"» (метод filter). Или: «Преврати каждую книгу в её краткое описание» (метод map). Или: «Отсортируй книги по году издания» (метод sorted).

Зачем это нужно? В реальном программировании ты постоянно работаешь со списками данных: список пользователей, список товаров в корзине, список сообщений в чате, список координат на карте. Методы массивов позволяют обрабатывать эти списки быстро, красиво и без лишних ошибок. Они делают код короче и понятнее, как если бы библиотекарь использовал робота-помощника вместо ручного труда.

💡 Важно: Методы массивов не изменяют оригинальный массив (если не сказано иное). Они создают новый массив с результатом. Это безопасно — ты не потеряешь исходные данные.

Теория: как это работает изнутри

Давай заглянем «под капот» Swift. Массив — это упорядоченная коллекция элементов одного типа. В памяти компьютера элементы хранятся рядом друг с другом (последовательно). Это как вагоны поезда: каждый вагон знает свой номер, и чтобы добраться до 5-го вагона, нужно пройти через 1-й, 2-й, 3-й и 4-й.

Когда ты вызываешь метод, например filter, Swift делает следующее:

  1. Создаёт новый пустой массив (новый «поезд»).
  2. Проходит по каждому элементу исходного массива (по каждому вагону).
  3. Проверяет условие (например, «книга — фэнтези?»).
  4. Если условие верно — копирует элемент в новый массив.
  5. Возвращает новый массив.

Метод map работает похоже, но вместо проверки условия он преобразует каждый элемент. Например, у тебя есть массив чисел [1, 2, 3]. Ты говоришь: «Каждое число умножь на 2». Swift создаёт новый массив, проходит по каждому числу, умножает его и кладёт результат в новый массив. На выходе — [2, 4, 6].

Метод reduce — это «свёртка». Представь, что ты хочешь сложить все числа в массиве. Swift берёт начальное значение (например, 0), проходит по каждому элементу и «накапливает» результат: 0 + 1 = 1, 1 + 2 = 3, 3 + 3 = 6. Итог — 6.

Все эти методы используют замыкания (closures) — это такие мини-функции, которые ты передаёшь методу «на лету». Замыкание говорит: «Делай вот так с каждым элементом». Swift очень умный и часто позволяет писать замыкания очень кратко, используя сокращённые имена параметров ($0, $1).

⚠️ Важно: Методы массивов — это не магия, а оптимизированный код. Они работают быстро, но если у тебя массив из миллиона элементов, любой метод будет выполняться какое-то время. Для обычных задач (сотни-тысячи элементов) скорость незаметна.

Синтаксис в Swift: все формы и варианты

В Swift есть два основных типа коллекций: Array (массив) и Dictionary (словарь). Есть ещё Set (множество), но о нём позже. Сейчас сосредоточимся на массивах — они используются чаще всего.

Основные методы массивов (таблица-справочник)

Метод Что делает Пример вызова Результат
map Преобразует каждый элемент numbers.map { $0 * 2 } Новый массив с удвоенными числами
filter Оставляет только элементы, проходящие условие numbers.filter { $0 > 5 } Новый массив с числами больше 5
reduce Сворачивает массив в одно значение numbers.reduce(0, +) Сумма всех чисел
sorted Сортирует массив names.sorted() Новый отсортированный массив
forEach Проходит по каждому элементу (ничего не возвращает) names.forEach { print($0) } Печатает каждое имя
first(where:) Находит первый элемент, удовлетворяющий условию numbers.first { $0 > 10 } Опциональное число (может быть nil)
contains Проверяет, есть ли элемент в массиве names.contains("Анна") true или false
allSatisfy Проверяет, все ли элементы удовлетворяют условию numbers.allSatisfy { $0 > 0 } true или false
compactMap Как map, но отбрасывает nil strings.compactMap { Int($0) } Массив чисел, nil-значения удалены
flatMap «Разглаживает» вложенные массивы arrays.flatMap { $0 } Один плоский массив

Формы записи замыканий

У методов массивов есть несколько способов передачи замыкания. Вот они от самого полного до самого краткого:

// 1. Полная форма с указанием параметров и return
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map({ (number: Int) -> Int in
    return number * 2
})

// 2. Сокращённая форма (типы выводятся автоматически)
let doubled2 = numbers.map({ number in
    return number * 2
})

// 3. Ещё короче (опускаем return, если одна строка)
let doubled3 = numbers.map({ number in number * 2 })

// 4. Используем сокращённые имена параметров ($0, $1, ...)
let doubled4 = numbers.map({ $0 * 2 })

// 5. Если замыкание — последний аргумент, можно вынести за скобки (trailing closure)
let doubled5 = numbers.map { $0 * 2 }

// 6. Если метод принимает только замыкание, скобки можно опустить полностью
let doubled6 = numbers.map { $0 * 2 }  // Самый популярный вариант!
💡 Совет: Начинающим лучше использовать форму с $0 и trailing closure — это стандарт в Swift-сообществе. Не бойся $0, это просто «первый аргумент».

Примеры кода — от простого к сложному

Пример 1: Базовый map — преобразование чисел

// У нас есть массив с оценками учеников
let scores = [3, 4, 5, 2, 5, 4, 3]

// Хотим увеличить каждую оценку на 1 балл (но не выше 5)
let improvedScores = scores.map { score in
    // Если оценка уже 5, оставляем 5, иначе увеличиваем на 1
    if score == 5 {
        return 5
    } else {
        return score + 1
    }
}

print("Было: (scores)")          // [3, 4, 5, 2, 5, 4, 3]
print("Стало: (improvedScores)") // [4, 5, 5, 3, 5, 5, 4]

// Объяснение:
// map проходит по каждому элементу scores.
// Для каждого score мы проверяем: если 5 — возвращаем 5, иначе score + 1.
// Результат — новый массив improvedScores.

Пример 2: filter — отбор чётных чисел

let numbers = [12, 7, 8, 15, 22, 3, 10]

// Оставляем только чётные числа
let evenNumbers = numbers.filter { number in
    // Оператор % возвращает остаток от деления
    // Если остаток от деления на 2 равен 0 — число чётное
    return number % 2 == 0
}

print("Все числа: (numbers)")      // [12, 7, 8, 15, 22, 3, 10]
print("Чётные: (evenNumbers)")     // [12, 8, 22, 10]

// Короткая версия (профессиональный стиль):
let evenNumbersShort = numbers.filter { $0 % 2 == 0 }

Пример 3: reduce — подсчёт суммы и произведения

let prices = [150, 230, 80, 420, 95]

// Считаем общую сумму покупок
// reduce(0, +) означает: начать с 0 и складывать каждый элемент
let total = prices.reduce(0, +)
print("Общая сумма: (total) рублей") // 975

// Считаем произведение всех чисел (начальное значение 1)
let product = prices.reduce(1, *)
print("Произведение: (product)") // 150*230*80*420*95 = огромное число

// Более сложный пример: собираем строку из элементов
let words = ["Я", "учу", "Swift"]
let sentence = words.reduce("") { partialResult, word in
    // partialResult — то, что накопилось, word — текущее слово
    if partialResult.isEmpty {
        return word
    } else {
        return partialResult + " " + word
    }
}
print(sentence) // "Я учу Swift"

Пример 4: sorted — сортировка с разными условиями

let names = ["Иван", "Анна", "Пётр", "Елена", "Максим"]

// Сортировка по алфавиту (по умолчанию — по возрастанию)
let sortedNames = names.sorted()
print("По алфавиту: (sortedNames)") // ["Анна", "Елена", "Иван", "Максим", "Пётр"]

// Сортировка по убыванию (от Я до А)
let reversedNames = names.sorted { $0 > $1 }
print("В обратном порядке: (reversedNames)") // ["Пётр", "Максим", "Иван", "Елена", "Анна"]

// Сортировка по длине имени (от короткого к длинному)
let byLength = names.sorted { $0.count < $1.count }
print("По длине: (byLength)") // ["Иван", "Анна", "Пётр", "Елена", "Максим"]

Пример 5: compactMap — безопасное преобразование строк в числа

// Представь, что мы получили данные из текстового поля
let inputStrings = ["123", "привет", "456", "0", "abc", "789"]

// Попробуем превратить каждую строку в число
// Int("привет") вернёт nil, так как это не число
// compactMap автоматически отбрасывает nil
let validNumbers = inputStrings.compactMap { Int($0) }

print("Исходные строки: (inputStrings)")
print("Получившиеся числа: (validNumbers)") // [123, 456, 0, 789]

// Если бы мы использовали обычный map, то получили бы массив с опционалами:
let optionalNumbers = inputStrings.map { Int($0) }
print("С map: (optionalNumbers)") // [Optional(123), nil, Optional(456), Optional(0), nil, Optional(789)]

Пример 6: flatMap — работа с вложенными массивами

// У нас есть список групп студентов
let groups = [
    ["Анна", "Борис"],
    ["Виктор", "Галина", "Дмитрий"],
    ["Елена"]
]

// Хотим получить один общий список всех студентов
let allStudents = groups.flatMap { $0 }
print("Все студенты: (allStudents)")
// ["Анна", "Борис", "Виктор", "Галина", "Дмитрий", "Елена"]

// flatMap также может комбинировать элементы из разных массивов
let numbers1 = [1, 2, 3]
let letters = ["a", "b"]

// Создаём все возможные пары число-буква
let pairs = numbers1.flatMap { number in
    letters.map { letter in
        "(number)(letter)"
    }
}
print("Все комбинации: (pairs)")
// ["1a", "1b", "2a", "2b", "3a", "3b"]

Пример 7: Комбинация методов — цепочка вызовов

// Реальная задача: у нас есть список заказов
struct Order {
    let product: String
    let price: Int
    let quantity: Int
}

let orders = [
    Order(product: "Ноутбук", price: 50000, quantity: 2),
    Order(product: "Мышь", price: 1500, quantity: 5),
    Order(product: "Клавиатура", price: 3000, quantity: 0), // нет в наличии
    Order(product: "Монитор", price: 25000, quantity: 1)
]

// Хотим: отфильтровать заказы с quantity > 0,
// посчитать общую стоимость каждого (price * quantity),
// отсортировать по убыванию стоимости,
// и вывести только названия товаров

let result = orders
    .filter { $0.quantity > 0 }              // оставляем только доступные товары
    .map { ($0.product, $0.price * $0.quantity) }  // превращаем в кортеж (название, общая стоимость)
    .sorted { $0.1 > $1.1 }                  // сортируем по стоимости (убывание)
    .map { $0.0 }                            // оставляем только названия

print("Товары по убыванию выручки: (result)")
// ["Ноутбук", "Монитор", "Мышь"]

// Объяснение цепочки:
// 1. filter убирает клавиатуру (quantity = 0)
// 2. map создаёт пары: ("Ноутбук", 100000), ("Мышь", 7500), ("Монитор", 25000)
// 3. sorted сортирует по второму элементу кортежа (стоимости)
// 4. map забирает только первый элемент (название)

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет легче. Swift и Python очень похожи в работе с коллекциями, но есть важные различия.

Операция Swift Python Примечание
Преобразование map { $0 * 2 } map(lambda x: x * 2, list) или [x * 2 for x in list] В Swift используется замыкание, в Python — lambda или list comprehension
Фильтрация filter { $0 > 5 } filter(lambda x: x > 5, list) или [x for x in list if x > 5] Swift использует замыкания, Python — lambda или comprehension
Свёртка reduce(0, +) functools.reduce(lambda a, b: a + b, list, 0) или sum(list) В Python есть встроенные sum, min, max
Сортировка sorted() или sorted { $0 > $1 } sorted(list) или sorted(list, reverse=True) Очень похожи, но в Swift для обратного порядка используется замыкание
Проверка наличия contains(5) 5 in list В Python используется оператор in, в Swift — метод
Типизация Строгая: массив может содержать только один тип Динамическая: список может содержать любые типы В Swift массив [Int] не может содержать строки
Производительность Высокая, компилируемый язык Ниже, интерпретируемый язык Swift обычно быстрее для больших данных
Изменяемость let — неизменяемый, var — изменяемый Списки всегда изменяемы, кортежи — нет В Swift контроль через let/var
💡 Главное отличие: В Python очень популярны list comprehensions (генераторы списков) — [x*2 for x in list]. В Swift такой конструкции нет, вместо неё используются цепочки методов map, filter и т.д. Это вопрос стиля: comprehensions короче, но цепочки методов читаются как предложения.

Типичные ошибки и подводные камни

Ошибка 1: Забыть, что методы возвращают новый массив

var numbers = [1, 2, 3, 4, 5]
numbers.map { $0 * 2 }
print(numbers) // [1, 2, 3, 4, 5] — ничего не изменилось!

// Правильно: присвоить результат
numbers = numbers.map { $0 * 2 }
print(numbers) // [2, 4, 6, 8, 10]

Ошибка 2: Использовать filter, когда нужен first(where:)

let users = ["Анна", "Борис", "Виктор", "Анна"]

// Плохо: ищем первую Анну через filter
let allAnnas = users.filter { $0 == "Анна" } // ["Анна", "Анна"]
let firstAnna = allAnnas.first // Optional("Анна")

// Хорошо: используем first(where:)
let firstAnnaDirect = users.first { $0 == "Анна" } // Optional("Анна")
// first(where:) останавливается, как только нашёл первый подходящий элемент
// Это быстрее, особенно на больших массивах

Ошибка 3: Путаница между sorted() и sort()

var mutableNumbers = [3, 1, 4, 1, 5]

// sorted() — возвращает новый массив
let sortedCopy = mutableNumbers.sorted()
print(mutableNumbers) // [3, 1, 4, 1, 5] — оригинал не изменился

// sort() — сортирует массив на месте (только для var)
mutableNumbers.sort()
print(mutableNumbers) // [1, 1, 3, 4, 5] — оригинал изменился

// Ошибка: попытка вызвать sort() на let
let immutableNumbers = [3, 1, 4]
// immutableNumbers.sort() // Ошибка компиляции! Нельзя изменить let-константу

Ошибка 4: Забыть про опциональность в first(where:)

let emptyArray: [Int] = []
let firstEven = emptyArray.first { $0 % 2 == 0 }
// firstEven имеет тип Int? (опциональный Int)

// Ошибка: использовать значение без проверки
// let doubled = firstEven * 2 // Ошибка компиляции!

// Правильно: использовать optional binding
if let number = firstEven {
    print("Первое чётное: (number)")
} else {
    print("Чётных чисел нет")
}

Ошибка 5: Слишком сложные замыкания внутри цепочек

// Плохо: всё в одну строку, трудно читать
let result = data.filter { $0.isValid }.map { $0.process() }.filter { $0 > 0 }.sorted { $0 > $1 }

// Хорошо: разбить на шаги с понятными именами
let validItems = data.filter { $0.isValid }
let processedItems = validItems.map { $0.process() }
let positiveItems = processedItems.filter { $0 > 0 }
let sortedItems = positiveItems.sorted { $0 > $1 }

Лучшие практики и советы профессионалов

1. Используй цепочки методов вместо циклов for-in

Цепочки методов (map, filter, reduce) делают код декларативным — ты говоришь что сделать, а не как. Это уменьшает количество ошибок и делает код самодокументируемым.

// Плохо: цикл for
var result = [Int]()
for number in numbers {
    if number > 0 {
        result.append(number * 2)
    }
}

// Хорошо: цепочка методов
let result = numbers.filter { $0 > 0 }.map { $0 * 2 }

2. Давай замыканиям имена, если логика сложная

// Вместо того чтобы писать сложное замыкание внутри filter
let expensiveProducts = products.filter { product in
    product.price > 1000 && product.quantity > 0 && product.category == "electronics"
}

// Вынеси условие в отдельную функцию
func isExpensiveAndAvailable(_ product: Product) -> Bool {
    return product.price > 1000 && product.quantity > 0 && product.category == "electronics"
}

let expensiveProducts = products.filter(isExpensiveAndAvailable)

3. Используй compactMap вместо map + filter для опционалов

// Плохо: два прохода по массиву
let numbers = strings.map { Int($0) }.filter { $0 != nil }.map { $0! }

// Хорошо: один проход
let numbers = strings.compactMap { Int($0) }

4. Для производительности: используй lazy, если обрабатываешь огромные массивы

// Обычная цепочка создаёт промежуточные массивы
let result = bigArray.filter { $0 > 0 }.map { $0 * 2 }

// Ленивая цепочка не создаёт промежуточные массивы
let lazyResult = bigArray.lazy.filter { $0 > 0 }.map { $0 * 2 }
// Результат будет вычислен только когда понадобится (например, при итерации)

5. Не бойся использовать forEach для побочных эффектов

// Когда нужно что-то сделать с каждым элементом, но не нужно возвращать результат
users.forEach { user in
    sendEmail(to: user)
    logActivity(user)
}

Вопросы для самопроверки

  1. Вопрос: Какая разница между map и filter?

    Ответ: map преобразует каждый элемент массива (количество элементов не меняется). filter отбирает элементы по условию (количество элементов может уменьшиться).

  2. Вопрос: Что вернёт [1, 2, 3].reduce(0, +)?

    Ответ: 6. Начинаем с 0, прибавляем 1 → 1, прибавляем 2 → 3, прибавляем 3 → 6.

  3. Вопрос: Почему после вызова numbers.map { $0 * 2 } массив numbers не изменился?

    Ответ: Потому что map возвращает новый массив, не изменяя исходный. Нужно присвоить результат: numbers = numbers.map { $0 * 2 }.

  4. Вопрос: В чём разница между sorted() и sort()?

    Ответ: sorted() возвращает новый отсортированный массив, не изменяя оригинал. sort() сортирует массив на месте (изменяет оригинал) и доступен только для var.

  5. Вопрос: Какой метод использовать, чтобы превратить массив строк в массив чисел, отбросив некорректные значения?

    Ответ: compactMap. Например: ["1", "a", "3"].compactMap { Int($0) } вернёт [1, 3].

  6. Вопрос: Что такое trailing closure и зачем оно нужно?

    Ответ: Это синтаксис, при котором замыкание пишется после скобок вызова метода. Это делает код чище: numbers.map { $0 * 2 } вместо numbers.map({ $0 * 2 }).

  7. Вопрос: Как проверить, все ли элементы массива положительные?

    Ответ: Использовать метод allSatisfy: numbers.allSatisfy { $0 > 0 }.

  8. Вопрос: Чем отличается first(where:) от filter?

    Ответ: first(where:) возвращает только первый подходящий элемент (или nil) и прекращает поиск после нахождения. filter возвращает массив всех подходящих элементов.

  9. Вопрос: Можно ли использовать map на словаре (Dictionary)?

    Ответ: Да, но map на словаре возвращает массив кортежей (ключ, значение). Для преобразования словаря в другой словарь используй mapValues или compactMapValues.

  10. Вопрос: Что произойдёт, если вызвать reduce на пустом массиве?

    Ответ: Вернётся начальное значение. Например, [].reduce(0, +) вернёт 0.

Итог и что изучить дальше

Мы прошли большой путь! Ты узнал, что методы массивов — это мощные инструменты для обработки данных без циклов и лишнего кода. Главные методы, которые нужно запомнить: