Перейти к основному содержимому

Объектно-ориентированное программирование в Swift

Разработчику Архитектору
Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое или вы учите Swift с нуля, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — как это устроено в Swift.

Теория и синтаксис Swift

Понятие ООПКак выражено в Swift
АДТclass (ссылочный тип), struct / enum (значимые типы)
Инкапсуляцияprivate, fileprivate, internal, public, open
Наследованиеодин суперкласс; протоколы вместо множества базовых классов
Полиморфизм подтиповoverride, протоколы, any / some (existentials)
Параметрический полиморфизмgenerics (Array<Element>)
Модульностьмодули и import на уровне пакетов

Определения — раздел 4-08-oop. Синтаксис Swift — о разделе Swift.


ООП в Swift

Swift сочетает классическое ООП с протокол-ориентированным стилем Apple: поведение чаще выносят в protocol и extension, глубокие иерархии классов встречаются реже. Значимые типы (struct, enum) — полноценный инструмент моделирования, а не упрощённые классы.

Кратко для новичка:

  • class — ссылочный тип (ARC); struct / enum — значимые типы, копируются при присваивании.
  • protocol — контракт методов и свойств; extension добавляет реализацию по умолчанию.
  • init — конструктор; у классов двухфазная инициализация с super.init.
  • Уровни доступа (private, internal, public, open) задают границы API модуля.

Если вы приходите из Java или C#, полезно сразу увидеть отличия:

ТемаJavaC#Swift
Значимые типытолько примитивы / обёрткиstruct, recordstruct, enum — первый выбор для моделей
Наследование классоводин базовый классодин базовый классодин суперкласс; множественное — через протоколы
Интерфейсыinterfaceinterfaceprotocol + расширения с реализацией по умолчанию
Память классовGCGCARC — детерминированный подсчёт ссылок
Абстрактные классыabstractabstractнет ключевого слова; эмуляция через протоколы
Модификаторы для наследованияpublic / protectedpublic / protected / internalopen (наследование снаружи модуля), public (без наследования снаружи)
NullnullnullOptional (?)
Переопределение@Overrideoverrideoverride; запрет — final

Интерактивная схема — класс и объект (псевдокод, подходит для любого ООП-языка). Полный разбор принципов: ООП в разделе "Код и разработка".

КЛАСС Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ

объект barsik := новый Кот(имя="Барсик", возраст=3)
barsik.мяукнуть()

Псевдокод отделяет описание типа (КЛАСС Кот) от конкретного экземпляра (barsik). Поля задают состояние, метод — поведение; новый Кот(...) соответствует вызову инициализатора.

Play ITЗагрузка интерактивного демо…


Четыре столпа ООП в Swift

СтолпСутьВ Swift
АбстракцияСкрыть детали, оставить существенноеprotocol, публичный API класса, public/internal границы модуля
ИнкапсуляцияДанные + методы доступа под контролем типаprivate поля, вычисляемые свойства, валидация в set
НаследованиеОбщая база + специализация: SuperClass, override, цепочка init
ПолиморфизмОдин интерфейс — разные реализацииoverride, протоколы, any Drawable

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…


Пример класса

Код ITЗагрузка примера кода…

Разбор примера:

  • class Unit — ссылочный тип: две переменные могут указывать на один объект, изменения видны через обе ссылки.
  • Хранимые свойства (health, strength) задают состояние; damageвычисляемое свойство без собственного хранилища.
  • attack(target:) демонстрирует внешние имена параметров: вызов читается как фраза warrior.attack(target: mage).
  • let warrior = Unit() фиксирует ссылку, но не запрещает менять warrior.health — класс остаётся изменяемым.
  • Память освобождается автоматически через ARC, когда счётчик ссылок обнуляется.

Ключевое слово class определяет новый класс. Имя класса начинается с заглавной буквы по соглашению о стиле кодирования Swift. Тело класса ограничено фигурными скобками. Класс представляет шаблон для создания объектов с единым набором свойств и поведения.

Свойство damage объявлено как вычисляемое без хранимого значения. При каждом обращении выполняется вычисление на основе текущих характеристик объекта — актуальный урон без ручного обновления.

Swift использует автоматический подсчёт ссылок (ARC) для управления памятью классов. Разработчику не требуется вручную выделять и освобождать память, но важно понимать циклы сильных ссылок (см. раздел ARC).


Инициализаторы

В Swift инициализатор — это метод с именем init. Компилятор требует полной инициализации всех хранимых свойств до завершения конструктора (двухфазная инициализация для классов).

Виды инициализаторов

ВидСинтаксисНазначение
Designated (основной)init(...)Главный путь создания; инициализирует все свойства; в подклассе вызывает super.init
Convenienceconvenience init(...)Вспомогательный; обязан делегировать в self.init(...) того же класса
Requiredrequired init(...)Все подклассы обязаны реализовать этот инициализатор
Failableinit?(...) / init!(...)Возвращает nil при невалидных данных; init! — неявно разворачиваемый optional
class Document {
let id: String
var title: String

// Designated
init(id: String, title: String) {
self.id = id
self.title = title
}

// Convenience — всегда через self.init
convenience init(title: String) {
self.init(id: UUID().uuidString, title: title)
}

// Failable — nil, если id пустой
init?(id: String) {
guard !id.isEmpty else { return nil }
self.id = id
self.title = "Без названия"
}
}

class SecureDocument: Document {
let clearance: Int

init(id: String, title: String, clearance: Int) {
self.clearance = clearance
super.init(id: id, title: title)
}

// required — подкласс обязан сохранить фабричный путь
required init(title: String) {
self.clearance = 0
super.init(title: title)
}
}

Правила для классов:

  1. Сначала инициализируются собственные свойства подкласса, затем super.init.
  2. convenience init не может вызывать super.init напрямую — только другой init того же класса.
  3. required init нужен, когда базовый класс требует единообразного способа создания (например, при NSCoding / Decodable).

Для struct и enum: компилятор синтезирует memberwise init, если не объявлены свои. Failable init полезен для парсинга: init?(rawValue:) у enum или валидации входных данных.

Фазы инициализации и цепочка в иерархии

Код ITЗагрузка примера кода…

Двухфазная инициализация: фаза 1 — всем свойствам заданы значения; фаза 2 — можно вызывать методы self и обращаться к свойствам. Компилятор проверяет порядок на этапе сборки.

Код ITЗагрузка примера кода…

В иерархии наследования каждый уровень отвечает за свои свойства; super.init передаёт управление родителю после локальной инициализации.

Код ITЗагрузка примера кода…

required гарантирует, что любой подкласс предоставит тот же контракт создания — иначе код, вызывающий Type.init(...), сломается при подстановке наследника.

Код ITЗагрузка примера кода…

Создание экземпляра — вызов подходящего init. Failable init требует разворачивания: if let doc = Document(id: "") { ... }.

Сравнение с Java и C#

JavaC#Swift
Основной конструкторимя классаимя классаinit
Цепочка конструкторовthis(...): this(...)convenience initself.init
Вызов базовогоsuper(...): base(...)super.init(...) после своих полей
Неудачное созданиеисключение / Optional (Java)исключениеinit?nil
Обязательный для наследниковнет ключевого слованетrequired init

В C# именованные и опциональные параметры конструктора часто заменяют набор convenience init в Swift. В Java несколько перегруженных конструкторов эквивалентны нескольким init с разными сигнатурами; convenience при этом не нужен, если каждый инициализатор сам задаёт все поля.


Свойства

Хранимые и вычисляемые

Хранимое (stored)Вычисляемое (computed)
ПамятьЗанимает место в экземпляреТолько код get / set
Синтаксисvar name: Typevar area: Double { get { ... } }
letКонстанта после инициализацииТолько get (read-only)
Типичное применениеПоля моделиПроизводные значения (area, isEmpty)
struct Rectangle {
var width: Double
var height: Double

// Только get — read-only computed
var area: Double { width * height }

// get + set — set пишет в хранимые поля
var size: (Double, Double) {
get { (width, height) }
set { width = newValue.0; height = newValue.1 }
}
}

В классах вычисляемые свойства не объявляют lazy. В структурах изменение свойств из методов требует mutating (см. Методы).

Наблюдатели willSet и didSet

Наблюдатели вешаются на хранимые свойства (в том числе с дефолтным значением). willSet получает newValue, didSet — старое значение в oldValue.

class TemperatureSensor {
var temperature: Double = 20.0 {
willSet { print("Будет \(newValue)°C") }
didSet {
if temperature > 100 { print("Критическая температура!") }
}
}
}

Отличие от set у computed property: наблюдатели реагируют на присваивание, а не на пересчёт при чтении. Для валидации входа удобнее кастомный set у computed или проверка в didSet с откатом.

Ленивые свойства

lazy var откладывает инициализацию до первого чтения. Применимо к var экземпляра (часто в классах). Замыкание инициализации { ... }() выполняется один раз.

class DataManager {
lazy var databaseConnection: DatabaseConnection = {
print("Подключение к БД")
return DatabaseConnection()
}()
}

lazy несовместим с let и с вычисляемыми свойствами: компилятор должен знать адрес хранилища до первого доступа. В многопоточной среде первый доступ к lazy синхронизируется — но полагаться на побочные эффекты при инициализации не стоит.

Свойства в Java и C#

В Java поля чаще публичные или с парой getX/setX; в C# — идиоматичны свойства public int Health { get; set; }. В Swift отдельного синтаксиса get/set в объявлении хранимого свойства нет: наблюдатели willSet/didSet дополняют stored property, а валидация часто оформляется через computed property с приватным backing storage:

class Account {
private var _balance: Decimal = 0
var balance: Decimal {
get { _balance }
set {
guard newValue >= 0 else { return }
_balance = newValue
}
}
}

Код ITЗагрузка примера кода…


Методы

Методы экземпляра

Методы экземпляра работают с self и имеют доступ ко всем свойствам объекта.

Код ITЗагрузка примера кода…

Методы типа (static / class)

Вызываются через имя типа: MathUtilities.factorial(5). У классов class func допускает переопределение в подклассе; static func — нет.

Код ITЗагрузка примера кода…

mutating для значимых типов

У struct и enum метод, меняющий self, помечается mutating — явный сигнал копирования-on-write и безопасности при работе с let-константой контейнера.

Код ITЗагрузка примера кода…

Имена параметров и метки

У методов внешние имена параметров по умолчанию совпадают с внутренними; _ скрывает внешнее имя. Это отличается от Java/C#, где позиционные аргументы без меток:

func move(from start: Point, to end: Point) { }
move(from: a, to: b) // обязательные метки
func jump(_ distance: Int) { }
jump(3) // без метки

Такой API читается как предложение и считается идиоматичным в SwiftUI и Foundation.

Вложенные функции

Вложенные func внутри метода изолируют вспомогательную логику без загрязнения пространства имён типа:

class DataProcessor {
func process(data: [Int]) -> [Int] {
func filterPositive(_ value: Int) -> Bool { value > 0 }
return data.filter(filterPositive).map { $0 * $0 }
}
}

Инкапсуляция и уровни доступа

Инкапсуляция в Swift — это границы видимости + контролируемый API. Внутреннее состояние прячут за private полями; наружу отдают методы и свойства с валидацией.

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Интерфейс класса служит контрактом между разработчиком класса и его пользователями. Он определяет, как взаимодействовать с объектом, не раскрывая механизмов работы.

Пять уровней доступа

МодификаторВидимостьАналог в Java/C#
openЛюбой модуль; можно наследовать и переопределять снаружинет прямого аналога; ближе к public + разрешённое наследование
publicЛюбой модуль; нельзя наследовать/override снаружи модуляpublic
internalТекущий модуль (по умолчанию)package-private / internal
fileprivateТекущий файл
privateТекущее объявление + расширения в том же файлеprivate
public class ApiClient {
public private(set) var token: String? // читать снаружи, писать — только внутри
private let session: URLSession

fileprivate func log(_ message: String) { /* видно в этом файле */ }
}

Для фреймворков: публикуйте public API, детали держите internal/private. open class нужен редко — когда вы сознательно разрешаете subclassing извне (UIKit-подобные иерархии).

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Практика: начинайте с private, расширяйте до internal, public — только для стабильного API пакета. Минимальный публичный интерфейс упрощает рефакторинг и тестирование.


struct, class и enum

Swift предлагает три способа объявить составной тип. Выбор влияет на семантику копирования, наследование и модель памяти.

structclassenum
СемантикаЗначение (копия при присваивании)Ссылка (ARC)Значение; дискриминированное объединение
НаследованиеНет (только протоколы)Один суперклассНет
Идентичность== по содержимому (если Equatable)=== — один объект в памятиСравнение кейсов
ДеинициализаторНетdeinitНет
Когда выбиратьКоординаты, DTO, модели UIДелегаты, менеджеры, общее mutable-состояниеКонечные состояния, ошибки, AST

Enum с associated values

В отличие от Java/C# enum (именованные константы), Swift enum может нести разные данные в каждом кейсе:

enum NetworkResult {
case success(data: Data, statusCode: Int)
case failure(error: Error)
case redirect(url: URL)
}

enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)

var area: Double {
switch self {
case .circle(let r): return .pi * r * r
case .rectangle(let w, let h): return w * h
}
}
}

Это ближе к алгебраическим типам (Haskell Either, Rust enum) и заменяет иерархии классов с отдельным классом на каждый вариант.

Семантика значений и ссылок

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Правило Apple: по умолчанию выбирайте struct; переходите на class, когда нужна идентичность объекта (два экземпляра — одна и та же сущность в памяти) или deinit/общая мутабельность по ссылке.

Copy-on-write

Коллекции Swift (Array, Dictionary, String) и многие struct используют copy-on-write: физическая копия данных создаётся только при мутации, пока экземпляры разделяют буфер. Это даёт семантику значений без лишних копий — важное отличие от наивного копирования struct в C#.

Когда enum вместо иерархии классов

Если вариантов конечное число и у каждого свой набор полей — enum с associated values короче и безопаснее, чем abstract class + наследники (как в Java/C#). Компилятор проверяет исчерпывающий switch; добавление кейса выдаёт предупреждения во всех switch (при switch над enum).


Наследование

Наследование в Swift — одиночное для классов. Общее поведение для разных ветвей иерархии чаще выносят в протоколы.

Код ITЗагрузка примера кода…

Дочерний класс добавляет свойства и методы; override меняет поведение родителя. final class / final func запрещает дальнейшее переопределение.

Код ITЗагрузка примера кода…

Множественное наследование классов запрещено; вместо него — несколько протоколов: class Bird: Animal, Flyable, Codable.

Преимущества иерархии: общая логика в базовом классе, специализация в наследниках. Риск: хрупкая база — предпочитайте композицию, если отношение не строгое is-a (является).


Переопределение и перегрузка

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

  • Переопределение (override) — та же сигнатура, другая реализация в подклассе; вызов родителя — super.method().
  • Перегрузка — одно имя, разные параметры; выбор на этапе компиляции (ad hoc-полиморфизм).
  • final фиксирует реализацию; без него open/public классы в фреймворках могут неожиданно subclass'иться.

Типы, экземпляры и жизненный цикл

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Класс — ссылочный тип: === сравнивает идентичность (один адрес), == — только если вы реализовали Equatable по полям. deinit вызывается при обнулении счётчика ARC — место для закрытия ресурсов.


Композиция

Отношение имеет (has-a) вместо является (is-a): объект содержит другие объекты как поля. В Swift композиция сочетается с протоколами (protocol Engine { func start() }).

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Агрегация — слабая связь (weak var owner). Композиция — владелец создаёт и уничтожает части вместе с собой. Для разрыва циклов ARC в делегатах используйте weak.

В экосистеме Apple композиция доминирует: UIViewController имеет view, UITableView имеет dataSource (протокол), таблица не наследует источник данных. В Java/C# совет composition over inheritance совпадает, в Swift его подкрепляют протоколы и value types.


Полиморфизм

Полиморфизм подтипов: переменная базового типа или протокола указывает на объект наследника; вызывается фактическая реализация (динамическое связывание у class).

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Абстрактных классов в Swift нет. Паттерны замены:

  1. protocol + реализации в struct/class.
  2. Базовый class с fatalError в методах, которые обязан переопределить наследник (как UIView).

any Protocol (existential) хранит значение любого соответствующего типа; some Protocol — непрозрачный конкретный тип (полиморфизм с оптимизацией).


Протокол-ориентированное программирование

POP — идиома Swift: поведение описывают протоколы, общий код — в extension, типы (struct, class, enum) принимают протоколы без общего предка.

protocol Describable {
var description: String { get }
}

extension Describable {
func log() { print(description) } // реализация по умолчанию
}

struct User: Describable {
let name: String
var description: String { "User(\(name))" }
}

Преимущества перед глубоким наследованием:

  • Один тип соответствует нескольким протоколам (Equatable, Codable, CustomStringConvertible).
  • extension добавляет методы к существующим типам, в том числе из SDK (extension String: Describable).
  • Тестирование проще: подставляется mock-структура вместо subclass.

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Условное соответствие (extension Array: Equatable where Element: Equatable) добавляет протокол только когда параметры типа это позволяют — мощный инструмент обобщённого POP.

Расширения типов

extension может добавлять вычисляемые свойства, методы, вложенные типы, соответствие протоколам — без новых хранимых свойств в extension (ограничение языка).

extension Int {
var squared: Int { self * self }
}

Используйте extensions для группировки протокольных реализаций (// MARK: - UITableViewDataSource) и разделения сгенерированного/ручного кода.

POP и классическое наследование (как в Java)

Типичная Java-модель: interface Serializable + abstract class AbstractRepository + class UserRepository. В Swift чаще:

protocol Repository {
associatedtype Item
func fetchAll() -> [Item]
}

struct UserRepository: Repository {
typealias Item = User
func fetchAll() -> [User] { /* ... */ }
}

associatedtype обобщает протокол (аналог generic interface в Java/C#). Реализации — value types без накладных расходов наследования. Тесты подставляют MockRepository: Repository без subclassing.

Расширения стандартной библиотеки

Паттерн retroactive modeling: вы не владеете типом String, но добавляете протокол в extension в своём модуле:

extension URL: Identifiable {
public var id: String { absoluteString }
}

В Java/C# такое возможно только через обёртку или адаптер; в Swift extension — штатный приём.


Автоматический подсчёт ссылок

ARC действует только для ссылочных типов (class). Значимые типы (struct, enum) копируются при присваивании; счётчик ссылок не участвует.

Тип ссылкиСчётчик ARCКогда обнуляется
Strong (по умолчанию)+1При выходе из области / присвоении nil
weakне увеличиваетАвтоматически nil, когда объект уничтожен
unownedне увеличиваетПредполагает, что владелец живёт дольше; краш при обращении после dealloc

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Цикл сильных ссылок: A держит B, B держит A — ни один не освободится. Решение: weak/unowned на одной стороне (типично делегат weak var delegate).

Замыкания захватывают self сильно; в escaping-closure используйте [weak self] или [unowned self].

В Java/C# циклы collect'ятся GC; в Swift разработчик обязан разрывать retain cycles явно — плата за предсказуемое время освобождения.

Значимые типы и ARC

struct с полем-классом копирует структуру, но вложенный класс остаётся общим по ссылке:

struct Wrapper {
var label: String
var counter: Counter // class Counter
}
var a = Wrapper(label: "A", counter: Counter())
var b = a
b.counter.increment() // меняет тот же Counter, что и a.counter

Поэтому даже при struct-обёртке вложенный class остаётся общим по ссылке — проектируйте границы явно (class только там, где нужна общая мутабельность).


Обобщённое программирование

Generics дают параметрический полиморфизм: один алгоритм — разные типы с проверкой на этапе компиляции.

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Ограничения T: Equatable, T: Collection связывают обобщения с протоколами — мост между POP и generics.


Сравнение с Java и C# (шпаргалка)

ЗадачаJavaC#Swift
Модель данных без идентичностиrecord / POJOrecord structstruct
Интерфейс + default methodinterface + defaultinterface + default implprotocol + extension
Защита от наследованияfinal classsealedfinal
Nullable@Nullable / Optional (Java 8+)T?T?
Строковая интерполяциятекстовые блоки$"..."\(...)
Свойстваget/set методыpropertyvar / computed
Enum с данныминет (до sealed types)ограниченноenum + associated values
ПамятьGCGCARC для классов

Swift ближе к C# по синтаксису свойств и optional, но ближе к функциональным языкам по enum и значимым типам. Java-разработчику стоит привыкнуть к value semantics и протоколам вместо abstract class + interface pairs.


Учебные примеры ООП

Небольшие самодостаточные программы, которые показывают классы, объекты, инкапсуляцию, наследование и взаимодействие нескольких типов на одной предметной области.

Класс и объект

Чертёж класса Figure и конкретные объекты — круг и квадрат.

Код ITЗагрузка примера кода…


Банковский счёт

Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.

Код ITЗагрузка примера кода…


Наследование

Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().

Код ITЗагрузка примера кода…


Смартфон

Состояние объекта: заряд батареи, звонки и подзарядка.

Код ITЗагрузка примера кода…


Студент

Список оценок, средний балл и проходной порог.

Код ITЗагрузка примера кода…


Корзина покупок

Взаимодействие Product, Cart и Order при оформлении заказа.

Код ITЗагрузка примера кода…


Автомобиль

Пробег, расход топлива и напоминание о техобслуживании.

Код ITЗагрузка примера кода…


Пользователь

Скрытый пароль, вход в систему и публикация сообщений.

Код ITЗагрузка примера кода…

Содержание