В Swift основные строительные блоки для создания типов данных — это классы и структуры.
Несмотря на внешнее сходство, у них есть фундаментальные различия, которые влияют на то, как они работают в памяти и как их следует использовать.
Понимание этих различий — ключ к написанию эффективного и предсказуемого кода.
Прежде чем углубляться в различия, посмотрим, что у них общего.
И классы, и структуры предоставляют схожий интерфейс для определения новых типов данных.
| Общая черта | Описание |
|---|---|
| Свойства | Могут хранить значения (переменные и константы) |
| Методы | Могут содержать функции, которые работают со свойствами и не только |
| Инициализаторы | Могут иметь специальные методы init для настройки начального состояния |
| Расширения | Можно расширять функциональность с помощью extension |
| Протоколы | Могут соответствовать протоколам, реализуя их требования |
Пример определения класса и структуры выглядит схоже:
/// Класс
class PersonClass {
var name: String
var age: Int
/// Обязательно указать инициализатор
init(name: String, age: Int) {
self.name = name
self.age = age
}
func greet() {
print("Привет, меня зовут \(name)")
}
}
/// Структура
struct PersonStruct {
var name: String
var age: Int
/// Стандартный инициализатор (name: String, age: Int) создан автоматически
func greet() {
print("Привет, меня зовут \(name)")
}
}
Теперь рассмотрим ключевые различия, которые определяют поведение этих типов.
Классы) против типа-значение (Структуры)Давайте рассмотрим фундаментальное различие.
Пример с классом:
class Car {
/// Модель автомобиля
var model: String
/// Инициализатор
init(model: String) {
self.model = model
}
}
var car1 = Car(model: "Tesla")
var car2 = car1
// Меняем модель только у car2
car2.model = "BMW"
// ОБРАТИТЕ ВНИМАНИЕ – НАЗВАНИЕ МОДЕЛИ ИЗМЕНИЛОСЬ У ОБЕИХ МАШИН
print(car1.model) // BMW
print(car2.model) // BMW
ЭТО И ЕСТЬ ФУНДАМЕНТАЛЬНОЕ ОТЛИЧИЕ, ОБЯЗАТЕЛЬНО УЯСНИТЕ
Обе переменные ссылаются на один и тот же объект в памяти.
Когда мы сказали, что var car2 = car1 фактически мы не создали новый независимый объект, мы просто сказали, что теперь у нас есть объект car2 который ссылается на ту же ячейку в памяти, что и car1
Если мы изменим car1 или car2 то копия другого объекта так же претерпит изменения так как они ссылаются на один и тот же адрес в памяти.
Что значит “ссылаются на один и тот же адрес в памяти” ?
Дело в том, что когда мы создаём класс – ему выделяется место в памяти (представьте себе сейф, с кучей ячеек) это место (ячейка) обозначается неким идентификатором (ссылкой), по которой мы можем найти этот созданный объект.
Скажем, у нас есть созданный car1 и хранится он по адресу 0x6000001436a0 и теперь, когда мы создадим сколь угодно много объектов на основе car1 (var car2 = car1, var car3 = car2…)
все они будут ссылаться на один и тот же адрес памяти и делить между собой одну ссылку 0x6000001436a0, а следовательно если мы будем менять любой из них – изменятся все.
Поэтому, когда мы говорим, что например car2.model = "УАЗ" это значит, что все остальные car1, car3 и так далее тоже изменят свою модель, даже несмотря на то, что мы их и не трогали.
Это очень важный принцип классов, это нужно тщательно усвоить, потому, что на практике, когда перед вами встанет выбор – какой из типов данных использовать, нужно отталкиваться от того, какое поведение вы желаете увидеть, при изменении вашего объекта.
Давайте представим, что пользователь заполняет данные о себе на трёх экранах:
Так как у нас есть куча независимых экранов, на которых мы дополняем информацию о пользователе – нам удобно выбрать в качестве структуры данных для пользователя – класс и постепенно наполнять его данными.
В таком случае, где бы мы не поменяли данные у нашего реального объекта userLoh он всегда будет иметь актуальную информацию.
При переходе с одного экрана на другой, у нас будут меняться классы этих экранов (SignUpStep1ViewModel(user: userLoh), SignUpStep2ViewModel(user: userLoh)…), но пользователь всегда будет одним и тем же, на каком из экранов мы бы его не изменили – информация всегда будет актуальна, что очень удобно.
Пример со структурой:
struct Car {
var model: String
}
var car1 = Car(model: "Tesla")
var car2 = car1
car2.model = "BMW"
/// Car1 и Car2 полностью независимые копии
print(car1.model) // Tesla
print(car2.model) // BMW
Здесь car2 — независимая копия. Изменения в одной не влияют на другую.
Как вы, я надеюсь, заметили – в случае со структурой объект car2 полностью независим от car1, впрочем как и car1 полностью независим от car2.
Если мы накопируем миллиард каров, никакой из них абсолютно никак не будет влиять на другие.
Дело в том, что структуры имеют иное поведение, они хранятся в совершенно другом месте нежели классы.
Структуры хранятся в стеке (это прям совсем рядышком с процессором – ооочень быстрое место, процессор за доли наносекунд способен работать со структурами, но размер этого хранилища (Стека) ограничен). Стек - имеет чёткий порядок элементов.
Классы же хранятся в куче – это своеобразная помойка, она находится дальше от дома (процессора) и гораздо больше нежели стек (в котором хранятся структуры) – классы медленнее и там ещё бегают крысы – это очень плохо.
Дело в том, что с классами случаются проблемы, особенно когда речь заходит о многопоточном программировании, а всё из-за того, что они делят одну ссылку на всех + в отличии от стурктур, которые хранятся в чётком порядке одна за другой – у классов полный беспорядок, за ними постоянно нужно следить.
К счастью – в подавляющем большинстве случаев всё происходит автоматически за счёт ARC (Автоматическая система, которая удаляет ненужные классы, как только на них больше никто не ссылается)
Представьте, что в комнате сидят четыре человека и играют в Мафию. В колоде только одна карта Мафии.
Колода это класс.
В адекватном мире, мы сначало вручим колоду в руки Пете, затем Маше, потом Васе и наконец дойдём до Еблантия. Каждый из них по очереди достанет карту – проблем нет.
Но давайте представим, что мы живём в мире, где возможно многопоточное взятие кард из колоды.
У нас есть четыре потока (Петя, Маша, Вася, Еблантий) – теперь они могут выполнять работу одновременно.
Мы не можем предсказать кто из них возьмёт колоду первым, мы не знаем, кто из них первым вернёт колоду на место – мы не знаем абсолютно ничего. Велик шанс, что они начнут одновременно доставать карту из колоды.
Если они только посмотрят на неё и не будут менять – проблем нет, но представьте, что случится если одну и ту же колоду, в один и тот же момент будут менять четыре потока ?
Когда колода вернётся на место – мы не имеем ни малейшего представления, что с ней случилось в итоге. Ведь одновременно все потоки могут вытащить карту мафии, даже несмотря на то, что она одна в колоде.
Вполне возможно, что у нас окажется две мафии, вполне возможно, что ни одной. Ведь каждый поток изменил колоду, в то время, как этим же самым занимались другие потоки, но никто из потоков не синхронизирует свои действия с другим потоком.
Дело в том, что при синхронном программировании – все задачи выполняются шаг за шагом – мы взяли колоду, вытащили карту, отдали колоду следующему.
Но при многопоточном программировании – все могли взять колоду одновременно, одновременно вытащить карту и вернуть на место.
В этом одновременно и проблема и преимущество классов – одни и теже данные мы можем менять от куда угодно, когда угодно.
Если бы наша колода была структурой – то каждый из игроков взял бы полностью независимую копию колоды и мог бы менять её как угодно – это никак бы не отразилось на остальных.
Но ведь тогда теряется смысл игры) – одну колоду нужно поделить на всех, а не создать десять копий одной и той же колоды и менять её независимо.
Поэтому колода – должна быть классом, но нам нужно управлять потоками и не разрешать менять колоду, до тех пор, пока с ней работает другой поток.
На данном этапе можете не ломать голову по поводу многопоточности, поломаете позже.
Выбирайте структуру, когда наступит момент и окажется, что структура не подходит – переходите на класс. Таким образом вы на своём личном опыте поймёте различия, а это самое ценное.
Обычно проблему представляет ситуация, ни когда вы сталкиваетесь со системными ограничениями – тут всё просто xCode сам скажет, что вам нужен именно класс, а когда вы сталкиваетесь с логикой, которую должны продумать только вы и никто вам не подскажет.
В таком случае – всё равно выбирайте структуру (пока вы не получите достаточно опыта, что бы сразу сделать правильный выбор), но обязательно тестируйте текущее поведение и сравнивайте его с ожидаемым! (print вам в помощь)
Задайте себе вопрос:
“Если я дам кому-то этот объект, и он его изменит — хочу ли я, чтобы эти изменения увидели все?”
НЕТ → используйте структуру (фотография)
ДА → используйте класс (офис)
Вы работаете в фотолаборатории и собираетесь распечатать сотню копий одной фотографий – какой тип данных для фотографии выбрать?
// Фотография
struct Photo {
var person: String
var location: String
var date: Date
}
let original = Photo(person: "Петя", location: "Москва", date: Date())
print(original.person) // "Петя"
// Вы делаете копию для друга и подписываете её на обратной стороне
var copyOfMyPhotoForFriend = original
copyOfMyPhotoForFriend.person = "Вася ❤️ (копия для друга)"
print(original.person) // "Петя" – // Оригинал НЕ изменился!
print(copyOfMyPhotoForFriend.person) // "Вася ❤️ (копия для друга)"
Очевидно, что когда я сфотографирую свой прекрасный торс и отдам копию этой фотографии своему другу, в тот момент, когда он решит пририсовать мне член на лбу – разумеется, я не ожидаю, что и на моей оригинальной фотографии у меня появятся подобные, вульгарные изменения.
Но если бы я выбрал class для Photo – то именно так и произошло бы, член на лбу пририсовался и на копии фотографии, сделанной для друга и на моей оригинальной, которую никто не трогал!
Всё зависит от конкретной ситуации, от логики, которая ожидается – возможно, если бы мы участвовали в съёмках “Гарри Поттера”, то именно так и должно быть по сценарию – Гарри дарит копию своей обнажённой фотографии Драко Малфою, тот пририсовывает нимбус на своей копии и Оооо чудо – нимбус пририсовался и на оригинальной фотографии Гарри – ММмаагия.
А теперь давайте рассмотрим другой пример –
// Наш офис
class Office {
var buildingName: String
var temperature: Int
init(name: String, temp: Int) {
self.buildingName = name
self.temperature = temp
}
}
// Мы создали наш офис
let cityOffice = Office(name: "Moscow-City", temp: 22)
// Петя и Вася получили свои места в Офисе
petya.office = cityOffice
vasya.office = cityOffice
// Вася изменил температуру
vasya.office.temperature = 24
// Разумеется его коллега Петя почувствует это изменение, они же в одном офисе!
// Вася же не может поменять температуру в офисе только для себя
print(petya.office.temperature) // 24
Когда в одном офисе работают разные люди и кто-то один меняет настройки кондиционера, очевидно, что все присутствующие так или иначе это почувствуют.
Класс хорош для ситуации, когда несколько объектов делят один ресурс.
Структуры не поддерживают наследование!
| Классы | Структуры |
|---|---|
| Поддерживают наследование | Не поддерживают наследование |
Могут переопределять методы (override) |
Не могут переопределять |
Могут вызывать методы суперкласса (super) |
Нет понятия суперкласса |
Пример наследования в классе:
class Animal {
func speak() {
print("...")
}
}
class Dog: Animal {
override func speak() {
print("Гав!")
}
}
Со структурами такое невозможно.
| Классы | Структуры |
|---|---|
Могут иметь деинициализаторы deinit |
Не могут иметь деинициализаторы |
deinit вызывается перед освобождением объекта |
— |
Пример деинициализатора:
class FileHandler {
var filename: String
init(filename: String) {
self.filename = filename
}
deinit {
print("Закрываем файл \(filename)")
}
}
Пример со структурой:
Если у нас в структуре есть функция, которая меняет структуру (присваивает иное значение полю), то такая функция должна быть помечена как mutating (xCode подскажет)
struct Point {
var x = 0.0
var y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
let fixedPoint = Point(x: 3, y: 3)
// fixedPoint.moveBy(x: 2, y: 3) // Ошибка! Нельзя вызвать mutating метод у константы
var movablePoint = Point(x: 3, y: 3)
movablePoint.moveBy(x: 2, y: 3) // OK mutating можно вызвать только у var
В классе такого ограничения нет:
class PointClass {
var x = 0.0
var y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}
let point = PointClass()
point.moveBy(x: 2, y: 3) // Работает, даже если экземпляр константа
Мы не должны писать mutating func мы спокойно можем вызывать функцию у let (константы)
| Классы | Структуры |
|---|---|
Можно сравнивать ссылки с помощью операторов === и !== (равны ли ссылки на один объект) |
Для структур такого оператора нет; обычно сравнивают по свойствам через ==, если структура соответствует Equatable |
Пример сравнения классов:
Благодаря тернарному знаку равно (===) мы можем проверить, что данные в классе не просто одинаковы, а то, что они имеют одну и ту же ссылку и следовательно ссылаются на одно и тоже место в памяти.
let carA = Car(model: "Tesla")
let carB = carA
let carC = Car(model: "Tesla")
print(carA === carB) // true (одна и та же ссылка)
print(carA === carC) // false (разные объекты), хотя модели абсолютно равны (Tesla == Tesla)
Структуры не поддерживают ===, так как у них нет ссылок.
Для удобства сведём все отличия в одну таблицу.
| Характеристика | Класс | Структура |
|---|---|---|
| Тип | Ссылочный |
Значимый |
| Хранение | Куча (heap) |
Стек (stack) |
| Присваивание | Копируется ссылка |
Копируется значение |
| Наследование | Поддерживается |
Не поддерживается |
| Деинициализатор | Присутствует (deinit) |
Отсутствует |
| Mutating методы | Не нужны |
Требуют mutating |
| Идентичность | ===, !== |
Нет |
| Циклические ссылки | Возможны |
Нет |
| Использование в коллекциях | Хранятся ссылки |
Хранятся копии |
| Производительность | Накладные расходы на кучу и ARC |
Эффективнее (стек) |
Разницу между классом и структурой – нужно чувствовать сердцем, а это приходит только с опытом.
Подобного рода отличия, практически не возможно уяснить теоретически – как и сказал выше, они придут с опытом, когда вы пару раз ошибётесь с выбором.
Классы и структуры в Swift имеют много общего, но их поведение в памяти и возможности кардинально различаются.
Понимание этих отличий помогает писать безопасный, эффективный и понятный код.