Как мы все знаем, структуры — это типы значений, которые легковесны, быстры и безопасны для создания моделей и DTO (Data Transfer Object). Однако их неправильное использование может повлиять на производительность приложения.
В этой статье мы обсудим ошибки, которые могут замедлить работу приложения.
Цена больших значений в Swift
Как вы знаете, структуры — это типы значений, то есть при передаче или назначении они создают копию.
Проще говоря, когда мы присваиваем значение одной структуры другой, Swift создаёт новую независимую копию этой структуры.
Давайте разберёмся на примере:
struct Apple {
var color: String
}
var apple1 = Apple(color: "Red")
var apple2 = apple1
Здесь и apple1, и apple2 — независимые объекты; любые изменения в apple1 не влияют на объект apple2.
В приведённом выше примере есть только одно свойство с именем color, и при копировании структуры всё работает нормально.
Но что произойдёт, если наша структура станет слишком большой — например, если структура будет содержать много вложенных структур?
Копирование этого объекта может быть затратным и существенно повлиять на производительность.
Давайте разберёмся на примере:
struct UserProfile {
let name: String
let bio: String
let posts: [Post]
let followers: [Followers]
let following: [Followings]
}
Здесь вы видите структуру UserProfile, которая содержит множество других структур (таких как публикации, подписчики и подписки).
Давайте используем этот UserProfile где-нибудь.
func updateBio(for profile: UserProfile, with newBio: String) -> UserProfile {
var newProfile = profile
newProfile = UserProfile(
name: profile.name,
bio: newBio,
posts: profile.posts,
followers: profile.followers,
following: profile.following
)
return newProfile
}
Хотя мы просто обновляем bio, нам всё равно приходится копировать все эти массивы структуры (посты, подписчики, на кого подписаны).
Это сильно влияет на память и производительность.
Чтобы решить эту проблему, вместо того, чтобы помещать всё в структуру, мы переместим большие данные в класс.
Таким образом, копируется только небольшая структура, а большие массивы переносятся в класс. Поскольку класс является ссылочным типом, его экземпляр не копируется, а только используется совместно.
final class UserDataStore {
var posts: [String] = []
var followers: [String] = []
var following: [String] = []
}
struct UserProfile {
let name: String
let bio: String
let store: UserDataStore
}
Как видите, мы переместили массив структур внутрь класса UserDataStore.
Теперь, когда нам нужно изменить свойство bio, мы сделаем это следующим образом:
func updateBio(_ profile: UserProfile, newBio: String) -> UserProfile {
var newProfile = profile
newProfile.bio = newBio
return newProfile
}
Здесь, когда мы копируем профиль в newProfile, копируются только имя и биография, а не хранилище (потому что это класс).
Думаю, к этому моменту мы уже лучше разобрались!
Оптимизации Swift (Copy-on-Write)
Давайте разберёмся на примере:
struct UserProfile {
let name: String
let bio: String
let posts: [String]
let followers: [String]
let following: [String]
}
В приведённом выше коде нет вложенных структур; есть только String и [String]. По сути, Swift использует хитрую оптимизацию, называемую Copy-on-Write (COW), для собственных типов данных, таких как String, Int, Array, Dictionary и т.д.
Это означает, что мы можем записать в структуру столько значений, сколько нам нужно, без ущерба для производительности, но значения должны быть собственных типов, таких как Int, String, Array и т. д.
Swift упоминает об этом в документации по оптимизации. Вы также можете ознакомиться с ответами на Stack Overflow.
Использование структур для общего изменяемого состояния
Выше мы хорошо разобрались со структурами, но некоторые разработчики часто рассматривают их как ссылочные типы (которые могут иметь общее состояние).
Давайте разберём это на примере:
struct Counter {
var count: Int
}
var counter1 = Counter(count: 0)
var counter2 = counter1
Здесь мы скопировали counter1 в counter2, но это не значит, что изменения в counter1 также повлияют на counter2.
counter1.count += 1 print(counter2.count) // 0
Здесь мы обновляем значение счётчика объекта counter1, а не counter2. Вывод значения counter2 даёт его результат, а не счётчик counter1.
Это связано с тем, что counter1 и counter2 — это разные независимые объекты.

