5.14. ООП в Swift
ООП в Swift
Пример класса
class Unit {
var name: String = "Имя"
var intel: Int = 10
var agility: Int = 10
var strength: Int = 10
var health: Int = 100
var mana: Int = 50
var level: Int = 1
var damage: Int {
(intel + agility + strength) + (level * 2)
}
func attack(target: Unit) {
print("\(name) атакует \(target.name) и наносит \(damage) единиц урона.")
target.health -= damage
print("\(target.name) теперь имеет \(target.health) здоровья.")
}
}
let warrior = Unit()
warrior.name = "Воин"
warrior.intel = 5
warrior.agility = 15
warrior.strength = 30
let mage = Unit()
mage.name = "Маг"
mage.intel = 35
mage.agility = 10
mage.strength = 5
warrior.attack(target: mage)
mage.attack(target: warrior)
Ключевое слово class определяет новый класс. Имя класса начинается с заглавной буквы по соглашению о стиле кодирования Swift. Тело класса ограничено фигурными скобками. Класс представляет шаблон для создания объектов с единым набором свойств и поведения.
Каждое свойство объявляется с ключевым словом var, что делает его изменяемым. Тип свойства указывается после двоеточия. Значения по умолчанию присваиваются сразу при объявлении свойства. Тип String хранит текстовые значения, тип Int хранит целые числа. Все свойства инициализируются значениями по умолчанию, что удовлетворяет требованию Swift о полной инициализации объекта.
Свойство damage объявлено как вычисляемое без хранимого значения. Тело свойства заключено в фигурные скобки и содержит выражение для расчёта значения. При каждом обращении к свойству выполняется вычисление на основе текущих значений характеристик объекта. Такой подход гарантирует актуальность урона без необходимости ручного обновления или вызова отдельного метода.
Метод объявляется через ключевое слово func. Параметр target имеет тип Unit и помечен внешним именем target, что делает вызов метода более выразительным. Строковая интерполяция через конструкцию \(выражение) встраивает значения переменных непосредственно в строку. Оператор -= уменьшает здоровье целевого объекта на величину урона. Функция print выводит текст в консоль и автоматически добавляет символ новой строки.
Вызов Unit() создаёт новый объект и вызывает инициализатор по умолчанию. Константа let объявляет неизменяемую ссылку на объект. Несмотря на неизменяемость ссылки, свойства объекта остаются изменяемыми, так как класс является ссылочным типом. После создания объекта значения его свойств изменяются через точечную нотацию.
Вызов метода attack(target: mage) использует внешнее имя параметра target, что повышает читаемость кода. Swift требует указания внешних имён параметров при вызове методов класса, если они не помечены как _. Такой подход делает вызовы методов похожими на естественный язык и улучшает понимание намерений кода.
Swift использует автоматический подсчёт ссылок для управления памятью. Каждый объект класса имеет счётчик ссылок, увеличивающийся при создании новой ссылки и уменьшающийся при выходе ссылки из области видимости. Когда счётчик достигает нуля, объект автоматически освобождается. Разработчику не требуется вручную управлять выделением и освобождением памяти.
Точки с запятой в конце инструкций не обязательны и обычно опускаются. Возврат значения из методов или вычисляемых свойств происходит автоматически для последнего выражения в теле. Скобки вокруг условий в управляющих конструкциях обязательны, но вокруг всего тела функции или метода не требуются. Такие особенности делают синтаксис лаконичным и легко читаемым.
Программа компилируется компилятором Swift в нативный код. Запуск выполняется через среду разработки Xcode или командную строку с помощью swift filename.swift. При выполнении создаются два объекта с различными характеристиками. Первый вызов метода уменьшает здоровье мага на величину урона воина. Второй вызов метода уменьшает здоровье воина на величину урона мага. Каждое действие сопровождается выводом информационного сообщения в консоль.
Swift обеспечивает строгую статическую типизацию с выводом типов. Компилятор проверяет корректность операций с типами на этапе компиляции. Попытка присвоить значение несовместимого типа или вызвать несуществующий метод приводит к ошибке компиляции. Такой подход предотвращает множество ошибок времени выполнения и повышает надёжность программного кода.
Свойства
Характеристики свойств в классах
Свойства в объектно-ориентированном программировании представляют собой переменные, которые принадлежат конкретному объекту. В Swift свойства хранят данные, определяющие состояние объекта. Каждый экземпляр класса содержит собственную копию всех свойств, что позволяет объектам иметь уникальные характеристики.
class User {
var name: String
var age: Int
var email: String
init(name: String, age: Int, email: String) {
self.name = name
self.age = age
self.email = email
}
}
Свойства определяют внутреннее состояние объекта. При создании экземпляра класса каждое свойство получает начальное значение. Это значение может изменяться в процессе работы программы, что отражает изменение состояния объекта.
Вычисляемые свойства
Вычисляемые свойства предоставляют доступ к значению, которое вычисляется динамически при каждом обращении. Они не хранят данные напрямую, а возвращают результат выполнения кода.
class Rectangle {
var width: Double
var height: Double
var area: Double {
return width * height
}
var perimeter: Double {
return 2 * (width + height)
}
}
Вычисляемые свойства полезны для получения производных значений на основе других свойств объекта. Они упрощают интерфейс класса, скрывая сложные вычисления внутри.
Наблюдатели свойств
Наблюдатели свойств позволяют отслеживать изменения значений. Метод willSet выполняется перед изменением свойства, а didSet — после изменения.
class TemperatureSensor {
var temperature: Double = 0.0 {
willSet {
print("Температура изменится с \(temperature) на \(newValue)")
}
didSet {
if temperature > 100 {
print("Внимание: критическая температура!")
}
}
}
}
Наблюдатели свойств обеспечивают реакцию на изменения состояния объекта. Они используются для валидации данных, логирования изменений или запуска дополнительных действий при модификации свойств.
Ленивые свойства
Ленивые свойства инициализируются только при первом обращении к ним. Это позволяет отложить создание ресурсоемких объектов до момента их фактического использования.
class DataManager {
lazy var databaseConnection: DatabaseConnection = {
print("Создание подключения к базе данных")
return DatabaseConnection()
}()
lazy var cache: [String: Any] = [:]
}
Ленивые свойства оптимизируют производительность приложения. Они особенно полезны для объектов, требующих значительных ресурсов для инициализации, но используемых не всегда.
Методы
Методы экземпляра
Методы экземпляра являются функциями, которые принадлежат конкретному объекту. Они имеют доступ ко всем свойствам и другим методам своего экземпляра.
class BankAccount {
var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
print("Пополнение на \(amount). Баланс: \(balance)")
}
func withdraw(amount: Double) -> Bool {
if balance >= amount {
balance -= amount
print("Снятие \(amount). Баланс: \(balance)")
return true
} else {
print("Недостаточно средств")
return false
}
}
}
Методы экземпляра определяют поведение объекта. Они работают с внутренним состоянием экземпляра и могут изменять его свойства. Каждый вызов метода связан с конкретным объектом.
Методы типа
Методы типа принадлежат самому классу, а не его экземплярам. Они вызываются через имя класса и не имеют доступа к свойствам конкретных объектов.
class MathUtilities {
class func calculateFactorial(of number: Int) -> Int {
guard number > 1 else { return 1 }
return number * calculateFactorial(of: number - 1)
}
static func isPrime(_ number: Int) -> Bool {
guard number > 1 else { return false }
for i in 2..<number {
if number % i == 0 {
return false
}
}
return true
}
}
Методы типа предоставляют функциональность, связанную с классом в целом. Они используются для создания вспомогательных функций, фабричных методов или операций, не требующих конкретного экземпляра.
Модифицирующие методы
Методы, изменяющие свойства структуры или перечисления, должны быть помечены ключевым словом mutating. Это явное указание на то, что метод изменяет состояние объекта.
struct Point {
var x: Double
var y: Double
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
mutating func reset() {
x = 0.0
y = 0.0
}
}
Ключевое слово mutating обеспечивает безопасность работы с неизменяемыми типами. Оно предотвращает случайное изменение свойств в контекстах, где объект должен оставаться постоянным.
Вложенные методы
Вложенные методы определяются внутри других методов. Они видны только в рамках родительского метода и используются для организации сложной логики.
class DataProcessor {
func process(data: [Int]) -> [Int] {
func filterPositive(_ value: Int) -> Bool {
return value > 0
}
func square(_ value: Int) -> Int {
return value * value
}
return data.filter(filterPositive).map(square)
}
}
Вложенные методы улучшают читаемость кода. Они позволяют разбить сложные операции на логические блоки и изолировать вспомогательную функциональность.
Инкапсуляция
Сокрытие реализации
Инкапсуляция обеспечивает сокрытие внутренней реализации класса от внешнего мира. Свойства и методы, помеченные как приватные, доступны только внутри самого класса.
class SecureStorage {
private var encryptionKey: String
private var data: [String: String] = [:]
init(key: String) {
self.encryptionKey = key
}
func store(key: String, value: String) {
let encrypted = encrypt(value, with: encryptionKey)
data[key] = encrypted
}
func retrieve(key: String) -> String? {
guard let encrypted = data[key] else { return nil }
return decrypt(encrypted, with: encryptionKey)
}
private func encrypt(_ value: String, with key: String) -> String {
return value.reversed() + key
}
private func decrypt(_ value: String, with key: String) -> String {
let length = value.count - key.count
let index = value.index(value.startIndex, offsetBy: length)
return String(value[..<index]).reversed()
}
}
Сокрытие реализации защищает внутреннее состояние объекта от несанкционированного доступа. Оно позволяет изменять внутреннюю логику класса без влияния на код, использующий этот класс.
Интерфейс класса
Публичный интерфейс класса определяет набор методов и свойств, доступных внешнему коду. Хорошо спроектированный интерфейс предоставляет все необходимые функции, скрывая детали реализации.
class PaymentProcessor {
public func processPayment(amount: Double, currency: String) -> Bool {
validateAmount(amount)
validateCurrency(currency)
return executeTransaction(amount, currency)
}
public func getTransactionHistory() -> [Transaction] {
return loadHistory()
}
private func validateAmount(_ amount: Double) {
guard amount > 0 else {
fatalError("Сумма должна быть положительной")
}
}
private func validateCurrency(_ currency: String) {
let supportedCurrencies = ["USD", "EUR", "RUB"]
guard supportedCurrencies.contains(currency) else {
fatalError("Валюта не поддерживается")
}
}
private func executeTransaction(_ amount: Double, _ currency: String) -> Bool {
print("Выполнение транзакции: \(amount) \(currency)")
return true
}
private func loadHistory() -> [Transaction] {
return []
}
}
Интерфейс класса служит контрактом между разработчиком класса и его пользователями. Он определяет, как взаимодействовать с объектом, не раскрывая механизмов работы.
Преимущества инкапсуляции
Инкапсуляция повышает надежность программного обеспечения. Защищенные данные не могут быть изменены произвольным образом, что предотвращает ошибки и несогласованность состояния.
Инкапсуляция упрощает поддержку кода. Изменения внутренней реализации не влияют на внешний код, использующий класс через его публичный интерфейс.
Инкапсуляция способствует повторному использованию кода. Хорошо инкапсулированные классы можно легко интегрировать в различные проекты без модификации.
Наследование
Базовые принципы наследования
Наследование позволяет создавать новые классы на основе существующих. Дочерний класс получает все свойства и методы родительского класса, расширяя или изменяя их функциональность.
class Vehicle {
var brand: String
var model: String
var year: Int
init(brand: String, model: String, year: Int) {
self.brand = brand
self.model = model
self.year = year
}
func startEngine() {
print("Двигатель запущен")
}
func stopEngine() {
print("Двигатель остановлен")
}
}
class Car: Vehicle {
var numberOfDoors: Int
init(brand: String, model: String, year: Int, doors: Int) {
self.numberOfDoors = doors
super.init(brand: brand, model: model, year: year)
}
func openTrunk() {
print("Багажник открыт")
}
}
class Motorcycle: Vehicle {
var hasSidecar: Bool
init(brand: String, model: String, year: Int, sidecar: Bool) {
self.hasSidecar = sidecar
super.init(brand: brand, model: model, year: year)
}
func doWheelie() {
print("Выполняется вилли")
}
}
Наследование создает иерархию классов. Родительские классы определяют общую функциональность, а дочерние добавляют специфические особенности.
Цепочка инициализации
Цепочка инициализации обеспечивает правильную последовательность создания объектов в иерархии наследования. Дочерний класс должен инициализировать свои свойства до вызова инициализатора родителя.
class Animal {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
print("Инициализация Animal")
}
}
class Dog: Animal {
var breed: String
var isTrained: Bool
init(name: String, age: Int, breed: String, trained: Bool) {
self.breed = breed
self.isTrained = trained
super.init(name: name, age: age)
print("Инициализация Dog")
}
}
class GuideDog: Dog {
var certificationNumber: String
init(name: String, age: Int, breed: String, certification: String) {
self.certificationNumber = certification
super.init(name: name, age: age, breed: breed, trained: true)
print("Инициализация GuideDog")
}
}
Цепочка инициализации гарантирует, что все свойства объекта получают корректные значения. Каждый класс в иерархии отвечает за инициализацию своих собственных свойств.
Множественное наследование
Swift не поддерживает множественное наследование классов. Класс может наследовать только от одного родительского класса. Для достижения функциональности множественного наследования используются протоколы.
protocol Flyable {
func fly()
var maxAltitude: Double { get }
}
protocol Swimmable {
func swim()
var maxDepth: Double { get }
}
class Duck: Animal, Flyable, Swimmable {
var maxAltitude: Double = 1000.0
var maxDepth: Double = 10.0
func fly() {
print("Утка летит")
}
func swim() {
print("Утка плавает")
}
}
Ограничение на множественное наследование упрощает архитектуру программ. Протоколы предоставляют гибкий механизм для добавления функциональности без сложностей множественного наследования.
Преимущества иерархии наследования
Иерархия наследования организует код в логическую структуру. Общая функциональность размещается в базовых классах, а специфическая — в производных.
Иерархия наследования упрощает расширение функциональности. Новые классы могут добавляться в существующую структуру без изменения базового кода.
Иерархия наследования способствует повторному использованию кода. Базовые классы содержат общую логику, которая используется всеми производными классами.
Переопределение и перегрузка методов
Переопределение методов
Переопределение методов позволяет дочернему классу изменить поведение метода, унаследованного от родительского класса. Переопределенный метод должен иметь ту же сигнатуру, что и оригинальный.
class Shape {
var color: String = "black"
func draw() {
print("Рисование фигуры цветом \(color)")
}
func area() -> Double {
return 0.0
}
}
class Circle: Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
super.init()
}
override func draw() {
print("Рисование круга радиусом \(radius) цветом \(color)")
}
override func area() -> Double {
return Double.pi * radius * radius
}
}
class Rectangle: Shape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
super.init()
}
override func draw() {
print("Рисование прямоугольника \(width)x\(height) цветом \(color)")
}
override func area() -> Double {
return width * height
}
}
Переопределение методов обеспечивает полиморфное поведение. Каждый класс в иерархии может предоставлять свою реализацию общих операций.
Перегрузка методов
Перегрузка методов позволяет создавать несколько методов с одинаковыми именами, но разными параметрами. Компилятор выбирает подходящую версию метода на основе переданных аргументов.
class Calculator {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
func add(_ a: Double, _ b: Double) -> Double {
return a + b
}
func add(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}
func add(_ a: String, _ b: String) -> String {
return a + b
}
}
class Logger {
func log(_ message: String) {
print("[INFO] \(message)")
}
func log(_ message: String, level: String) {
print("[\(level)] \(message)")
}
func log(_ message: String, file: String, line: Int) {
print("[\(file):\(line)] \(message)")
}
func log(error: Error) {
print("[ERROR] \(error.localizedDescription)")
}
}
Перегрузка методов повышает удобство использования классов. Разработчики могут вызывать методы с различными наборами параметров, получая ожидаемый результат.
Требования к переопределению
Переопределение методов требует использования ключевого слова override. Это явное указание компилятору на намерение изменить поведение родительского метода.
class BaseClass {
func performAction() {
print("Базовое действие")
}
final func criticalAction() {
print("Критическое действие")
}
}
class DerivedClass: BaseClass {
override func performAction() {
print("Расширенное действие")
super.performAction()
}
// Ошибка: нельзя переопределить final метод
// override func criticalAction() { }
}
Ключевое слово final запрещает переопределение метода в дочерних классах. Это используется для защиты критически важной функциональности от изменений.
Вызов родительской реализации
Вызов родительской реализации через super позволяет расширить поведение метода, не заменяя его полностью. Это полезно для добавления дополнительной логики до или после выполнения базового метода.
class Parent {
func setup() {
print("Родительская настройка")
}
func cleanup() {
print("Родительская очистка")
}
}
class Child: Parent {
override func setup() {
print("Дочерняя предварительная настройка")
super.setup()
print("Дочерняя дополнительная настройка")
}
override func cleanup() {
print("Дочерняя предварительная очистка")
super.cleanup()
print("Дочерняя дополнительная очистка")
}
}
Вызов родительской реализации сохраняет базовую функциональность. Дочерний класс добавляет свою логику, не теряя преимуществ родительского кода.
Типы и экземпляры
Классы как типы
Классы в Swift являются ссылочными типами. Переменные класса хранят ссылки на объекты в памяти, а не сами объекты. При присваивании переменной класса копируется ссылка, а не объект.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
var person1 = Person(name: "Алексей", age: 30)
var person2 = person1
person2.name = "Иван"
print(person1.name) // Вывод: Иван
print(person2.name) // Вывод: Иван
Ссылочные типы позволяют нескольким переменным ссылаться на один и тот же объект. Изменения через одну переменную отражаются во всех других переменных, ссылающихся на тот же объект.
Создание экземпляров
Создание экземпляра класса происходит через вызов инициализатора. Инициализатор выделяет память для объекта и устанавливает начальные значения всех свойств.
class Product {
let id: Int
var name: String
var price: Double
var inStock: Bool
init(id: Int, name: String, price: Double, inStock: Bool) {
self.id = id
self.name = name
self.price = price
self.inStock = inStock
}
convenience init(id: Int, name: String, price: Double) {
self.init(id: id, name: name, price: price, inStock: true)
}
}
let laptop = Product(id: 1, name: "Ноутбук", price: 50000.0, inStock: true)
let phone = Product(id: 2, name: "Смартфон", price: 30000.0)
Инициализаторы обеспечивают корректное создание объектов. Они проверяют переданные значения и устанавливают начальное состояние экземпляра.
Жизненный цикл объектов
Жизненный цикл объекта включает создание, использование и уничтожение. Объект существует в памяти до тех пор, пока на него есть хотя бы одна активная ссылка.
class Resource {
let name: String
init(name: String) {
self.name = name
print("Ресурс \(name) создан")
}
deinit {
print("Ресурс \(name) уничтожен")
}
func use() {
print("Использование ресурса \(name)")
}
}
func demonstrateLifecycle() {
let resource = Resource(name: "Файл")
resource.use()
// Ресурс будет уничтожен при выходе из функции
}
demonstrateLifecycle()
// Вывод:
// Ресурс Файл создан
// Использование ресурса Файл
// Ресурс Файл уничтожен
Деструктор deinit вызывается автоматически при уничтожении объекта. Он используется для освобождения ресурсов, таких как файловые дескрипторы или сетевые соединения.
Сравнение экземпляров
Сравнение экземпляров классов происходит по ссылкам. Оператор === проверяет, ссылаются ли две переменные на один и тот же объект в памяти.
class Book {
let title: String
let author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
let book1 = Book(title: "1984", author: "Джордж Оруэлл")
let book2 = book1
let book3 = Book(title: "1984", author: "Джордж Оруэлл")
print(book1 === book2) // true - одна и та же ссылка
print(book1 === book3) // false - разные объекты
print(book1 !== book3) // true - разные ссылки
if book1 === book2 {
print("book1 и book2 ссылаются на один объект")
}
Сравнение по ссылкам отличается от сравнения по значению. Два разных объекта с одинаковыми свойствами считаются различными при сравнении через ===.
Композиция
Принцип композиции
Композиция представляет собой сборку сложных объектов из более простых компонентов. Вместо наследования класс содержит экземпляры других классов как свои свойства.
class Engine {
var horsepower: Int
var isRunning: Bool = false
init(horsepower: Int) {
self.horsepower = horsepower
}
func start() {
isRunning = true
print("Двигатель запущен")
}
func stop() {
isRunning = false
print("Двигатель остановлен")
}
}
class Wheel {
var size: Int
var pressure: Double
init(size: Int, pressure: Double) {
self.size = size
self.pressure = pressure
}
func rotate() {
print("Колесо вращается")
}
}
class Car {
let engine: Engine
var wheels: [Wheel]
var brand: String
init(brand: String, engine: Engine, wheels: [Wheel]) {
self.brand = brand
self.engine = engine
self.wheels = wheels
}
func start() {
engine.start()
print("\(brand) готов к движению")
}
func drive() {
engine.start()
wheels.forEach { $0.rotate() }
}
}
let carEngine = Engine(horsepower: 200)
let carWheels = [
Wheel(size: 17, pressure: 2.3),
Wheel(size: 17, pressure: 2.3),
Wheel(size: 17, pressure: 2.3),
Wheel(size: 17, pressure: 2.3)
]
let myCar = Car(brand: "Toyota", engine: carEngine, wheels: carWheels)
myCar.start()
Композиция создает гибкую архитектуру. Объекты могут комбинироваться различными способами для достижения нужной функциональности.
Преимущества композиции
Композиция упрощает тестирование кода. Компоненты можно тестировать изолированно, а затем комбинировать в сложные системы.
Композиция повышает переиспользуемость кода. Один и тот же компонент может использоваться в различных контекстах без модификации.
Композиция уменьшает связанность между классами. Изменения в одном компоненте не влияют на другие компоненты системы.
Отношения "имеет" и "является"
Отношение "имеет" реализуется через композицию. Класс содержит экземпляры других классов как часть своего состояния.
class Library {
var books: [Book] = []
var librarians: [Librarian] = []
func addBook(_ book: Book) {
books.append(book)
}
func addLibrarian(_ librarian: Librarian) {
librarians.append(librarian)
}
}
class University {
var name: String
var libraries: [Library] = []
var departments: [Department] = []
init(name: String) {
self.name = name
}
func addLibrary(_ library: Library) {
libraries.append(library)
}
}
Отношение "имеет" описывает структурную связь между объектами. Один объект содержит другие объекты как свои части.
Агрегация и композиция
Агрегация представляет слабую связь между объектами. Компоненты могут существовать независимо от контейнера.
class Department {
var name: String
weak var university: University?
init(name: String) {
self.name = name
}
}
class Professor {
var name: String
var department: Department?
init(name: String) {
self.name = name
}
}
class Course {
var title: String
var professor: Professor?
init(title: String) {
self.title = title
}
}
Агрегация позволяет компонентам иметь независимый жизненный цикл. Объекты могут быть добавлены или удалены из контейнера без влияния на их существование.
Полиморфизм
Полиморфизм через наследование
Полиморфизм позволяет объектам разных классов реагировать на одинаковые сообщения по-разному. Базовый класс определяет интерфейс, а производные классы предоставляют конкретные реализации.
class Animal {
var name: String
init(name: String) {
self.name = name
}
func makeSound() {
print("Животное издает звук")
}
func move() {
print("Животное движется")
}
}
class Dog: Animal {
override func makeSound() {
print("Гав-гав!")
}
override func move() {
print("Собака бегает")
}
}
class Cat: Animal {
override func makeSound() {
print("Мяу!")
}
override func move() {
print("Кошка прыгает")
}
}
class Bird: Animal {
override func makeSound() {
print("Чирик!")
}
override func move() {
print("Птица летает")
}
}
func demonstrateAnimals() {
let animals: [Animal] = [
Dog(name: "Рекс"),
Cat(name: "Мурка"),
Bird(name: "Чижик")
]
for animal in animals {
print("\(animal.name):")
animal.makeSound()
animal.move()
print()
}
}
Полиморфизм упрощает работу с коллекциями объектов разных типов. Код может взаимодействовать с объектами через общий интерфейс, не зная их конкретного типа.
Полиморфизм через протоколы
Протоколы в Swift предоставляют механизм полиморфизма без наследования. Любой тип, соответствующий протоколу, может использоваться в контексте, ожидающем этот протокол.
protocol Drawable {
func draw()
var area: Double { get }
}
class Circle: Drawable {
var radius: Double
init(radius: Double) {
self.radius = radius
}
func draw() {
print("Рисование круга радиусом \(radius)")
}
var area: Double {
return Double.pi * radius * radius
}
}
class Square: Drawable {
var side: Double
init(side: Double) {
self.side = side
}
func draw() {
print("Рисование квадрата со стороной \(side)")
}
var area: Double {
return side * side
}
}
class Triangle: Drawable {
var base: Double
var height: Double
init(base: Double, height: Double) {
self.base = base
self.height = height
}
func draw() {
print("Рисование треугольника")
}
var area: Double {
return 0.5 * base * height
}
}
func renderShapes(_ shapes: [Drawable]) {
for shape in shapes {
shape.draw()
print("Площадь: \(shape.area)")
print()
}
}
let shapes: [Drawable] = [
Circle(radius: 5.0),
Square(side: 4.0),
Triangle(base: 6.0, height: 8.0)
]
renderShapes(shapes)
Протоколы обеспечивают гибкость в проектировании. Типы могут соответствовать нескольким протоколам одновременно, комбинируя различные функциональные возможности.
Динамическое связывание
Динамическое связывание определяет, какая реализация метода будет вызвана во время выполнения программы. Swift использует таблицы виртуальных методов для реализации динамического связывания.
class PaymentMethod {
func processPayment(amount: Double) {
print("Обработка платежа: \(amount)")
}
}
class CreditCardPayment: PaymentMethod {
override func processPayment(amount: Double) {
print("Обработка платежа кредитной картой: \(amount)")
}
}
class PayPalPayment: PaymentMethod {
override func processPayment(amount: Double) {
print("Обработка платежа через PayPal: \(amount)")
}
}
class CryptoPayment: PaymentMethod {
override func processPayment(amount: Double) {
print("Обработка платежа криптовалютой: \(amount)")
}
}
class PaymentProcessor {
var paymentMethod: PaymentMethod
init(paymentMethod: PaymentMethod) {
self.paymentMethod = paymentMethod
}
func executePayment(amount: Double) {
paymentMethod.processPayment(amount: amount)
}
}
let processor = PaymentProcessor(paymentMethod: CreditCardPayment())
processor.executePayment(amount: 1000.0)
processor.paymentMethod = PayPalPayment()
processor.executePayment(amount: 1000.0)
processor.paymentMethod = CryptoPayment()
processor.executePayment(amount: 1000.0)
Динамическое связывание позволяет изменять поведение объекта во время выполнения. Это обеспечивает гибкость и адаптивность программных систем.
Абстрактные классы и методы
Swift не имеет встроенной поддержки абстрактных классов, но этот паттерн можно эмулировать через протоколы или базовые классы с фатальными ошибками.
protocol ShapeProtocol {
func area() -> Double
func perimeter() -> Double
func draw()
}
class AbstractShape {
func area() -> Double {
fatalError("Метод area() должен быть переопределен")
}
func perimeter() -> Double {
fatalError("Метод perimeter() должен быть переопределен")
}
func draw() {
fatalError("Метод draw() должен быть переопределен")
}
}
class Rectangle: AbstractShape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
override func area() -> Double {
return width * height
}
override func perimeter() -> Double {
return 2 * (width + height)
}
override func draw() {
print("Рисование прямоугольника \(width)x\(height)")
}
}
class Ellipse: AbstractShape {
var radiusX: Double
var radiusY: Double
init(radiusX: Double, radiusY: Double) {
self.radiusX = radiusX
self.radiusY = radiusY
}
override func area() -> Double {
return Double.pi * radiusX * radiusY
}
override func perimeter() -> Double {
return 2 * Double.pi * sqrt((radiusX * radiusX + radiusY * radiusY) / 2)
}
override func draw() {
print("Рисование эллипса с радиусами \(radiusX) и \(radiusY)")
}
}
Абстрактные классы определяют общую структуру без конкретной реализации. Производные классы обязаны предоставить реализацию всех абстрактных методов.
Модификаторы доступа
Уровни доступа в Swift
Swift предоставляет пять уровней доступа для контроля видимости кода. Каждый уровень определяет, где может использоваться объявление.
public class PublicClass {
public var publicProperty: String = "Публичное"
internal var internalProperty: String = "Внутреннее"
fileprivate var fileprivateProperty: String = "Файловое"
private var privateProperty: String = "Приватное"
public func publicMethod() {
print("Публичный метод")
}
internal func internalMethod() {
print("Внутренний метод")
}
fileprivate func fileprivateMethod() {
print("Файловый метод")
}
private func privateMethod() {
print("Приватный метод")
}
}
internal class InternalClass {
// По умолчанию все объявления internal
var property: String = "Свойство"
func method() {
print("Метод")
}
}
fileprivate class FilePrivateClass {
var property: String = "Свойство"
}
private class PrivateClass {
var property: String = "Свойство"
}
Уровни доступа защищают внутреннюю реализацию от несанкционированного использования. Они помогают создавать четкие границы между компонентами системы.
Public доступ
Ключевое слово public делает объявление доступным из любого модуля. Это используется для API, которые должны быть доступны внешним пользователям фреймворка.
public class NetworkManager {
public static let shared = NetworkManager()
public func fetchData(from url: String, completion: @escaping (Data?) -> Void) {
// Реализация загрузки данных
print("Загрузка данных с \(url)")
completion(nil)
}
public func uploadData(_ data: Data, to url: String, completion: @escaping (Bool) -> Void) {
// Реализация загрузки данных
print("Отправка данных на \(url)")
completion(true)
}
}
public struct User {
public let id: Int
public let name: String
public let email: String
public init(id: Int, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
public func displayName() -> String {
return name
}
}
Публичный доступ определяет стабильный интерфейс модуля. Изменения в публичном API требуют обновления версии и могут повлиять на пользователей фреймворка.
Internal доступ
Ключевое слово internal делает объявление доступным в пределах текущего модуля. Это уровень доступа по умолчанию для всех объявлений в Swift.
class DatabaseManager {
static let shared = DatabaseManager()
func connect() {
print("Подключение к базе данных")
}
func disconnect() {
print("Отключение от базы данных")
}
func execute(query: String) -> [String: Any] {
print("Выполнение запроса: \(query)")
return [:]
}
}
struct CacheEntry {
let key: String
let value: Any
let timestamp: Date
func isExpired(after seconds: TimeInterval) -> Bool {
return Date().timeIntervalSince(timestamp) > seconds
}
}
enum ErrorType {
case network
case database
case validation
case unknown
var description: String {
switch self {
case .network: return "Ошибка сети"
case .database: return "Ошибка базы данных"
case .validation: return "Ошибка валидации"
case .unknown: return "Неизвестная ошибка"
}
}
}
Внутренний доступ обеспечивает инкапсуляцию на уровне модуля. Компоненты могут взаимодействовать внутри модуля, но скрыты от внешнего кода.
File-private доступ
Ключевое слово fileprivate ограничивает доступ объявления пределами одного файла исходного кода. Это полезно для вспомогательных функций и типов, используемых только в одном файле.
// File: DataProcessor.swift
fileprivate struct ParserState {
var position: Int = 0
var buffer: [Character] = []
mutating func advance() {
position += 1
}
func current() -> Character? {
guard position < buffer.count else { return nil }
return buffer[position]
}
}
fileprivate func validateFormat(_ data: String) -> Bool {
return !data.isEmpty && data.count <= 1000
}
fileprivate func normalizeData(_ data: String) -> String {
return data.trimmingCharacters(in: .whitespacesAndNewlines)
}
class DataProcessor {
func process(_ rawData: String) -> String {
guard validateFormat(rawData) else {
return ""
}
let normalized = normalizeData(rawData)
var state = ParserState()
state.buffer = Array(normalized)
// Обработка данных
return normalized
}
}
Файловый доступ изолирует вспомогательную функциональность. Это предотвращает загрязнение глобального пространства имен модуля.
Private доступ
Ключевое слово private ограничивает доступ объявления пределами текущего объявления типа. Это самый строгий уровень доступа в Swift.
class BankAccount {
private var balance: Double = 0.0
private var transactionHistory: [Transaction] = []
private struct Transaction {
let amount: Double
let date: Date
let type: TransactionType
enum TransactionType {
case deposit
case withdrawal
}
}
private func logTransaction(_ transaction: Transaction) {
transactionHistory.append(transaction)
}
private func validateWithdrawal(_ amount: Double) -> Bool {
return balance >= amount
}
func deposit(_ amount: Double) {
guard amount > 0 else { return }
balance += amount
let transaction = Transaction(
amount: amount,
date: Date(),
type: .deposit
)
logTransaction(transaction)
}
func withdraw(_ amount: Double) -> Bool {
guard amount > 0, validateWithdrawal(amount) else { return false }
balance -= amount
let transaction = Transaction(
amount: amount,
date: Date(),
type: .withdrawal
)
logTransaction(transaction)
return true
}
func getBalance() -> Double {
return balance
}
}
Приватный доступ обеспечивает максимальную инкапсуляцию. Внутренние детали реализации полностью скрыты от внешнего кода.
Стратегии использования модификаторов доступа
Модификаторы доступа следует использовать для создания четких границ между компонентами. Публичный интерфейс должен быть минимальным и стабильным.
Внутренние детали реализации должны быть скрыты с помощью приватных или файловых модификаторов. Это позволяет изменять реализацию без влияния на внешний код.
Модификаторы доступа помогают документировать намерения разработчика. Они явно указывают, какие части кода предназначены для внешнего использования.
Протоколы и расширения
Протоколы как контракты
Протоколы определяют интерфейс, который типы обязаны реализовать. Они задают набор требований к свойствам, методам и другим функциональным возможностям без предоставления конкретной реализации.
protocol Identifiable {
var id: String { get }
func generateID() -> String
}
protocol Loggable {
func log(message: String)
var logLevel: LogLevel { get set }
}
enum LogLevel {
case debug, info, warning, error
}
class User: Identifiable, Loggable {
var id: String
var name: String
var logLevel: LogLevel = .info
init(id: String, name: String) {
self.id = id
self.name = name
}
func generateID() -> String {
return UUID().uuidString
}
func log(message: String) {
print("[\(logLevel)] \(name): \(message)")
}
}
struct NetworkRequest: Identifiable {
var id: String
var url: String
var method: String
func generateID() -> String {
return "\(method)-\(url.hashValue)"
}
}
Протоколы позволяют группировать типы по функциональности, а не по иерархии наследования. Различные классы, структуры и перечисления могут соответствовать одному протоколу, обеспечивая полиморфное поведение.
Расширения протоколов
Расширения протоколов предоставляют реализацию по умолчанию для требований протокола. Типы, соответствующие протоколу, могут использовать эту реализацию или предоставить собственную.
extension Loggable {
func logDebug(_ message: String) {
guard logLevel == .debug else { return }
log(message: "[DEBUG] \(message)")
}
func logError(_ message: String) {
logLevel = .error
log(message: "[ERROR] \(message)")
}
mutating func setLogLevel(_ level: LogLevel) {
logLevel = level
}
}
extension Identifiable {
func isValidID() -> Bool {
return !id.isEmpty && id.count >= 5
}
func prefixID(with prefix: String) -> String {
return "\(prefix)-\(id)"
}
}
class Service: Loggable {
var logLevel: LogLevel = .warning
func log(message: String) {
print("SERVICE LOG: \(message)")
}
}
let service = Service()
service.logDebug("Начало обработки") // Не выводится при уровне .warning
service.logError("Критическая ошибка") // Выводится с изменением уровня
Расширения протоколов уменьшают дублирование кода. Общая функциональность реализуется один раз и становится доступной всем типам, соответствующим протоколу.
Условное соответствие протоколам
Типы могут соответствовать протоколам только при выполнении определённых условий. Условное соответствие позволяет добавлять функциональность динамически на основе характеристик типа.
protocol JSONRepresentable {
func toJSON() -> [String: Any]
}
extension Array: JSONRepresentable where Element: JSONRepresentable {
func toJSON() -> [String: Any] {
return ["items": self.map { $0.toJSON() }]
}
}
extension Dictionary: JSONRepresentable where Value: JSONRepresentable {
func toJSON() -> [String: Any] {
return self.mapValues { $0.toJSON() }
}
}
class Product: JSONRepresentable {
var id: Int
var name: String
var price: Double
init(id: Int, name: String, price: Double) {
self.id = id
self.name = name
self.price = price
}
func toJSON() -> [String: Any] {
return [
"id": id,
"name": name,
"price": price
]
}
}
let products = [
Product(id: 1, name: "Ноутбук", price: 50000.0),
Product(id: 2, name: "Мышь", price: 1500.0)
]
let json = products.toJSON()
print(json)
Условное соответствие делает систему типов более гибкой. Функциональность добавляется только тем типам, которые могут её корректно реализовать.
Автоматический подсчёт ссылок
Принцип работы ARC
Automatic Reference Counting управляет памятью объектов в среде выполнения Swift. Система отслеживает количество активных ссылок на каждый объект и освобождает память, когда ссылки исчезают.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) создан")
}
deinit {
print("\(name) уничтожен")
}
}
var person1: Person? = Person(name: "Анна")
var person2: Person? = person1
var person3: Person? = person1
person2 = nil
person3 = nil
person1 = nil
// Вывод:
// Анна создана
// Анна уничтожен
ARC увеличивает счётчик ссылок при присваивании объекта новой переменной или передаче в функцию. Счётчик уменьшается при выходе переменной из области видимости или присваивании nil.
Сильные ссылки
Сильные ссылки являются стандартным типом ссылок в Swift. Каждая сильная ссылка увеличивает счётчик удержания объекта, предотвращая его уничтожение.
class Department {
var name: String
var manager: Employee?
init(name: String) {
self.name = name
}
}
class Employee {
var name: String
var department: Department?
init(name: String) {
self.name = name
}
}
var itDepartment: Department? = Department(name: "IT")
var developer: Employee? = Employee(name: "Иван")
itDepartment?.manager = developer
developer?.department = itDepartment
itDepartment = nil
developer = nil
// Объекты не уничтожены — образовалась циклическая зависимость
Сильные ссылки обеспечивают безопасность памяти в большинстве сценариев. Проблемы возникают при создании циклических зависимостей между объектами.
Слабые ссылки
Слабые ссылки не увеличивают счётчик удержания объекта. Они автоматически становятся nil при уничтожении целевого объекта, разрывая циклические зависимости.
class Apartment {
let unit: String
weak var tenant: Person?
init(unit: String) {
self.unit = unit
}
}
class PersonWithApartment {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) освобождает квартиру")
}
}
var person: PersonWithApartment? = PersonWithApartment(name: "Мария")
var apartment: Apartment? = Apartment(unit: "42B")
person?.apartment = apartment
apartment?.tenant = person
person = nil
// Вывод: Мария освобождает квартиру
// Квартира автоматически теряет ссылку на жильца
Слабые ссылки применяются в отношениях «родитель-ребёнок», где ребёнок ссылается на родителя. Это предотвращает утечки памяти при циклических связях.
Независимые ссылки
Независимые ссылки похожи на слабые, но не становятся nil автоматически. Они предполагают, что объект будет существовать дольше, чем ссылка на него. Использование независимой ссылки после уничтожения объекта вызывает критическую ошибку.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) удалён из системы")
}
}
class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Карта \(number) деактивирована")
}
}
var customer: Customer? = Customer(name: "Алексей")
customer?.card = CreditCard(number: "1234-5678-9012-3456", customer: customer!)
customer = nil
// Вывод:
// Карта 1234-5678-9012-3456 деактивирована
// Алексей удалён из системы
Независимые ссылки используются, когда жизненный цикл одного объекта полностью зависит от другого. Они обеспечивают небольшой выигрыш в производительности по сравнению со слабыми ссылками.
Инициализация и деинициализация
Фазы инициализации
Инициализация объекта в Swift происходит в две фазы. Первая фаза гарантирует, что все свойства получают начальные значения. Вторая фаза позволяет инициализатору обращаться к свойствам self и вызывать методы.
class Vehicle {
let brand: String
let model: String
var currentSpeed: Double
init(brand: String, model: String) {
// Фаза 1: инициализация всех свойств
self.brand = brand
self.model = model
self.currentSpeed = 0.0
// Фаза 2: можно использовать self
print("Создан \(brand) \(model)")
}
convenience init(brand: String) {
self.init(brand: brand, model: "Базовая модель")
}
}
class ElectricCar: Vehicle {
let batteryCapacity: Double
var chargeLevel: Double
init(brand: String, model: String, batteryCapacity: Double) {
// Фаза 1 дочернего класса
self.batteryCapacity = batteryCapacity
self.chargeLevel = 100.0
// Вызов инициализатора родителя до завершения фазы 1
super.init(brand: brand, model: model)
// Фаза 2 дочернего класса
print("Электромобиль готов, заряд: \(chargeLevel)%")
}
}
Двухфазная инициализация предотвращает обращение к неинициализированным свойствам. Компилятор проверяет корректность последовательности инициализации на этапе сборки.
Обязательные инициализаторы
Ключевое слово required помечает инициализаторы, которые должны быть реализованы во всех дочерних классах. Это гарантирует единообразие создания объектов в иерархии наследования.
class DataModel {
let identifier: UUID
required init() {
self.identifier = UUID()
}
init(identifier: UUID) {
self.identifier = identifier
}
}
class UserAccount: DataModel {
var username: String
required init() {
self.username = "guest"
super.init()
}
init(username: String) {
self.username = username
super.init()
}
}
class AdminAccount: UserAccount {
var privileges: [String]
required init() {
self.privileges = ["read", "write", "delete"]
super.init()
}
init(username: String, privileges: [String]) {
self.privileges = privileges
super.init(username: username)
}
}
Обязательные инициализаторы обеспечивают согласованность интерфейса создания объектов. Все классы в иерархии предоставляют одинаковые способы инициализации.
Отложенная инициализация свойств
Ключевое слово lazy откладывает инициализацию свойства до первого обращения к нему. Это полезно для ресурсоёмких объектов или свойств, зависящих от других частей состояния объекта.
class ImageProcessor {
let sourcePath: String
let targetFormat: String
lazy var image: NSImage = {
print("Загрузка изображения из \(sourcePath)")
return NSImage(contentsOfFile: sourcePath) ?? NSImage()
}()
lazy var filters: [ImageFilter] = {
print("Создание набора фильтров")
return [
BlurFilter(radius: 2.0),
ContrastFilter(level: 1.2),
SharpenFilter(amount: 0.8)
]
}()
lazy var cacheDirectory: URL = {
let path = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first
return path?.appendingPathComponent("image_processor") ?? URL(fileURLWithPath: "/tmp")
}()
init(sourcePath: String, targetFormat: String) {
self.sourcePath = sourcePath
self.targetFormat = targetFormat
print("Процессор изображений создан")
}
func process() {
// Свойства инициализируются только при первом обращении
_ = image
_ = filters
_ = cacheDirectory
print("Обработка завершена")
}
}
Отложенная инициализация оптимизирует использование ресурсов. Объекты создаются только при реальной необходимости, что уменьшает накладные расходы при создании экземпляра.
Обобщённое программирование
Обобщённые функции
Обобщённые функции работают с типами, не привязываясь к конкретной реализации. Они обеспечивают повторное использование кода для различных типов данных.
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temporary = a
a = b
b = temporary
}
func firstElement<T>(_ [T]) -> T? {
return array.first
}
func contains<T: Equatable>(_ array: [T], value: T) -> Bool {
return array.contains(value)
}
func merge<T>(_ first: [T], _ second: [T]) -> [T] {
return first + second
}
var x = 5
var y = 10
swapValues(&x, &y)
print("x = \(x), y = \(y)")
let names = ["Анна", "Борис", "Виктор"]
if let first = firstElement(names) {
print("Первое имя: \(first)")
}
let numbers = [1, 2, 3, 4, 5]
print("Содержит 3: \(contains(numbers, value: 3))")
let combined = merge([1, 2], [3, 4])
print("Объединённый массив: \(combined)")
Обобщённые функции повышают гибкость кода. Одна реализация работает с множеством типов, сохраняя при этом статическую типизацию.
Обобщённые типы
Обобщённые типы определяют классы, структуры и перечисления, параметризованные типами. Они позволяют создавать контейнеры и алгоритмы, независимые от конкретных типов данных.
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
func peek() -> Element? {
return items.last
}
var isEmpty: Bool {
return items.isEmpty
}
var count: Int {
return items.count
}
}
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let capacity: Int
init(capacity: Int) {
self.capacity = capacity
}
func get(_ key: Key) -> Value? {
return storage[key]
}
mutating func set(_ key: Key, value: Value) {
if storage.count >= capacity && !storage.keys.contains(key) {
storage.removeFirst()
}
storage[key] = value
}
mutating func remove(_ key: Key) {
storage.removeValue(forKey: key)
}
func contains(_ key: Key) -> Bool {
return storage.keys.contains(key)
}
}
enum Result<Value, ErrorType> {
case success(Value)
case failure(ErrorType)
var isSuccess: Bool {
if case .success = self { return true }
return false
}
var isFailure: Bool {
if case .failure = self { return true }
return false
}
func getValue() -> Value? {
if case .success(let value) = self {
return value
}
return nil
}
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
print(intStack.pop() ?? 0)
var stringCache = Cache<String, String>(capacity: 100)
stringCache.set("name", value: "Иван")
if let name = stringCache.get("name") {
print("Имя из кэша: \(name)")
}
let result: Result<Int, String> = .success(42)
if result.isSuccess, let value = result.getValue() {
print("Успешный результат: \(value)")
}
Обобщённые типы создают переиспользуемые компоненты. Стек, кэш или результат операции работают с любыми типами, сохраняя семантику контейнера.
Ограничения обобщённых параметров
Ограничения обобщённых параметров задают требования к типам, которые могут использоваться с обобщённой конструкцией. Тип должен соответствовать протоколу или быть подклассом указанного класса.
protocol Drawable {
func draw()
}
protocol Serializable {
func serialize() -> Data
}
class Renderer<T: Drawable> {
func render(_ item: T) {
item.draw()
}
}
class Serializer<T: Serializable> {
func serialize(_ item: T) -> Data {
return item.serialize()
}
}
class NetworkManager<T: Codable> {
func send(_ item: T) throws {
let data = try JSONEncoder().encode(item)
// Отправка данных
print("Отправлено \(data.count) байт")
}
func receive(data: Data) throws -> T {
return try JSONDecoder().decode(T.self, from: data)
}
}
class SortedArray<T: Comparable> {
private var elements: [T] = []
func insert(_ element: T) {
let index = elements.firstIndex(where: { $0 > element }) ?? elements.count
elements.insert(element, at: index)
}
func remove(_ element: T) -> Bool {
if let index = elements.firstIndex(of: element) {
elements.remove(at: index)
return true
}
return false
}
var sortedElements: [T] {
return elements
}
}
struct Point: Comparable, Codable {
var x: Double
var y: Double
static func < (lhs: Point, rhs: Point) -> Bool {
return lhs.x < rhs.x || (lhs.x == rhs.x && lhs.y < rhs.y)
}
}
let points = SortedArray<Point>()
points.insert(Point(x: 3.0, y: 4.0))
points.insert(Point(x: 1.0, y: 2.0))
print(points.sortedElements)
Ограничения обобщённых параметров обеспечивают безопасность типов. Компилятор проверяет соответствие типов требованиям до выполнения программы.
Значимые и ссылочные типы
Семантика значений
Структуры и перечисления в Swift являются значимыми типами. При присваивании или передаче в функцию создаётся копия данных. Изменения в одной переменной не влияют на другие переменные.
struct Point {
var x: Double
var y: Double
mutating func moveBy(dx: Double, dy: Double) {
x += dx
y += dy
}
}
var point1 = Point(x: 0.0, y: 0.0)
var point2 = point1
point2.moveBy(dx: 5.0, dy: 5.0)
print("point1: (\(point1.x), \(point1.y))") // (0.0, 0.0)
print("point2: (\(point2.x), \(point2.y))") // (5.0, 5.0)
func modifyPoint(_ point: inout Point) {
point.moveBy(dx: 10.0, dy: 10.0)
}
modifyPoint(&point1)
print("point1 после модификации: (\(point1.x), \(point1.y))") // (10.0, 10.0)
Значимые типы обеспечивают предсказуемость поведения. Каждая переменная управляет собственными данными, что упрощает рассуждение о состоянии программы.
Семантика ссылок
Классы являются ссылочными типами. Переменные хранят ссылки на объект в памяти. При присваивании копируется ссылка, а не объект. Все переменные ссылаются на один и тот же экземпляр.
class Counter {
var value: Int = 0
func increment() {
value += 1
}
func reset() {
value = 0
}
}
let counter1 = Counter()
let counter2 = counter1
counter1.increment()
counter1.increment()
print("counter1.value = \(counter1.value)") // 2
print("counter2.value = \(counter2.value)") // 2
counter2.reset()
print("counter1.value = \(counter1.value)") // 0
Ссылочные типы позволяют разделять состояние между различными частями программы. Это полезно для управления общими ресурсами, такими как сетевые соединения или базы данных.
Выбор между структурами и классами
Структуры предпочтительны для простых моделей данных, не имеющих идентичности. Они подходят для геометрических примитивов, диапазонов, точек в пространстве.
struct Rectangle {
var origin: Point
var size: Size
var area: Double {
return size.width * size.height
}
var center: Point {
return Point(
x: origin.x + size.width / 2,
y: origin.y + size.height / 2
)
}
}
struct RGBColor {
var red: UInt8
var green: UInt8
var blue: UInt8
var grayscale: UInt8 {
return UInt8(Double(red) * 0.299 + Double(green) * 0.587 + Double(blue) * 0.114)
}
}
Классы необходимы при работе с объектами, имеющими уникальную идентичность. Они подходят для моделей предметной области, контроллеров, делегатов и других сущностей с жизненным циклом.
class Document {
let identifier: UUID
var content: String
var lastModified: Date
init(content: String) {
self.identifier = UUID()
self.content = content
self.lastModified = Date()
}
func save() {
lastModified = Date()
// Сохранение документа
}
}
class NetworkSession {
private let session: URLSession
private var activeTasks: [URLSessionTask] = []
init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}
func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
let task = session.dataTask(with: url, completionHandler: completion)
activeTasks.append(task)
task.resume()
}
func cancelAllTasks() {
activeTasks.forEach { $0.cancel() }
activeTasks.removeAll()
}
}
Выбор между структурами и классами влияет на архитектуру приложения. Значимые типы упрощают многопоточность и тестирование, ссылочные типы обеспечивают совместное использование состояния.