Métodos de arreglos y colecciones en Swift: guía detallada
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь. У тебя есть огромный стеллаж с книгами. Каждая книга — это отдельный элемент данных (число, строка, объект). Раньше, чтобы найти книгу по жанру «фэнтези», тебе приходилось вручную перебирать каждую книгу, записывать названия на листочек, потом сортировать их по алфавиту, а затем переписывать всё в отчёт. Это долго, скучно и легко ошибиться.
Методы массивов и коллекций в Swift — это как набор волшебных инструментов для библиотекаря. Вместо того чтобы делать всё руками, ты говоришь: «Массив, найди мне все книги жанра "фэнтези"» (метод filter). Или: «Преврати каждую книгу в её краткое описание» (метод map). Или: «Отсортируй книги по году издания» (метод sorted).
Зачем это нужно? В реальном программировании ты постоянно работаешь со списками данных: список пользователей, список товаров в корзине, список сообщений в чате, список координат на карте. Методы массивов позволяют обрабатывать эти списки быстро, красиво и без лишних ошибок. Они делают код короче и понятнее, как если бы библиотекарь использовал робота-помощника вместо ручного труда.
💡 Важно: Методы массивов не изменяют оригинальный массив (если не сказано иное). Они создают новый массив с результатом. Это безопасно — ты не потеряешь исходные данные.
Теория: как это работает изнутри
Давай заглянем «под капот» Swift. Массив — это упорядоченная коллекция элементов одного типа. В памяти компьютера элементы хранятся рядом друг с другом (последовательно). Это как вагоны поезда: каждый вагон знает свой номер, и чтобы добраться до 5-го вагона, нужно пройти через 1-й, 2-й, 3-й и 4-й.
Когда ты вызываешь метод, например filter, Swift делает следующее:
- Создаёт новый пустой массив (новый «поезд»).
- Проходит по каждому элементу исходного массива (по каждому вагону).
- Проверяет условие (например, «книга — фэнтези?»).
- Если условие верно — копирует элемент в новый массив.
- Возвращает новый массив.
Метод 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)
}
Вопросы для самопроверки
-
Вопрос: Какая разница между
mapиfilter?Ответ:
mapпреобразует каждый элемент массива (количество элементов не меняется).filterотбирает элементы по условию (количество элементов может уменьшиться). -
Вопрос: Что вернёт
[1, 2, 3].reduce(0, +)?Ответ: 6. Начинаем с 0, прибавляем 1 → 1, прибавляем 2 → 3, прибавляем 3 → 6.
-
Вопрос: Почему после вызова
numbers.map { $0 * 2 }массивnumbersне изменился?Ответ: Потому что
mapвозвращает новый массив, не изменяя исходный. Нужно присвоить результат:numbers = numbers.map { $0 * 2 }. -
Вопрос: В чём разница между
sorted()иsort()?Ответ:
sorted()возвращает новый отсортированный массив, не изменяя оригинал.sort()сортирует массив на месте (изменяет оригинал) и доступен только дляvar. -
Вопрос: Какой метод использовать, чтобы превратить массив строк в массив чисел, отбросив некорректные значения?
Ответ:
compactMap. Например:["1", "a", "3"].compactMap { Int($0) }вернёт[1, 3]. -
Вопрос: Что такое trailing closure и зачем оно нужно?
Ответ: Это синтаксис, при котором замыкание пишется после скобок вызова метода. Это делает код чище:
numbers.map { $0 * 2 }вместоnumbers.map({ $0 * 2 }). -
Вопрос: Как проверить, все ли элементы массива положительные?
Ответ: Использовать метод
allSatisfy:numbers.allSatisfy { $0 > 0 }. -
Вопрос: Чем отличается
first(where:)отfilter?Ответ:
first(where:)возвращает только первый подходящий элемент (или nil) и прекращает поиск после нахождения.filterвозвращает массив всех подходящих элементов. -
Вопрос: Можно ли использовать
mapна словаре (Dictionary)?Ответ: Да, но
mapна словаре возвращает массив кортежей (ключ, значение). Для преобразования словаря в другой словарь используйmapValuesилиcompactMapValues. -
Вопрос: Что произойдёт, если вызвать
reduceна пустом массиве?Ответ: Вернётся начальное значение. Например,
[].reduce(0, +)вернёт 0.
Итог и что изучить дальше
Мы прошли большой путь! Ты узнал, что методы массивов — это мощные инструменты для обработки данных без циклов и лишнего кода. Главные методы, которые нужно запомнить:
- map — преобразование каждого элемента
- filter — отбор по условию
- reduce