🍎 Swift

面向对象编程:Swift 中的类与对象——详细指南

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты архитектор и тебе нужно построить 100 одинаковых домов в новом районе. Что ты сделаешь: будешь рисовать каждый дом с нуля на отдельном листе бумаги? Конечно нет! Ты создашь один чертёж (или план дома), а затем строители будут возводить дома по этому чертежу. Каждый построенный дом — это отдельное здание, но все они сделаны по одному проекту.

В программировании этот принцип называется Объектно-Ориентированное Программирование (ООП). И здесь есть два главных понятия:

  • Класс — это тот самый чертёж. Он описывает, какие характеристики (свойства) будут у объекта и что он умеет делать (методы). Сам по себе класс — это просто описание, как инструкция.
  • Объект — это конкретный дом, построенный по чертежу. У каждого объекта есть свои собственные данные (например, свой номер дома, свой цвет стен), но структура у всех одинаковая.
💡 Важное замечание: Класс — это тип данных, который ты сам придумываешь. Swift уже знает про Int, String, Double. А класс — это твой собственный тип, например, «Студент», «Автомобиль» или «Игровой персонаж».

Давай рассмотрим ещё одну аналогию — формочка для печенья. У тебя есть одна металлическая формочка в виде звезды (это класс). Ты раскатываешь тесто и выдавливаешь 30 печенек. Каждая печенька — это объект. Все они звёздочки, но каждую можно украсить по-разному: одна с шоколадом, другая с посыпкой. Формочка одна, а печенек много.

Зачем это нужно в реальной разработке? Представь, что ты пишешь игру. У тебя 500 врагов на уровне. Если описывать каждого врага отдельно — это будет 500 кусков кода. А если создать класс Enemy (Враг) и просто создать 500 объектов этого класса — код сокращается в 500 раз! Плюс, если нужно добавить всем врагам новое свойство (например, «броня»), ты меняешь только класс, и все 500 объектов получают это свойство автоматически.

Теория: как это работает изнутри

Давай заглянем «под капот» и разберёмся, как Swift управляет классами и объектами. Это поможет тебе понимать, почему код работает именно так, а не иначе.

Память: стек и куча

Когда ты создаёшь объект класса, Swift выделяет для него память в специальной области, которая называется куча (heap). Почему не в стеке (stack), как для обычных чисел? Потому что объекты могут быть большими, и их время жизни сложнее предсказать. Объект может жить дольше, чем функция, в которой он создан. Стек работает по принципу «последним пришёл — первым ушёл», а для объектов это не подходит.

В переменной, которая «держит» объект, хранится не сам объект, а ссылка (указатель) на него. Это как адрес дома: ты не носишь дом в кармане, ты знаешь, где он находится. Поэтому, когда ты передаёшь объект в функцию, ты передаёшь не копию, а ссылку. Изменения, сделанные внутри функции, повлияют на оригинальный объект.

⚠️ Важно: Это отличается от структур (struct) в Swift. Структуры передаются по значению — каждый раз создаётся копия. Классы — по ссылке. Это ключевое различие, которое мы ещё обсудим.

Жизненный цикл объекта

  1. Объявление класса — ты пишешь описание: какие свойства и методы будут.
  2. Создание объекта (инициализация) — ты вызываешь специальный метод init(), который выделяет память и устанавливает начальные значения свойств.
  3. Использование — объект живёт, ты вызываешь его методы, меняешь свойства.
  4. Уничтожение (деинициализация) — когда объект больше не нужен, Swift автоматически вызывает deinit и освобождает память. Это называется ARC (Automatic Reference Counting) — автоматический подсчёт ссылок. Swift сам следит, сколько переменных указывают на объект. Когда счётчик становится равен нулю, объект удаляется.

Три кита ООП (на примере Swift)

В классическом ООП есть три главных принципа. В Swift они работают немного по-своему, но суть та же:

Принцип Что означает Как реализовано в Swift
Инкапсуляция Скрытие внутреннего устройства. Пользователь класса не должен знать, как внутри всё устроено. Он только вызывает методы. Модификаторы доступа: private, public, internal. Свойства можно сделать private(set) — читать снаружи можно, менять — нет.
Наследование Создание нового класса на основе существующего. Дочерний класс перенимает все свойства и методы родителя и может добавить свои. Класс может наследовать только от одного класса (одиночное наследование). Пишется через двоеточие: class Dog: Animal.
Полиморфизм Один интерфейс — много реализаций. Разные классы могут иметь методы с одинаковыми названиями, но работать по-разному. Достигается через наследование и протоколы. Класс-родитель может объявить метод, а дочерние классы переопределяют его (override).

Синтаксис в Swift: все формы и варианты

Теперь перейдём к практике. Как писать классы в Swift? Давай разберём все возможные варианты синтаксиса.

Базовая структура класса

// Объявляем класс с помощью ключевого слова class
class ИмяКласса {
    // Свойства (переменные и константы)
    var свойство1: Тип
    let свойство2: Тип
    
    // Инициализатор (конструктор) — обязателен, если есть свойства без значений
    init(параметр1: Тип, параметр2: Тип) {
        self.свойство1 = параметр1
        self.свойство2 = параметр2
    }
    
    // Методы (функции внутри класса)
    func имяМетода() -> ТипВозврата {
        // тело метода
    }
}

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

В Swift есть несколько способов создать инициализатор:

class User {
    var name: String
    var age: Int
    var email: String?
    
    // 1. Назначенный инициализатор (designated) — основной
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.email = nil  // можно не писать, если тип Optional
    }
    
    // 2. Удобный инициализатор (convenience) — вызывает назначенный
    convenience init(name: String) {
        self.init(name: name, age: 0)  // возраст по умолчанию
    }
    
    // 3. Опциональный инициализатор (failable) — может вернуть nil
    init?(name: String, age: Int, email: String) {
        if age < 0 { return nil }  // проверка: возраст не может быть отрицательным
        self.name = name
        self.age = age
        self.email = email
    }
    
    // 4. Деинициализатор — вызывается перед удалением объекта
    deinit {
        print("Пользователь (name) удалён из памяти")
    }
}

Свойства: хранимые и вычисляемые

class Rectangle {
    // Хранимое свойство (хранит значение)
    var width: Double
    var height: Double
    
    // Вычисляемое свойство (не хранит, а вычисляет на лету)
    var area: Double {
        // Геттер — получаем значение
        get {
            return width * height
        }
        // Сеттер — устанавливаем новое значение (меняем ширину)
        set(newArea) {
            width = sqrt(newArea * width / height)  // пример расчёта
        }
    }
    
    // Наблюдатели свойств (property observers)
    var color: String = "White" {
        willSet {
            print("Цвет меняется с (color) на (newValue)")
        }
        didSet {
            print("Цвет изменён. Теперь: (color)")
        }
    }
    
    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
}

Модификаторы доступа

Модификатор Доступ Пример использования
open Максимальный. Можно наследовать и переопределять даже из другого модуля. Для библиотек и фреймворков
public Видно везде, но наследовать/переопределять можно только внутри модуля. Публичный API
internal Доступно внутри всего модуля (приложения). Это значение по умолчанию. Обычные классы внутри проекта
fileprivate Доступно только в пределах одного файла. Скрытие вспомогательных деталей
private Доступно только внутри фигурных скобок этого класса. Скрытие внутреннего состояния

Примеры кода — от простого к сложному

Теперь давай посмотрим на живые примеры. Каждый пример — рабочий, его можно скопировать и запустить в Playground.

Пример 1: Простейший класс «Собака»

// Создаём класс Dog
class Dog {
    var name: String      // свойство: имя
    var breed: String     // свойство: порода
    
    // Инициализатор
    init(name: String, breed: String) {
        self.name = name   // self — это сам объект
        self.breed = breed
    }
    
    // Метод: собака лает
    func bark() {
        print("(name): Гав-гав!")
    }
}

// Создаём объект (экземпляр класса)
let myDog = Dog(name: "Рекс", breed: "Овчарка")
// Обращаемся к свойствам
print("Мою собаку зовут (myDog.name), она породы (myDog.breed)")
// Вызываем метод
myDog.bark()  // Выведет: Рекс: Гав-гав!

Пример 2: Класс с вычисляемым свойством

class Temperature {
    var celsius: Double
    
    // Вычисляемое свойство: переводим Цельсий в Фаренгейт
    var fahrenheit: Double {
        get {
            return celsius * 9 / 5 + 32
        }
        set(newFahrenheit) {
            celsius = (newFahrenheit - 32) * 5 / 9
        }
    }
    
    init(celsius: Double) {
        self.celsius = celsius
    }
}

let temp = Temperature(celsius: 25)
print("(temp.celsius)°C = (temp.fahrenheit)°F")  // 25°C = 77.0°F

// Меняем через Фаренгейт
temp.fahrenheit = 100
print("Теперь в Цельсиях: (temp.celsius)")  // 37.777...

Пример 3: Наследование — класс «Кошка» от «Животное»

// Базовый класс (родитель)
class Animal {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    func makeSound() {
        print("(name) издаёт звук")
    }
}

// Дочерний класс (наследник)
class Cat: Animal {
    var hasFur: Bool  // дополнительное свойство
    
    init(name: String, age: Int, hasFur: Bool) {
        self.hasFur = hasFur
        super.init(name: name, age: age)  // вызываем инициализатор родителя
    }
    
    // Переопределяем метод родителя
    override func makeSound() {
        print("(name): Мяу-мяу!")
    }
    
    // Новый метод, которого нет у Animal
    func purr() {
        print("(name) мурлычет: мр-р-р")
    }
}

let myCat = Cat(name: "Мурка", age: 3, hasFur: true)
myCat.makeSound()  // Мурка: Мяу-мяу!
myCat.purr()       // Мурка мурлычет: мр-р-р

Пример 4: Инкапсуляция — банковский счёт

class BankAccount {
    // Приватное свойство — никто не может изменить баланс напрямую
    private var balance: Double = 0.0
    let accountNumber: String
    
    init(accountNumber: String) {
        self.accountNumber = accountNumber
    }
    
    // Публичный метод для пополнения
    func deposit(amount: Double) {
        guard amount > 0 else {
            print("Сумма должна быть положительной")
            return
        }
        balance += amount
        print("Внесено (amount). Баланс: (balance)")
    }
    
    // Публичный метод для снятия
    func withdraw(amount: Double) -> Bool {
        if amount > balance {
            print("Недостаточно средств. Доступно: (balance)")
            return false
        }
        balance -= amount
        print("Снято (amount). Остаток: (balance)")
        return true
    }
    
    // Только для чтения — геттер
    func getBalance() -> Double {
        return balance
    }
}

let myAccount = BankAccount(accountNumber: "12345")
myAccount.deposit(amount: 1000)
myAccount.withdraw(amount: 500)
// myAccount.balance = 9999  // Ошибка! balance — private

Пример 5: Статические свойства и методы

class MathUtils {
    // Статическое свойство — принадлежит классу, а не объекту
    static let pi = 3.1415926535
    
    // Статический метод
    static func square(_ number: Double) -> Double {
        return number * number
    }
    
    // Статическое вычисляемое свойство
    static var description: String {
        return "Утилита для математических расчётов"
    }
}

// Вызываем без создания объекта!
print(MathUtils.pi)          // 3.1415926535
print(MathUtils.square(5))   // 25.0
print(MathUtils.description)

Пример 6: Протоколы — контракты для классов

// Протокол — это список требований
protocol Drivable {
    var speed: Int { get set }
    func drive()
    func stop()
}

// Класс, который подписывается на протокол
class Car: Drivable {
    var speed: Int = 0
    var brand: String
    
    init(brand: String) {
        self.brand = brand
    }
    
    func drive() {
        speed = 60
        print("(brand) едет со скоростью (speed) км/ч")
    }
    
    func stop() {
        speed = 0
        print("(brand) остановился")
    }
}

let myCar = Car(brand: "Tesla")
myCar.drive()
myCar.stop()

Пример 7: Расширения (Extensions) — добавляем методы в класс

// Исходный класс
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// Расширяем класс новыми методами
extension Person {
    func introduce() {
        print("Привет! Меня зовут (name), мне (age) лет.")
    }
    
    // Вычисляемое свойство в расширении
    var isAdult: Bool {
        return age >= 18
    }
}

let person = Person(name: "Анна", age: 20)
person.introduce()  // Привет! Меня зовут Анна, мне 20 лет.
print(person.isAdult)  // true

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Но есть важные различия, которые нужно знать.

Характеристика Swift Python
Ключевое слово для класса class class
Инициализатор init() — обязателен, если есть свойства без значений __init__() — не обязателен, свойства можно добавить позже
self Обязательно везде: в свойствах и методах Обязательно, но называется self по соглашению
Типизация Строгая статическая. Тип указывается явно или выводится Динамическая. Тип может меняться во время выполнения
Наследование Только одиночное (один родитель) Множественное (несколько родителей)
Модификаторы доступа Есть: private, public и другие Нет формальных. Используется соглашение: _private (одно подчёркивание)
Перегрузка операторов Есть, но через специальные методы Есть через магические методы (__add__)
Протоколы/Интерфейсы Протоколы (protocol) — мощный механизм Абстрактные классы через ABC или протоколы через typing
Расширения Есть (extension) — можно добавлять методы к любому классу Нет встроенного механизма, но можно модифицировать классы (monkey patching)
Деинициализатор deinit — вызывается при удалении объекта __del__ — вызывается сборщиком мусора, но не гарантированно
🔑 Ключевое отличие: В Swift классы — это ссылочные типы (передаются по ссылке). В Python всё является объектом, но неизменяемые типы (int, str, tuple) ведут себя как значения, а изменяемые (list, dict) — как ссылки. В Swift есть отдельные структуры (struct) для типов-значений.

Типичные ошибки и подводные камни

Даже опытные разработчики иногда попадаются на этих граблях. Давай разберём самые частые ошибки.

Ошибка 1: Забыл вызвать super.init() в наследнике

class Parent {
    var value: Int
    init(value: Int) {
        self.value = value
    }
}

class Child: Parent {
    var extra: String
    init(extra: String, value: Int) {
        self.extra = extra
        // Ошибка! Нужно вызвать super.init(value: value) до или после?
        // В Swift — сначала нужно инициализировать свои свойства, потом super
    }
}

Решение: Сначала инициализируем все свои свойства, потом вызываем super.init().

Ошибка 2: Циклические ссылки (retain cycles)

class Person {
    var name: String
    var pet: Pet?  // Человек владеет питомцем
    
    init(name: String) {
        self.name = name
    }
    deinit { print("(name) удалён") }
}

class Pet {
    var name: String
    var owner: Person?  // Питомец ссылается на хозяина
    
    init(name: String) {
        self.name = name
    }
    deinit { print("(name) удалён") }
}

var person: Person? = Person(name: "Иван")
var pet: Pet? = Pet(name: "Барсик")
person?.pet = pet
pet?.owner = person
// Теперь у нас циклическая ссылка! Объекты не удалятся из памяти
person = nil
pet = nil  // deinit не вызовется!

Решение: Использовать weak или unowned для одной из ссылок.

Ошибка 3: Путаница между классом и структурой

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

Ошибка 4: Неправильное использование self

В Swift self не обязателен, если нет конфликта имён. Но в замыканиях (closures) нужно быть осторожным — там self может создать цикл ссылок.

Ошибка 5: Забыл про опциональность

class Book {
    var title: String
    var author: String?
    
    init(title: String) {
        self.title = title
        // author автоматически nil
    }
}

let book = Book(title: "Swift для начинающих")
// print(book.author.count)  // Ошибка! author — Optional, нужно развернуть
print(book.author?.count ?? 0)  // Правильно: используем optional chaining

Лучшие практики и советы профессионалов

Вот что советуют опытные iOS-разработчики, которые пишут на Swift каждый день.

1. Отдавай предпочтение структурам, а не классам

Apple рекомендует использовать struct по умолчанию. Классы используй только когда нужно наследование или ссылочная семантика. Структуры безопаснее в многопоточном коде и быстрее работают.

2. Используй final для классов, которые не должны наследоваться

final class MathHelper {
    // Этот класс нельзя наследовать
    // Компилятор может оптимизировать вызовы методов
}

3. Делай свойства только для чтения, если это возможно

class User {
    private(set) var id: String  // можно читать, но нельзя менять снаружи
    private var password: String // вообще не видно снаружи
}

4. Используй ленивые свойства (lazy) для тяжёлых вычислений

class DataManager {
    lazy var bigData: [String] = {
        // Этот код выполнится только при первом обращении к свойству
        print("Загрузка данных...")
        return ["data1", "data2", "data3"]
    }()
}

5. Соблюдай принцип единственной ответственности

Один класс — одна задача. Если класс «Студент» умеет и учиться, и печатать диплом, и отправлять email — это плохо. Раздели на несколько классов.

6. Используй протоколы для слабой связанности

Вместо того чтобы класс зависел от конкретного другого класса, пусть он зависит от протокола. Это упрощает тестирование и замену компонентов.

7. Документируй свои классы

/// Представляет игрового персонажа
/// - Важно: Каждый персонаж имеет уникальный ID
class Player {
    /// Уникальный идентификатор персонажа
    let id: String
    /// Текущий уровень здоровья (0-100)
    var health: Int
}

Вопросы для самопроверки

Проверь, как ты усвоил материал. Попробуй ответить на вопросы, прежде чем смотреть ответы.

  1. Вопрос: В чём разница между классом и объектом?

    Ответ: Класс — это чертёж (шаблон), а объект — конкретный экземпляр, созданный по этому чертежу. Класс описывает свойства и методы, объект имеет конкретные значения свойств.

  2. Вопрос: Что такое инициализатор и зачем он нужен?

    Ответ: Инициализатор (init) — это специальный метод, который вызывается при создании объекта. Он устанавливает начальные значения всех свойств и подготавливает объект к использованию.

  3. Вопрос: Какой модификатор доступа скрывает свойство от других классов, но оставляет его видимым внутри файла?

    Ответ: fileprivate. Свойство будет доступно только в пределах того файла, где объявлен класс.

  4. Вопрос: Что такое наследование? Приведи пример.

    Ответ: Наследование — это создание нового класса на основе существующего. Например, класс Car может наследовать от класса Vehicle. Car получает все свойства и методы Vehicle и может добавить свои.

  5. Вопрос: В чём разница между хранимым и вычисляемым свойством?

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

  6. Вопрос: Что такое циклическая ссылка и как её избежать?

    Ответ: Циклическая ссылка возникает, когда два объекта ссылаются друг на друга, и ARC не может их удалить. Избежать можно, объявив одну из ссылок как weak или unowned.

  7. Вопрос: Можно ли в Swift наследовать класс от двух родителей?

    Ответ: Нет, в Swift только одиночное наследование. Но можно подписаться на несколько протоколов.

  8. Вопрос: Что такое статическое свойство? Как к нему обратиться?

    Ответ: Статическое свойство принадлежит самому классу, а не объекту. Обращаются к нему через имя класса: MathUtils.pi.

  9. Вопрос: Чем отличается class от struct в Swift?

    Ответ: Классы — ссылочные типы (передаются по ссылке), поддерживают наследование, имеют деинициализатор. Структуры — типы-значения (копируются при передаче), не поддерживают наследование, но быстрее и безопаснее.

  10. Вопрос: Что такое полиморфизм? Приведи пример.

    Ответ: Полиморфизм — это способность разных классов отвечать на одни и те же методы по-своему. Пример: классы Dog и Cat оба наследуют от Animal и переопределяют метод makeSound(). Вызывая makeSound() у разных объектов, мы получаем разные звуки.

Итог и что изучить дальше

Поздравляю! Ты прошёл большой путь от понимания аналогии с чертежами до написания полноценных классов с наследованием и протоколами. Давай подведём итог:

  • Класс — это шаблон для создания объектов. Он объединяет данные (свойства)