Перейти к содержимому
Теория · Классы · урок 1 из 10

Классы

Чему вы научитесь

Чему вы научитесь:

  • объявлять класс с init и объяснять, почему два имени могут вести к одному экземпляру;
  • понимать, что let у класса закрепляет только ссылку, а не сам экземпляр;
  • наследовать класс, переопределять методы и проверять тип через is и as?;
  • выбирать между структурой и классом и объяснять выбор на собеседовании.
Один лист obj, к которому привязаны две бирки на нитях — a и b

Представьте лист бумаги с заголовком — документ. К листу на длинных нитях привязаны две бирки: на одной написано a, на другой — b. Потяните за любую бирку, и вы придёте к одному и тому же листу.

Перепишите строчку, придя по бирке b, и по бирке a вы увидите уже новую строчку. Лист один, к нему ведут две нити.

Так устроены общие онлайн-документы: ссылку на документ можно раздать многим людям, документ при этом остаётся один, и правку любого из них видят все.

В теме о структурах var b = a делало ксерокопию бланка, и копию заполняли отдельно. Класс устроен как общий документ. Посмотрим, как это выглядит в коде:

SWIFT
class Document {
    var title = "Plan"
}

var a = Document()
var b = a
b.title = "Final"
print(a.title) // Final

Давайте разберём программу по строкам:

  1. class Document объявляет класс с одним свойством title. Как у структуры, у свойства есть значение по умолчанию.
  2. var a = Document() создаёт экземпляр класса — тот самый лист — и даёт имени a бирку к нему.
  3. var b = a не создаёт второй лист. Имя b получает вторую бирку к тому же экземпляру.
  4. b.title = "Final" переписывает заголовок общего листа.
  5. print(a.title) печатает Final: бирка a ведёт к тому же листу, и правка видна.

Будь Document структурой, программа напечатала бы Plan: b была бы отдельной копией.

Зачем нужен такой тип? Бывает, что несколько частей программы работают с одной и той же вещью: с банковским счётом, с персонажем игры, с открытым документом. Если каждая часть получит свою копию, изменения разойдутся. Класс даёт всем частям программы один общий экземпляр.

Ссылка вместо копии

Класс — тип, который, как и структура, объединяет свойства и методы, но при присваивании не копируется. В переменной класса лежит не сам экземпляр, а ссылка на него — та самая бирка на нити. Присваивание копирует ссылку, а экземпляр остаётся один. Экземпляр класса ещё называют объектом.

Класс — ссылочный тип, как и замыкание. А структуры, массивы, Int и String — значимые типы: у них присваивание копирует само значение.

Проверить, ведут ли две ссылки к одному экземпляру, можно оператором ===. Он отвечает true, только когда экземпляр один и тот же, а оператор !== отвечает наоборот. Работают они только с классами: у структуры нет ссылки, которую можно сравнить.

Нажимайте «Следующий шаг» и следите за a.title и b.title: они меняются вместе, потому что экземпляр один. А c появится сам по себе.

ВживуюДве ссылки, один экземпляршаг 1 из 8 · рамок: 1

Document() создаёт экземпляр, а в a попадает ссылка на него.

Следите за a.title и b.title: они меняются вместе

Каждый вызов Document() создаёт новый экземпляр — новый лист. Поэтому c никак не связан с a и b, и a === c отвечает false.

Здесь картинка с листом неточна. Настоящий документ можно скопировать кнопкой, а у класса такой кнопки нет: var b = a всегда даёт ещё одну бирку, а новый лист создаёт только вызов Document().

На собеседовании джуниору часто дают программу со структурой и спрашивают, что она напечатает, если заменить struct на class. Ответ: структура при присваивании копируется, а экземпляр класса остаётся общим, и правка через одно имя видна через другое.

Класс и init

Структура сама получает почленный инициализатор, например Point(x: 1, y: 2). Класс почленного инициализатора не получает. Если у каждого свойства есть значение по умолчанию, у класса есть пустой init(): так мы и создавали Document().

Если хотя бы у одного свойства нет значения по умолчанию, init пишут сами:

SWIFT
class Account {
    var owner: String
    var balance = 0
    init(owner: String) {
        self.owner = owner
    }
}

let anna = Account(owner: "Anna")
print(anna.owner, anna.balance) // Anna 0

Разберём, как создаётся счёт:

  1. У owner нет значения по умолчанию, поэтому его должен задать init.
  2. init(owner: String) получает владельца параметром, и self.owner = owner кладёт его в свойство нового экземпляра.
  3. balance уже равен 0, и init его не трогает.
  4. Account(owner: "Anna") вызывает этот init, а print печатает Anna 0.

Частая ошибка

Класс объявили как структуру: со свойством без значения по умолчанию и без init.

SWIFT
class Profile {
    var name: String // ✗
}
TEXT
error: class 'Profile' has no initializers

Программа не компилируется: почленного инициализатора у класса нет, и задать name нечем. Под ошибкой компилятор называет и причину — свойство name без начального значения. Исправить это можно двумя способами:

  • дать свойству значение по умолчанию: var name = "";
  • написать init(name: String), как у Account.

Главное

  • Класс объявляют словом class; в переменной класса лежит ссылка на экземпляр, а не сам экземпляр.
  • var b = a копирует ссылку: a и b ведут к одному экземпляру, и правка через одно имя видна через другое.
  • === проверяет, один ли это экземпляр; каждый вызов вроде Document() создаёт новый.
  • Класс не получает почленный инициализатор: если у свойства нет значения по умолчанию, классу нужен свой init.