Лекция 1. Классы и объекты в Swift
Цели лекции
После занятия студент должен уметь:
- объяснить разницу между классом и объектом;
- объявить класс, его свойства и методы;
- создать корректный инициализатор;
- различать хранимые и вычисляемые свойства;
- ограничить доступ к состоянию объекта;
- объяснить ссылочную семантику классов;
- отличить тождественность объектов от равенства их данных;
- спроектировать небольшой класс с ясной ответственностью.
1. Что такое объектно-ориентированное программирование
Объектно-ориентированное программирование — способ строить программу из взаимодействующих объектов. Каждый объект объединяет:
- состояние — данные, которые он хранит;
- поведение — операции, которые он умеет выполнять;
- идентичность — возможность отличить один объект от другого, даже когда их данные совпадают.
Например, банковский счёт хранит номер и баланс, умеет принимать пополнение и списание и остаётся конкретным счётом независимо от текущей суммы.
ООП не означает «всё нужно превратить в класс». Класс полезен, когда сущность имеет собственный жизненный цикл, изменяемое состояние или идентичность. Позже мы сравним классы со структурами Swift и научимся выбирать между ними.
2. Класс и экземпляр
Класс описывает устройство и поведение будущих объектов. Экземпляр — конкретный объект, созданный по этому описанию.
Для примеров с UUID и Decimal подключаем стандартный модуль Foundation:
import Foundation
final class Book { let title: String let author: String
init(title: String, author: String) { self.title = title self.author = author }
func description() -> String { "\(title) — \(author)" }}
let firstBook = Book(title: "Чистый код", author: "Роберт Мартин")let secondBook = Book(title: "1984", author: "Джордж Оруэлл")
print(firstBook.description())print(secondBook.description())Здесь Book — класс, а firstBook и secondBook — два разных экземпляра.
Обращение к свойствам и методам выполняется через точку.
Ключевое слово final запрещает наследование от класса. Для небольших моделей
это полезное значение по умолчанию: наследование следует разрешать осознанно, а
не случайно.
3. Хранимые свойства
Хранимое свойство занимает место в состоянии каждого экземпляра.
final class Student { let id: Int var name: String var score: Int
init(id: Int, name: String, score: Int = 0) { self.id = id self.name = name self.score = score }}letразрешает присвоить значение один раз во время инициализации;varразрешает менять значение после создания объекта;scoreимеет значение параметра по умолчанию;self.nameобозначает свойство объекта, аname— параметр инициализатора.
let student = Student(id: 1, name: "Амина")student.name = "Амина М."student.score = 90Переменная student объявлена через let, но свойства объекта менять можно.
let фиксирует ссылку: переменной нельзя назначить другой объект. Изменяемость
самого объекта определяется объявлениями его свойств.
// Ошибка: нельзя назначить другой объект константе student.// student = Student(id: 2, name: "Руслан")4. Инициализация и инварианты
Инициализатор переводит новый объект в корректное начальное состояние. Swift
требует, чтобы к моменту завершения init все хранимые свойства получили
значения.
Инвариант — условие, которое должно быть истинно для любого корректного состояния объекта. Например, баланс счёта не должен быть отрицательным.
final class BankAccount { let number: String private(set) var balance: Decimal
init?(number: String, initialBalance: Decimal) { guard !number.isEmpty, initialBalance >= 0 else { return nil }
self.number = number self.balance = initialBalance }}
let validAccount = BankAccount(number: "40817810", initialBalance: 1_000)let invalidAccount = BankAccount(number: "", initialBalance: -100) // nilinit? — проваливающийся инициализатор. Он возвращает nil, если создать
корректный объект невозможно. Благодаря проверке некорректный счёт вообще не
появляется в программе.
5. Методы экземпляра
Метод описывает действие объекта и имеет доступ к его свойствам.
final class BankAccount { let number: String private(set) var balance: Decimal
init?(number: String, initialBalance: Decimal) { guard !number.isEmpty, initialBalance >= 0 else { return nil } self.number = number self.balance = initialBalance }
func deposit(_ amount: Decimal) { guard amount > 0 else { return } balance += amount }
func withdraw(_ amount: Decimal) -> Bool { guard amount > 0, amount <= balance else { return false } balance -= amount return true }}Сравните два подхода:
// Плохо: внешний код сам решает, каким станет состояние.// account.balance -= 500
// Лучше: объект контролирует операцию и сохраняет свой инвариант.account?.withdraw(500)Метод важен не только потому, что сокращает код. Он создаёт контролируемую точку изменения состояния.
6. Инкапсуляция и уровни доступа
Инкапсуляция — сокрытие деталей реализации и предоставление небольшого, понятного интерфейса для работы с объектом.
Основные модификаторы доступа Swift:
| Модификатор | Где доступно объявление |
|---|---|
private | Внутри объявления и его расширений в том же файле |
fileprivate | Внутри текущего файла |
internal | Внутри текущего модуля; значение по умолчанию |
package | Внутри пакета Swift Package Manager |
public | В других модулях, но без свободного наследования |
open | В других модулях, включая наследование и переопределение |
Запись private(set) означает: читать свойство можно снаружи, а изменять —
только внутри разрешённой области.
final class Counter { private(set) var value = 0
func increment() { value += 1 }
func reset() { value = 0 }}
let counter = Counter()counter.increment()print(counter.value) // читать можно// counter.value = 100 // ошибка компиляцииХорошая инкапсуляция защищает правила предметной области. Делать все свойства
private автоматически не нужно: скрывайте то, прямое изменение чего может
сломать объект или привязать внешний код к деталям реализации.
7. Вычисляемые свойства
Вычисляемое свойство не хранит отдельное значение, а рассчитывает его при обращении.
final class Rectangle { var width: Double var height: Double
var area: Double { width * height }
var isSquare: Bool { width == height }
init(width: Double, height: Double) { self.width = width self.height = height }}У area и isSquare нет собственного места хранения. Их значения всегда
соответствуют текущим width и height, поэтому их невозможно случайно забыть
обновить.
Вычисляемое свойство может иметь get и set:
final class Temperature { var celsius: Double
var fahrenheit: Double { get { celsius * 9 / 5 + 32 } set { celsius = (newValue - 32) * 5 / 9 } }
init(celsius: Double) { self.celsius = celsius }}Если присутствует только чтение, ключевое слово get можно не писать.
8. Свойства и методы типа
Члены экземпляра принадлежат конкретному объекту. Члены типа принадлежат самому
типу и объявляются с помощью static.
final class User { static let minimumNameLength = 2 private(set) static var createdCount = 0
let name: String
init?(name: String) { guard name.count >= Self.minimumNameLength else { return nil } self.name = name Self.createdCount += 1 }}
let user = User(name: "Лейла")print(User.createdCount)Self обозначает текущий тип. Не следует превращать глобальное изменяемое
состояние в static без необходимости: оно усложняет тестирование и связывает
между собой независимые части программы.
9. Ссылочная семантика классов
Экземпляр класса имеет ссылочную семантику. Присваивание не создаёт независимую копию объекта — две переменные начинают ссылаться на один экземпляр.
final class Profile { var nickname: String
init(nickname: String) { self.nickname = nickname }}
let firstReference = Profile(nickname: "north")let secondReference = firstReference
secondReference.nickname = "south"
print(firstReference.nickname) // southprint(secondReference.nickname) // southСхематично:
firstReference ─┐ ├──> один объект ProfilesecondReference ─┘Это принципиальное отличие от структур, которые при присваивании ведут себя как значения. Ссылочная семантика полезна для объектов с идентичностью и совместно изменяемым состоянием, но требует внимательного контроля изменений.
10. Тождественность и равенство
Операторы === и !== проверяют, указывают ли две ссылки на один экземпляр
класса.
let a = Profile(nickname: "swift")let b = alet c = Profile(nickname: "swift")
print(a === b) // true: один объектprint(a === c) // false: разные объекты с одинаковыми даннымиОператор == имеет другой смысл — логическое равенство значений. Класс не
получает его автоматически. Чтобы сравнивать объекты по данным, тип должен
реализовать протокол Equatable.
final class Profile: Equatable { let id: UUID var nickname: String
init(id: UUID = UUID(), nickname: String) { self.id = id self.nickname = nickname }
static func == (lhs: Profile, rhs: Profile) -> Bool { lhs.id == rhs.id }}Решение о равенстве — часть модели. Для профиля логично сравнивать стабильные идентификаторы, а не изменяемые имена.
11. Наблюдатели свойств
willSet выполняется перед изменением свойства, didSet — после него.
final class Download { var progress: Int = 0 { willSet { print("Новое значение: \(newValue)%") } didSet { if progress < 0 { progress = 0 } if progress > 100 { progress = 100 } } }}Наблюдатели подходят для локальной реакции на изменение. Сложную бизнес-логику лучше помещать в явно названные методы: скрытые побочные эффекты свойства трудно заметить и тестировать.
12. Пример проектирования: корзина покупок
struct Product { let id: UUID let name: String let price: Decimal}
struct CartItem { let product: Product var quantity: Int
var subtotal: Decimal { product.price * Decimal(quantity) }}
final class ShoppingCart { private(set) var items: [CartItem] = []
var total: Decimal { items.reduce(0) { $0 + $1.subtotal } }
func add(_ product: Product, quantity: Int = 1) { guard quantity > 0 else { return }
if let index = items.firstIndex(where: { $0.product.id == product.id }) { items[index].quantity += quantity } else { items.append(CartItem(product: product, quantity: quantity)) } }
func remove(productID: UUID) { items.removeAll { $0.product.id == productID } }
func clear() { items.removeAll() }}Почему ShoppingCart — класс:
- корзина имеет собственную идентичность и жизненный цикл;
- несколько частей программы могут работать с одной корзиной;
- её состояние изменяется командами
add,removeиclear.
Почему Product и CartItem пока структуры: они удобны как значения, их
идентичность не требует совместного изменяемого объекта. Подробное сравнение
структур и классов будет темой следующей лекции.
13. Типичные ошибки
Класс без ответственности
Если один класс хранит пользователей, отправляет письма, работает с файлами и печатает отчёты, он знает слишком много. У класса должна быть ясная причина для изменения.
Открытое изменяемое состояние
final class Order { var total: Decimal = 0 var isPaid = false}Любой код может установить отрицательную сумму или отметить неоплаченный заказ как оплаченный. Состояние лучше менять через операции, сохраняющие правила.
Класс вместо простого значения
Координата, денежная сумма или диапазон обычно не требуют идентичности. Для них
часто лучше подходит struct. Использование классов повсюду создаёт лишнее
совместное состояние.
Путаница == и ===
===— один ли это экземпляр;==— равны ли значения по правиламEquatable.
Избыточное наследование
Не следует убирать final «на будущее». Наследование создаёт сильную связь между
типами. Мы изучим его отдельно и сравним с композицией и протоколами.
14. Мини-практика на лекции
Спроектируйте класс LibraryMember:
- У участника есть неизменяемый
id, имя и список взятых книг. - Список книг доступен для чтения, но не для прямого изменения снаружи.
- Метод
borrow(book:)не позволяет взять одну книгу дважды. - Метод
returnBook(id:)удаляет книгу по идентификатору и сообщает, была ли она найдена. - Вычисляемое свойство
borrowedCountвозвращает число книг. - Одновременно можно держать не более пяти книг.
Продумайте, какие сущности должны быть классами, а какие — структурами. Опишите инварианты до написания кода.
15. Вопросы для самопроверки
- Чем класс отличается от его экземпляра?
- Почему константа, содержащая ссылку на класс, не делает весь объект неизменяемым?
- Чем хранимое свойство отличается от вычисляемого?
- Как
private(set)помогает сохранить инварианты? - Когда полезен проваливающийся инициализатор
init?? - Что произойдёт при присваивании экземпляра класса другой переменной?
- Чем
===отличается от==? - Зачем объявлять класс
final? - Почему открытое изменяемое состояние опасно?
- В каких случаях сущность, похожую на объект предметной области, лучше представить структурой?
Итоги
- Класс объединяет состояние и поведение объектов с идентичностью.
- Инициализатор обязан создать корректное начальное состояние.
- Методы и ограничения доступа защищают инварианты объекта.
- Экземпляры классов имеют ссылочную семантику.
===проверяет тождественность ссылок, а==— логическое равенство.- Классы стоит использовать осознанно; для простых значений Swift предлагает структуры.