Это звучит элегантно. Структуры как значимые типы — неизменяемые, предсказуемые, потокобезопасные. Я в это влюбился. Фактически, я структурировал почти всю свою кодовую базу вокруг них. Все — от моделей до логики представления, сетевых оберток и даже анимаций — я пытался сделать все это с помощью структур.
Но с реальным опытом, особенно в крупномасштабных производственных приложениях, у меня был небольшой звоночек для пробуждения. Дело было не в том, что структуры плохи — они фантастические. Но рассматривать их как единственный ответ? Такой образ мышления в конечном итоге сжег меня.
Вот почему я перестал использовать структуры для всего в Swift — и почему вы тоже можете захотеть переосмыслить это.
1. Структуры не всегда ведут себя как типы значений
Нам говорят, что структуры копируются при назначении или передаче. Звучит безопасно, не так ли?
Но Swift использует копирование при записи (COW). Это означает, что под капотом Swift фактически разделяет хранилище между копиями — пока вы не измените одну из них.
Допустим, у вас есть:
struct UserProfile {
var name: String
var age: Int
}
var user1 = UserProfile(name: "Asha", age: 28)
var user2 = user1
user2.age = 30
print(user1.age) // 28 ✅ - Seems safe.
Пока все хорошо.
Но теперь попробуйте сделать это со ссылочным типом внутри вашей структуры:
class Address {
var city: String
init(city: String) {
self.city = city
}
}
struct User {
var name: String
var address: Address
}
var original = User(name: "Raj", address: Address(city: "Delhi"))
var copy = original
copy.address.city = "Bangalore"
print(original.address.city) // ❌ Output: "Bangalore"
Подождите, что? Мы же скопировали структуру, верно? Так почему же изменение копии повлияло на оригинал?
Потому что структуры не копируют свое содержимое глубоко. Если структура содержит ссылочные типы, эта ссылка является общей для всех копий.
Это разрушает иллюзию семантики значений и может привести к неожиданным ошибкам — особенно когда ваши структуры увеличиваются в размере и сложности.
2. Замыкания в структурах могут иметь обратный эффект
Вот хитрый момент, который попался мне во время интервью.
У меня была структура, хранящая замыкание. Все казалось безобидным:
struct Counter {
var count = 0
var onIncrement: (() -> Void)?
}
Теперь я инициализирую и назначаю замыкание:
var counterA = Counter()
counterA.onIncrement = { counterA.count += 1 }
var counterB = counterA
counterB.onIncrement?()
print(counterB.count) // 0
print(counterA.count) // 1 ❗️
Что происходит?
Это замыкание захватило counterA. Несмотря на то, что мы скопировали его в counterB, замыкание все еще указывает на оригинал.
Итак, теперь у вас есть тип значения (структура), который ведет себя как ссылка. Это большой красный флаг — особенно в асинхронном или многопоточном коде.
Фактически, Swift не позволит вам изменять self изнутри экранирующего замыкания в структуре:
struct Timer {
var seconds = 0
mutating func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.seconds += 1 // ❌ Error: Escaping closure captures mutating 'self'
}
}
}
Swift говорит: «Не делайте этого со структурами». И это правильно.
3. Жизненный цикл имеет значение — классы дают вам это
Вот сценарий: вы создаете SessionManager для обработки входа, обновления токена и выхода из системы. Должен ли он быть структурой?
Интуитивно структуры кажутся легкими и чистыми для решения задачи. Но подумайте об этом:
- Вам нужна деинициализация
- Вы хотите хранить слабые ссылки, чтобы избежать циклов удержания
- Вы можете передать это контроллеру представления или координатору
Здесь лучше подойдет класс. Пример:
class SessionManager {
var token: String?
func login() {
print("Logging in...")
}
deinit {
print("SessionManager deinitialized")
}
}
Вы получаете контроль жизненного цикла, совместное использование ссылок и инструменты управления памятью, такие как weak и unowned, — ни один из которых не доступен в структурах.
Попытка смоделировать это поведение с помощью структур только усложнит ситуацию.
4. Общее состояние и идентичность: структуры не могут помочь
Структуры не имеют идентичности. Если вам нужно узнать, указывают ли два экземпляра на один и тот же объект, это могут сделать только классы:
class NetworkClient {}
let a = NetworkClient()
let b = a
print(a === b) // ✅ true
С помощью структур вы не можете сравнивать идентичность — только значения. Это опасно, если вы пытаетесь моделировать общие объекты с помощью структур.
Допустим, вы моделируете MusicPlayer:
struct MusicPlayer {
var isPlaying = false
mutating func play() {
isPlaying = true
}
}
Теперь передайте его:
var player1 = MusicPlayer() var player2 = player1 player2.play() print(player1.isPlaying) // false ❌
Это нормально для независимых типов значений. Но если логика вашего приложения предполагает общее состояние (т. е. оба контроллера работают с одним и тем же игроком), вы только что внесли ошибку.
5. Производительность? Не думайте, что структуры быстрее
Часто говорят, что структуры быстрее, потому что они размещаются в стеке. Это отчасти правда, но не всегда.
В реальных приложениях большинство данных находятся в куче — особенно если ваши структуры:
- Слишком большие (массивы, словари)
- Вложенные
- Используются с замыканиями или асинхронными API
Swift в любом случае размещает структуры в куче в этих случаях.
С другой стороны, классы могут избежать ненужных копий благодаря ARC. Они передаются по ссылке, а память освобождается детерминированно (при правильном использовании).
Вот случай, когда я увидел лучшую производительность Я переключился в большой модели JSON со структуры на класс и стало лучше — потому что я часто ее мутировал, и копии структур замедляли работу.
Так что теперь я не предполагаю, что структуры «быстрее». Я измеряю, профилирую и делаю вывод на основе реальных чисел.
Простое практическое правило
После нескольких болезненных ошибок и нескольких радостных рефакторингов вот как я теперь решаю:
Одна реальная ошибка (и ее исправление)
Ошибка: API-запрос как структура
struct APIRequest {
var endpoint: String
var onComplete: ((Result<Data, Error>) -> Void)?
}
Я думал, что это безопасно. Но потом у меня появился механизм повтора:
var req = APIRequest(endpoint: "/login")
req.onComplete = { result in
print("Response received")
}
retry(request: req)
Где-то внутри retry структура скопировалась. Замыкание? Все еще указывает на оригинал. Я потерял счет тому, какой экземпляр фактически завершился.
Исправление: переключиться на класс
class APIRequest {
let endpoint: String
var onComplete: ((Result<Data, Error>) -> Void)?
init(endpoint: String) {
self.endpoint = endpoint
}
}
Просто. Надежно. Никаких скрытых копий. Жизнь стала лучше.
Заключение
Структуры Swift потрясающие. Но они не молоток для каждого гвоздя. Если вы относитесь к ним как к значениям по умолчанию для всего, вы подписываетесь на тонкие ошибки, неудобные обходные пути и ненужные рефакторинги в дальнейшем.
Я все еще использую структуры — часто. Но теперь я спрашиваю:
- Будет ли этот тип общим?
- Будет ли он содержать замыкания?
- Требует ли он слабых ссылок или жизненного цикла?
- Нужно ли мне отслеживание идентичности или мутаций?
Если ответ на любой из этих вопросов «да», я без колебаний тянусь к классу.
Поверьте мне: зрелость в Swift заключается не в том, чтобы всегда использовать структуры. Она в том, чтобы знать, когда этого не делать.

