Объектно-ориентированное программирование в Swift
Если ООП для вас новое или вы учите 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#, полезно сразу увидеть отличия:
| Тема | Java | C# | Swift |
|---|---|---|---|
| Значимые типы | только примитивы / обёртки | struct, record | struct, enum — первый выбор для моделей |
| Наследование классов | один базовый класс | один базовый класс | один суперкласс; множественное — через протоколы |
| Интерфейсы | interface | interface | protocol + расширения с реализацией по умолчанию |
| Память классов | GC | GC | ARC — детерминированный подсчёт ссылок |
| Абстрактные классы | abstract | abstract | нет ключевого слова; эмуляция через протоколы |
| Модификаторы для наследования | public / protected | public / protected / internal | open (наследование снаружи модуля), public (без наследования снаружи) |
| Null | null | null | Optional (?) |
| Переопределение | @Override | override | override; запрет — 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 |
| Convenience | convenience init(...) | Вспомогательный; обязан делегировать в self.init(...) того же класса |
| Required | required init(...) | Все подклассы обязаны реализовать этот инициализатор |
| Failable | init?(...) / 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)
}
}
Правила для классов:
- Сначала инициализируются собственные свойства подкласса, затем
super.init. convenience initне может вызыватьsuper.initнапрямую — только другойinitтого же класса.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#
| Java | C# | Swift | |
|---|---|---|---|
| Основной конструктор | имя класса | имя класса | init |
| Цепочка конструкторов | this(...) | : this(...) | convenience init → self.init |
| Вызов базового | super(...) | : base(...) | super.init(...) после своих полей |
| Неудачное создание | исключение / Optional (Java) | исключение | init? → nil |
| Обязательный для наследников | нет ключевого слова | нет | required init |
В C# именованные и опциональные параметры конструктора часто заменяют набор convenience init в Swift. В Java несколько перегруженных конструкторов эквивалентны нескольким init с разными сигнатурами; convenience при этом не нужен, если каждый инициализатор сам задаёт все поля.
Свойства
Хранимые и вычисляемые
Хранимое (stored) | Вычисляемое (computed) | |
|---|---|---|
| Память | Занимает место в экземпляре | Только код get / set |
| Синтаксис | var name: Type | var 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 предлагает три способа объявить составной тип. Выбор влияет на семантику копирования, наследование и модель памяти.
struct | class | enum | |
|---|---|---|---|
| Семантика | Значение (копия при присваивании) | Ссылка (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 нет. Паттерны замены:
protocol+ реализации вstruct/class.- Базовый
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# (шпаргалка)
| Задача | Java | C# | Swift |
|---|---|---|---|
| Модель данных без идентичности | record / POJO | record struct | struct |
| Интерфейс + default method | interface + default | interface + default impl | protocol + extension |
| Защита от наследования | final class | sealed | final |
| Nullable | @Nullable / Optional (Java 8+) | T? | T? |
| Строковая интерполяция | текстовые блоки | $"..." | \(...) |
| Свойства | get/set методы | property | var / computed |
| Enum с данными | нет (до sealed types) | ограниченно | enum + associated values |
| Память | GC | GC | ARC для классов |
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Загрузка примера кода…