Перейти к содержимому

Лекция 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 } — свойство обязано поддерживать и чтение, и запись: stored var или computed с геттером и сеттером.
struct User: Identifiable {
let id: String // let допустим для { get }
var name: String // var необходим для { get set }
}
let user = User(id: "001", name: "Анна")
print(user.id) // 001
print(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.off
lamp.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()) // Журнал №42
print(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) // false
print(7.isOdd) // true
print(5.squared) // 25
3.times { print("Привет!") }
// Привет!
// Привет!
// Привет!

Сравнение с Python — в Python нельзя добавить метод к int напрямую. Можно использовать наследование или monkey-patching (плохая практика):

# Python: нельзя расширить int напрямую
class MyInt(int):
@property
def is_even(self):
return self % 2 == 0

11.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) // 4
print("Ха".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()) // 15
print([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 — но только если элементы соответствуют Summarizable
extension 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 соответствует Displayable
extension Optional: Displayable where Wrapped: Displayable {
var displayText: String {
switch self {
case .some(let value): return value.displayText
case .none: return "Пусто"
}
}
}
let x: Int? = 42
let y: Int? = nil
print(x.displayText) // Число: 42
print(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) // true
print(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")] = 1500
scores[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)
// Кодирование в JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(product),
let json = String(data: data, encoding: .utf8) {
print(json)
}
// {
// "name" : "Клавиатура",
// "price" : 2500,
// "inStock" : true
// }
// Декодирование из JSON
let 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
// Масштабирование через протокол Scalable
var circle = GCircle(radius: 3.0)
circle.scale(by: 2.0)
print("После масштабирования: \(circle)")
// После масштабирования: Круг: площадь=113.10, периметр=37.70

16. Сравнение с Python

АспектSwiftPython
ИнтерфейсыПротоколы (protocol)ABC + @abstractmethod
Проверка соответствияКомпиляцияРантайм
Расширение типовextensionMonkey-patching (плохая практика)
Duck typingНет — строгие протоколыДа — «если ходит как утка…»
Множественное поведениеКомпозиция протоколовМножественное наследование
Реализации по умолчаниюРасширения протоколовМиксины, базовые реализации
Кодирование JSONCodablejson модуль / pydantic

16.1. Protocol из typing (Python 3.8+)

Начиная с Python 3.8, модуль typing поддерживает структурное подтипирование (structural subtyping), похожее на протоколы Swift:

from typing import Protocol, runtime_checkable
@runtime_checkable
class 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. Вопросы для самопроверки

  1. Чем протокол отличается от абстрактного класса? Почему в Swift нет ключевого слова abstract?
  2. Объясните разницу между { get } и { get set } в требованиях протокола к свойствам. Может ли let-свойство удовлетворить { get set }?
  3. Зачем нужно ключевое слово mutating в протоколах? Должен ли класс писать mutating при реализации?
  4. Что такое протоколо-ориентированное программирование (POP)? В чём его преимущества перед классическим ООП?
  5. Как использовать протокол в качестве типа переменной? Приведите пример с массивом протокольного типа.
  6. Что такое композиция протоколов (Protocol1 & Protocol2)? В каких случаях она полезна?
  7. Что можно, а что нельзя добавить через расширения (extensions)?
  8. Как работают реализации по умолчанию через расширения протоколов? Что произойдёт, если тип предоставит собственную реализацию?
  9. Что такое conditional conformance? Приведите пример с where.
  10. Сравните подход 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) — механизм написания универсального, типобезопасного кода, работающего с любыми типами.