Лекция 10. Протоколы и расширения
1. Введение
Протоколы — одна из центральных концепций Swift. Если наследование (лекция 8) позволяет строить вертикальные иерархии классов, то протоколы задают горизонтальные контракты: набор требований, которые тип обязуется выполнить. В отличие от классов, протоколы могут принимать структуры, перечисления и классы одновременно. Расширения дополняют эту систему, позволяя добавлять функциональность к уже существующим типам — даже к стандартным Int, String и Array.
В этой лекции мы изучим протоколы, расширения, протоколо-ориентированное программирование (POP) и сравним подход Swift с duck typing и ABC в Python.
2. Протоколы (Protocols) — аналог интерфейсов
Протокол определяет контракт — набор свойств и методов, которые тип обязан реализовать. Протокол сам не содержит реализации (за исключением расширений протоколов, о которых позже).
protocol Describable { var description: String { get } func describe() -> String}Сравнение с Python:
from abc import ABC, abstractmethod
class Describable(ABC): @abstractmethod def describe(self) -> str: passВ Python абстрактные классы (ABC) выполняют похожую роль, но проверка происходит в рантайме. В Swift несоответствие протоколу — ошибка компиляции.
3. Требования к свойствам: { get } и { get set }
Протокол указывает, должно ли свойство быть только для чтения или для чтения и записи:
protocol Identifiable { var id: String { get } // только чтение var name: String { get set } // чтение и запись}{ get }— тип может реализовать как stored property, так и computed read-only или read-write.{ get set }— свойство обязано поддерживать и чтение, и запись: storedvarили computed с геттером и сеттером.
struct User: Identifiable { let id: String // let допустим для { get } var name: String // var необходим для { get set }}
let user = User(id: "001", name: "Анна")print(user.id) // 001print(user.name) // АннаВажно: let удовлетворяет { get }, но не удовлетворяет { get set }.
4. Требования к методам
4.1. Обычные методы
protocol Drawable { func draw() -> String}
struct Circle: Drawable { var radius: Double func draw() -> String { return "Рисуем круг с радиусом \(radius)" }}4.2. Методы mutating
Для структур и перечислений метод, изменяющий self, должен быть помечен mutating и в протоколе:
protocol Togglable { mutating func toggle()}
enum Switch: Togglable { case on, off mutating func toggle() { self = (self == .on) ? .off : .on }}
var lamp = Switch.offlamp.toggle()print(lamp) // onПримечание: классы реализуют mutating-требование без ключевого слова mutating, так как классы — ссылочные типы и всегда могут изменять свои свойства.
5. Требования к инициализаторам
Протокол может требовать определённый инициализатор:
protocol Creatable { init(value: Int)}Для классов при соответствии требуется required:
class Counter: Creatable { var count: Int required init(value: Int) { self.count = value }}Для структур required не нужен — структуры не наследуются:
struct Point: Creatable { var x: Int var y: Int init(value: Int) { self.x = value self.y = value }}6. Принятие (conformance) протоколов
Протоколы могут принимать структуры, классы и перечисления — это главное отличие от наследования (только классы):
protocol Printable { func formatted() -> String}
// Структураstruct Book: Printable { var title: String func formatted() -> String { return "Книга: \(title)" }}
// Классclass Magazine: Printable { var issue: Int init(issue: Int) { self.issue = issue } func formatted() -> String { return "Журнал №\(issue)" }}
// Перечислениеenum Season: Printable { case spring, summer, autumn, winter func formatted() -> String { switch self { case .spring: return "Весна" case .summer: return "Лето" case .autumn: return "Осень" case .winter: return "Зима" } }}
print(Book(title: "Война и мир").formatted()) // Книга: Война и мирprint(Magazine(issue: 42).formatted()) // Журнал №42print(Season.winter.formatted()) // ЗимаТип может принимать несколько протоколов через запятую:
protocol Named { var name: String { get }}
protocol Aged { var age: Int { get }}
struct Student: Named, Aged { var name: String var age: Int}В Python множественное наследование решает ту же задачу, но с рисками конфликтов (diamond problem). В Swift протоколы безопаснее — они не содержат состояния.
7. Протокол как тип
Протокол можно использовать как тип переменной, параметр функции или элемент коллекции:
protocol Shape { func area() -> Double func describe() -> String}
struct CircleShape: Shape { var radius: Double func area() -> Double { return Double.pi * radius * radius } func describe() -> String { return "Круг r=\(radius)" }}
struct Rect: Shape { var width: Double, height: Double func area() -> Double { return width * height } func describe() -> String { return "Прямоугольник \(width)×\(height)" }}
// Переменная протокольного типаlet figure: Shape = CircleShape(radius: 5.0)print(figure.area()) // 78.5398...
// Параметр функцииfunc printArea(of shape: Shape) { print("\(shape.describe()): площадь = \(shape.area())")}printArea(of: Rect(width: 3, height: 4)) // Прямоугольник 3.0×4.0: площадь = 12.0
// Коллекцияlet shapes: [Shape] = [ CircleShape(radius: 2.0), Rect(width: 5, height: 3), CircleShape(radius: 1.0),]let totalArea = shapes.reduce(0.0) { $0 + $1.area() }print("Общая площадь: \(totalArea)")В Python duck typing позволяет передавать любой объект с нужными методами без объявления. В Swift контракт проверяется на этапе компиляции.
8. Композиция протоколов: Protocol1 & Protocol2
Можно потребовать, чтобы значение соответствовало нескольким протоколам одновременно:
protocol Named2 { var name: String { get }}
protocol Greetable { func greet() -> String}
struct Employee: Named2, Greetable { var name: String func greet() -> String { return "Привет, я \(name)!" }}
func welcome(person: Named2 & Greetable) { print("\(person.name): \(person.greet())")}
welcome(person: Employee(name: "Ольга"))// Ольга: Привет, я Ольга!Композиция протоколов — альтернатива множественному наследованию без его проблем.
9. Проверка соответствия: is, as?, as!
Эти операторы работают с протоколами так же, как с классами (лекция 8):
protocol Animal { var species: String { get }}
struct Dog: Animal { var species: String { return "Собака" } var name: String}
struct Cat: Animal { var species: String { return "Кошка" } var color: String}
let animals: [Animal] = [ Dog(name: "Рекс"), Cat(color: "рыжий"), Dog(name: "Бобик"),]
for animal in animals { if animal is Dog { print("\(animal.species) найдена") } if let cat = animal as? Cat { print("Кошка цвета: \(cat.color)") }}// Собака найдена// Кошка цвета: рыжий// Собака найдена| Оператор | Назначение | Безопасность |
|---|---|---|
is | Проверка соответствия | Всегда безопасен |
as? | Опциональное приведение | Возвращает nil при неудаче |
as! | Принудительное приведение | Крах при неудаче |
10. Протоколо-ориентированное программирование (POP)
POP — философия Swift, озвученная Apple на WWDC 2015: «Начинайте с протокола, а не с класса».
Отличия от классического ООП:
| Аспект | ООП | POP |
|---|---|---|
| Основной строительный блок | Класс | Протокол + структура |
| Повторное использование | Наследование | Композиция протоколов + расширения |
| Тип значения | Reference (класс) | Value (структура) |
| Иерархия | Вертикальная | Горизонтальная |
Пример POP-подхода:
protocol Flyable { var maxAltitude: Int { get } func fly() -> String}
protocol Swimmable { var maxDepth: Int { get } func swim() -> String}
struct Duck: Flyable, Swimmable { var maxAltitude: Int { return 500 } var maxDepth: Int { return 2 } func fly() -> String { return "Утка летит на высоте до \(maxAltitude) м" } func swim() -> String { return "Утка плывёт на глубине до \(maxDepth) м" }}
struct Penguin: Swimmable { var maxDepth: Int { return 50 } func swim() -> String { return "Пингвин ныряет до \(maxDepth) м" }}
let duck = Duck()print(duck.fly()) // Утка летит на высоте до 500 мprint(duck.swim()) // Утка плывёт на глубине до 2 мВ ООП пришлось бы создавать базовый класс Bird и мучиться с тем, что пингвин не летает. POP позволяет комбинировать способности через протоколы без жёстких иерархий.
11. Расширения (Extensions)
Расширения добавляют новую функциональность к уже существующему типу — без доступа к исходному коду и без наследования.
Что можно добавить через расширения:
- Вычисляемые свойства (computed properties)
- Методы (обычные и
mutating) - Новые инициализаторы
- Соответствие протоколам
Что нельзя добавить:
- Хранимые свойства (stored properties)
- Наблюдатели свойств (
willSet,didSet) к существующим свойствам
11.1. Добавление методов и вычисляемых свойств
extension Int { var isEven: Bool { return self % 2 == 0 } var isOdd: Bool { return !isEven } var squared: Int { return self * self }
func times(_ action: () -> Void) { for _ in 0..<self { action() } }}
print(7.isEven) // falseprint(7.isOdd) // trueprint(5.squared) // 25
3.times { print("Привет!") }// Привет!// Привет!// Привет!Сравнение с Python — в Python нельзя добавить метод к int напрямую. Можно использовать наследование или monkey-patching (плохая практика):
# Python: нельзя расширить int напрямуюclass MyInt(int): @property def is_even(self): return self % 2 == 011.2. Расширение строк
extension String { var wordCount: Int { return self.split(separator: " ").count }
func repeated(n: Int) -> String { return String(repeating: self, count: n) }
var reversed_str: String { return String(self.reversed()) }}
let text = "Swift это мощный язык"print(text.wordCount) // 4print("Ха".repeated(n: 3)) // ХаХаХаprint("Привет".reversed_str) // тевирП11.3. Расширение массивов
extension Array where Element: Numeric { func sum() -> Element { return self.reduce(0, +) }}
extension Array where Element: Comparable { func minMax() -> (min: Element, max: Element)? { guard let minVal = self.min(), let maxVal = self.max() else { return nil } return (minVal, maxVal) }}
print([1, 2, 3, 4, 5].sum()) // 15print([3.14, 2.71, 1.41].sum()) // 7.26
if let result = [10, 3, 7, 1, 9].minMax() { print("Мин: \(result.min), Макс: \(result.max)") // Мин: 1, Макс: 9}12. Расширения с условиями (conditional conformance): where
Ключевое слово where позволяет ограничить расширение условием на тип:
protocol Summarizable { func summary() -> String}
struct Score: Summarizable { var value: Int func summary() -> String { return "Счёт: \(value)" }}
// Расширение Array — но только если элементы соответствуют Summarizableextension Array where Element: Summarizable { func printAll() { for item in self { print(item.summary()) } }}
let scores = [Score(value: 90), Score(value: 75), Score(value: 88)]scores.printAll()// Счёт: 90// Счёт: 75// Счёт: 88
// [1, 2, 3].printAll() // Ошибка: Int не соответствует SummarizableУсловное соответствие (conditional conformance) — мощный механизм, позволяющий обобщённому типу соответствовать протоколу только при выполнении условия:
protocol Displayable { var displayText: String { get }}
extension Int: Displayable { var displayText: String { return "Число: \(self)" }}
// Optional соответствует Displayable, если Wrapped соответствует Displayableextension Optional: Displayable where Wrapped: Displayable { var displayText: String { switch self { case .some(let value): return value.displayText case .none: return "Пусто" } }}
let x: Int? = 42let y: Int? = nilprint(x.displayText) // Число: 42print(y.displayText) // Пусто13. Расширения протоколов — реализации по умолчанию
Это одна из самых мощных возможностей Swift. Расширение протокола предоставляет реализации по умолчанию для методов и вычисляемых свойств:
protocol Loggable { var logPrefix: String { get } func log(_ message: String)}
// Реализация по умолчаниюextension Loggable { var logPrefix: String { return "[LOG]" }
func log(_ message: String) { print("\(logPrefix) \(message)") }}
struct Server: Loggable { // Использует реализации по умолчанию}
struct Database: Loggable { var logPrefix: String { return "[DB]" } // Переопределяет}
Server().log("Запуск") // [LOG] ЗапускDatabase().log("Подключение") // [DB] ПодключениеТип может переопределить реализацию по умолчанию, предоставив свою. Это похоже на миксины в Python, но с гарантиями компилятора.
13.1. Практический пример: протокол с расширением
protocol Validatable { var isValid: Bool { get }}
extension Validatable { func validate() -> String { return isValid ? "Данные корректны" : "Ошибка валидации" }}
struct Email: Validatable { var address: String var isValid: Bool { return address.contains("@") && address.contains(".") }}
struct Password: Validatable { var text: String var isValid: Bool { return text.count >= 8 }}
let email = Email(address: "user@example.com")let password = Password(text: "12345")
print(email.validate()) // Данные корректныprint(password.validate()) // Ошибка валидации14. Стандартные протоколы Swift
Swift активно использует протоколы в стандартной библиотеке. Рассмотрим наиболее важные.
14.1. CustomStringConvertible
Определяет текстовое представление типа (аналог __str__ в Python):
struct City: CustomStringConvertible { var name: String var population: Int
var description: String { return "\(name) (население: \(population))" }}
let city = City(name: "Москва", population: 12_600_000)print(city) // Москва (население: 12600000)14.2. Equatable
Позволяет сравнивать экземпляры через == и != (аналог __eq__ в Python):
struct Coordinate: Equatable { var x: Double var y: Double}
let a = Coordinate(x: 1.0, y: 2.0)let b = Coordinate(x: 1.0, y: 2.0)let c = Coordinate(x: 3.0, y: 4.0)
print(a == b) // trueprint(a == c) // falseДля структур, где все свойства Equatable, Swift автоматически синтезирует реализацию.
14.3. Comparable
Позволяет сравнивать <, >, <=, >= и сортировать (аналог __lt__ в Python):
struct Temperature: Comparable { var celsius: Double
static func < (lhs: Temperature, rhs: Temperature) -> Bool { return lhs.celsius < rhs.celsius }}
let temps = [ Temperature(celsius: 36.6), Temperature(celsius: 0.0), Temperature(celsius: 100.0),]let sorted = temps.sorted()print(sorted.map { $0.celsius }) // [0.0, 36.6, 100.0]14.4. Hashable
Позволяет использовать тип как ключ словаря или элемент множества (аналог __hash__ в Python):
struct PlayerID: Hashable { var id: Int var server: String}
var scores: [PlayerID: Int] = [:]scores[PlayerID(id: 1, server: "EU")] = 1500scores[PlayerID(id: 2, server: "US")] = 1200
print(scores)14.5. Codable
Протокол для кодирования и декодирования данных (JSON, Plist и др.):
import Foundation
struct Product: Codable { var name: String var price: Double var inStock: Bool}
let product = Product(name: "Клавиатура", price: 2500.0, inStock: true)
// Кодирование в JSONlet encoder = JSONEncoder()encoder.outputFormatting = .prettyPrintedif let data = try? encoder.encode(product), let json = String(data: data, encoding: .utf8) { print(json)}// {// "name" : "Клавиатура",// "price" : 2500,// "inStock" : true// }
// Декодирование из JSONlet jsonString = """{"name": "Мышь", "price": 800.0, "inStock": false}"""let decoder = JSONDecoder()if let jsonData = jsonString.data(using: .utf8), let decoded = try? decoder.decode(Product.self, from: jsonData) { print(decoded.name) // Мышь print(decoded.price) // 800.0}В Python аналог — модуль json с ручной сериализацией или библиотеки вроде pydantic.
15. Комплексный пример: протоколы, расширения и POP
Объединим все концепции лекции в одном примере — система обработки геометрических фигур:
import Foundation
// --- Протоколы ---protocol GeometricShape: CustomStringConvertible { var name: String { get } func area() -> Double func perimeter() -> Double}
// Реализация по умолчаниюextension GeometricShape { var description: String { return "\(name): площадь=\(String(format: "%.2f", area())), периметр=\(String(format: "%.2f", perimeter()))" }}
protocol Scalable { mutating func scale(by factor: Double)}
// --- Типы, соответствующие протоколам ---struct GCircle: GeometricShape, Scalable { var name: String { return "Круг" } var radius: Double
func area() -> Double { return Double.pi * radius * radius } func perimeter() -> Double { return 2 * Double.pi * radius } mutating func scale(by factor: Double) { radius *= factor }}
struct GRect: GeometricShape, Scalable { var name: String { return "Прямоугольник" } var width: Double var height: Double
func area() -> Double { return width * height } func perimeter() -> Double { return 2 * (width + height) } mutating func scale(by factor: Double) { width *= factor; height *= factor }}
struct Triangle: GeometricShape { var name: String { return "Треугольник" } var a: Double, b: Double, c: Double
func area() -> Double { let s = (a + b + c) / 2 return (s * (s - a) * (s - b) * (s - c)).squareRoot() } func perimeter() -> Double { return a + b + c }}
// --- Расширение массива фигур ---extension Array where Element: GeometricShape { func totalArea() -> Double { return self.reduce(0.0) { $0 + $1.area() } }
func largest() -> Element? { return self.max(by: { $0.area() < $1.area() }) }}
// --- Использование ---let shapes: [any GeometricShape] = [ GCircle(radius: 5.0), GRect(width: 4.0, height: 6.0), Triangle(a: 3, b: 4, c: 5),]
for shape in shapes { print(shape) // Используется description из расширения протокола}// Круг: площадь=78.54, периметр=31.42// Прямоугольник: площадь=24.00, периметр=20.00// Треугольник: площадь=6.00, периметр=12.00
// Масштабирование через протокол Scalablevar circle = GCircle(radius: 3.0)circle.scale(by: 2.0)print("После масштабирования: \(circle)")// После масштабирования: Круг: площадь=113.10, периметр=37.7016. Сравнение с Python
| Аспект | Swift | Python |
|---|---|---|
| Интерфейсы | Протоколы (protocol) | ABC + @abstractmethod |
| Проверка соответствия | Компиляция | Рантайм |
| Расширение типов | extension | Monkey-patching (плохая практика) |
| Duck typing | Нет — строгие протоколы | Да — «если ходит как утка…» |
| Множественное поведение | Композиция протоколов | Множественное наследование |
| Реализации по умолчанию | Расширения протоколов | Миксины, базовые реализации |
| Кодирование JSON | Codable | json модуль / pydantic |
16.1. Protocol из typing (Python 3.8+)
Начиная с Python 3.8, модуль typing поддерживает структурное подтипирование (structural subtyping), похожее на протоколы Swift:
from typing import Protocol, runtime_checkable
@runtime_checkableclass Drawable(Protocol): def draw(self) -> str: ...
class Circle: def draw(self) -> str: return "Рисуем круг"
# Работает без явного наследования!def render(shape: Drawable) -> None: print(shape.draw())
render(Circle()) # Рисуем кругisinstance(Circle(), Drawable) # True (runtime_checkable)Это ближе к duck typing — Circle не наследует Drawable, но соответствует структурно. В Swift соответствие протоколу всегда явное — тип должен написать : Protocol.
17. Упражнения
Упражнение 1. Создайте протокол Payable с требованиями:
- свойство
salary: Double { get } - метод
monthlyPay() -> Double
Реализуйте структуры FullTimeEmployee (ежемесячная выплата = salary / 12) и Contractor (ежемесячная выплата = salary * количество отработанных дней / рабочих дней). Создайте массив [Payable] и выведите информацию о каждом.
Упражнение 2. Создайте протокол Stackable с ассоциированными типами не используя — только методы push, pop, peek, isEmpty. Реализуйте структуру IntStack, соответствующую этому протоколу. Добавьте расширение с реализацией метода count.
Упражнение 3. Расширьте тип Double:
- вычисляемое свойство
km(конвертация километров в метры) - вычисляемое свойство
celsius(создаёт строку с «°C») - метод
roundTo(places:)— округление до указанного количества знаков
Упражнение 4. Создайте протокол Serializable с методом toJSON() -> String. Добавьте реализацию по умолчанию через расширение протокола. Реализуйте две структуры, одна из которых переопределяет метод toJSON().
Упражнение 5. Перепишите следующий Python-код на Swift с использованием протоколов и расширений:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self) -> float: ...
@abstractmethod def perimeter(self) -> float: ...
def describe(self) -> str: return f"Площадь: {self.area():.2f}, Периметр: {self.perimeter():.2f}"
class Circle(Shape): def __init__(self, radius: float): self.radius = radius
def area(self) -> float: import math return math.pi * self.radius ** 2
def perimeter(self) -> float: import math return 2 * math.pi * self.radius
class Square(Shape): def __init__(self, side: float): self.side = side
def area(self) -> float: return self.side ** 2
def perimeter(self) -> float: return 4 * self.side
shapes = [Circle(5), Square(3)]for s in shapes: print(s.describe())Упражнение 6. Создайте структуры Student и Course, соответствующие Equatable, Comparable, CustomStringConvertible и Hashable. Продемонстрируйте сортировку, поиск в Set и вывод через print().
18. Вопросы для самопроверки
- Чем протокол отличается от абстрактного класса? Почему в Swift нет ключевого слова
abstract? - Объясните разницу между
{ get }и{ get set }в требованиях протокола к свойствам. Может лиlet-свойство удовлетворить{ get set }? - Зачем нужно ключевое слово
mutatingв протоколах? Должен ли класс писатьmutatingпри реализации? - Что такое протоколо-ориентированное программирование (POP)? В чём его преимущества перед классическим ООП?
- Как использовать протокол в качестве типа переменной? Приведите пример с массивом протокольного типа.
- Что такое композиция протоколов (
Protocol1 & Protocol2)? В каких случаях она полезна? - Что можно, а что нельзя добавить через расширения (extensions)?
- Как работают реализации по умолчанию через расширения протоколов? Что произойдёт, если тип предоставит собственную реализацию?
- Что такое
conditional conformance? Приведите пример сwhere. - Сравните подход Swift (протоколы + расширения) с Python (duck typing + ABC +
Protocolиз typing). Какие преимущества и недостатки у каждого?
19. Итоги
В этой лекции мы изучили:
- Протоколы — контракты для типов: требования к свойствам (
{ get },{ get set }), методам (обычным иmutating), инициализаторам. - Принятие протоколов структурами, классами и перечислениями — горизонтальное расширение возможностей без наследования.
- Протокол как тип — использование в переменных, параметрах и коллекциях.
- Композицию протоколов (
&) и проверку соответствия (is,as?,as!). - POP — протоколо-ориентированное программирование как философию Swift.
- Расширения — добавление вычисляемых свойств, методов и инициализаторов к существующим типам.
- Расширения стандартных типов (
Int,String,Array) и условные расширения сwhere. - Расширения протоколов — реализации по умолчанию, «миксины» с гарантиями компилятора.
- Стандартные протоколы:
CustomStringConvertible,Equatable,Comparable,Hashable,Codable.
В следующей лекции мы рассмотрим обобщения (Generics) — механизм написания универсального, типобезопасного кода, работающего с любыми типами.