1. Компилятор тут главный
Когда вы пишете программу, вы часто хотите сказать: «Мне неважно, кто ты — книга, пользователь или космический робот. Мне важно, чтобы ты умел вот это». В Swift для этого и существуют протоколы. Но протокол — это обещание, а обещания без проверки превращаются в «я точно приду на тренировку с понедельника».
Соответствие протоколу (conformance) — это момент, когда тип публично заявляет: «Да, я умею всё, что требует этот протокол», а компилятор отвечает: «Покажи». И действительно проверяет наличие всех требований и точность их сигнатур. Даже в сложных местах языка компилятор ведёт себя так же строго: если сигнатуры не совпали, будет ошибка, пока вы не приведёте всё к контракту.
2. Объявление конформанса и ошибки компилятора
Синтаксис соответствия выглядит очень по‑домашнему: после имени типа ставим двоеточие и перечисляем протоколы. Вроде просто, но именно в этой простоте кроется сила: читая объявление типа, вы сразу видите «какие роли он играет». Это похоже на бейджики на конференции: «спикер», «волонтёр», «участник» — и сразу понятнее, что от человека ждать.
Главное правило: как только вы написали : SomeProtocol, вы включили режим «компилятор как ревизор». Он не будет гадать, что вы «примерно это имели в виду». Он потребует всё строго по контракту.
import Foundation
protocol Named {
var name: String { get }
}
struct Book: Named {
let name: String
}
Здесь Book говорит: «Я Named». Компилятор смотрит: свойство name читается? Да. Значит — ок.
Если протоколов несколько, они перечисляются через запятую:
import Foundation
protocol Named { var name: String { get } }
protocol Priced { var price: Int { get } }
struct Product: Named, Priced {
let name: String
let price: Int
}
Ошибки конформанса в Swift обычно звучат неприятно, но по сути это бесплатный чек‑лист. Когда тип «почти соответствует», компилятор прямо пишет, чего не хватает. Это редкий случай, когда ругань — конструктивная: «Не выполнено такое-то требование», «Сигнатура не совпадает», «Нельзя удовлетворить { get set } через let».
Воспринимайте это как ситуацию, где вы собираете шкаф, а инструкция вдруг ожила и говорит: «Вы забыли винт №7». Да, обидно. Да, полезно.
Небольшая схема того, как это происходит:
flowchart TD
A["Вы объявили: struct X: P"] --> B["Компилятор читает требования P"]
B --> C{"Все требования реализованы?"}
C -- "Да" --> D["✅ Conformance принят Код компилируется"]
C -- "Нет" --> E["❌ Ошибка компиляции Список недостающих требований"]
3. Требования к свойствам: { get } и { get set } на практике
Свойства в протоколе кажутся простыми, пока не сталкиваешься с реальностью: stored properties, computed properties, let vs var. Здесь Swift довольно логичен: протокол описывает наблюдаемое поведение (можно ли читать/писать), а не способ хранения.
Если протокол требует { get }, вы можете удовлетворить его чем угодно, что умеет отдавать значение: let-свойством, var-свойством, computed property — неважно. Если требуется { get set }, тогда тип обязан позволять запись. И вот тут let уже не пройдёт (потому что «поставить новое значение» физически нельзя).
{ get } можно выполнить через let
import Foundation
protocol Titled {
var title: String { get }
}
struct Book: Titled {
let title: String
}
print(Book(title: "Dune").title) // Dune
let title подходит, потому что чтение есть.
{ get set } нельзя выполнить через let
import Foundation
protocol Renamable {
var title: String { get set }
}
struct Book: Renamable {
var title: String
}
Обратите внимание: мы поменяли let на var. Протокол буквально требует: «дай возможность менять».
{ get } можно выполнить computed property
Иногда вы не храните значение напрямую, а вычисляете его из других данных. Протоколу всё равно — если чтение работает, контракт выполнен.
import Foundation
protocol HasSummary {
var summary: String { get }
}
struct Book: HasSummary {
let title: String
let year: Int
var summary: String { "\(title) (\(year))" }
}
print(Book(title: "Dune", year: 1965).summary) // Dune (1965)
4. Требования к методам: сигнатура должна совпасть
Методы в протоколах — самая частая причина «почему оно не соответствует, я же написал метод!». Потому что метод — это не только имя. Это ещё и labels у параметров, их типы, возвращаемый тип, throws/async (мы сегодня не углубляемся), и иногда mutating.
Swift относится к сигнатуре как к отпечатку пальца. Если вы поменяли label, это уже другой метод. Поэтому rename(to:) и rename(_:) — разные требования, и компилятор не будет «догадываться», что вы хотели сказать одно и то же.
Labels — часть контракта
import Foundation
protocol Movable {
func move(to x: Int)
}
struct Cursor: Movable {
func move(to x: Int) {
print("Cursor moved to \(x)")
}
}
Cursor().move(to: 10) // Cursor moved to 10
Если бы вы написали func move(_: Int) — это не удовлетворило бы move(to:).
mutating в протоколе и реализация в struct
Если протокол говорит: «Этот метод меняет состояние», он часто ставит mutating в требование. Для struct и enum это критично: без mutating вы не сможете менять stored properties.
import Foundation
protocol CounterLike {
var count: Int { get set }
mutating func increment()
}
struct Counter: CounterLike {
var count: Int = 0
mutating func increment() {
count += 1
}
}
Реализация того же требования в class
У class нет ключевого слова mutating в методах. Но класс всё равно может соответствовать протоколу: просто пишем обычный метод.
import Foundation
protocol CounterLike {
var count: Int { get set }
mutating func increment()
}
final class CounterBox: CounterLike {
var count: Int = 0
func increment() {
count += 1
}
}
Почему это работает? Потому что mutating в протоколе означает: «разрешаю value-типам менять себя». Для класса «менять себя» — это обычный сценарий, он ссылочный. Поэтому отдельного mutating не нужно.
5. Требование init(...) в протоколе
Иногда одного API недостаточно: важно, чтобы объект можно было создать определённым способом. Тогда протокол требует инициализатор. Это прям как требование «вход в клуб только по паспорту»: неважно, кто вы, но документ покажите.
С struct и enum всё обычно просто: вы объявляете init с нужной сигнатурой, и компилятор доволен.
struct и протокольный init
import Foundation
protocol IntCreatable {
init(value: Int)
}
struct Shelf: IntCreatable {
let capacity: Int
init(value: Int) {
self.capacity = value
}
}
let s = Shelf(value: 20)
print(s.capacity) // 20
enum тоже может реализовать init
У enum часто удобно “сжать” разные варианты в кейсы, а инициализатором решить, какой кейс выбрать.
import Foundation
protocol IntCreatable {
init(value: Int)
}
enum ReadingStatus: IntCreatable {
case planned
case reading
case finished
init(value: Int) {
switch value {
case 0: self = .planned
case 1: self = .reading
default: self = .finished
}
}
}
class и required init: важная деталь
Для классов есть нюанс: если класс соответствует протоколу с init, Swift часто требует пометить этот инициализатор как required. Смысл простой: «Если кто-то унаследуется от этого класса, он тоже обязан уметь инициализироваться по контракту протокола».
import Foundation
protocol NamedCreatable {
init(name: String)
}
class User: NamedCreatable {
let name: String
required init(name: String) {
self.name = name
}
}
Это не «лишняя бюрократия», а защита: иначе подкласс мог бы случайно потерять требование протокола.
6. enum и конформанс: switch self как “двигатель” реализации
Когда люди впервые видят enum, они часто думают: «Ну это же просто список вариантов». На самом деле enum в Swift — это ещё и отличная машина для реализации поведения: вы можете описать контракт протокола, а внутри enum разруливать логику через switch self.
Это особенно полезно в CLI-приложениях: статусы, команды, режимы, результаты — всё это удобно моделировать enum, а протоколы дают единый интерфейс, чтобы внешний код был простым и читаемым.
import Foundation
protocol Describable {
var text: String { get }
}
enum Command: Describable {
case add
case list
case help
var text: String {
switch self {
case .add: return "add"
case .list: return "list"
case .help: return "help"
}
}
}
print(Command.list.text) // list
7. Пример: протокол LibraryItem
Чтобы примеры не были набором «левых» типов, давайте продолжим идею учебного CLI‑приложения “LibraryCLI”: мы управляем сущностями библиотеки (книги, возможно журналы), печатаем их, показываем списки, ищем.
Начнём с малого: определим протокол для того, что в библиотеке считается «элементом каталога». Нам нужно уметь читать id, title и получать человекочитаемую строку.
import Foundation
protocol LibraryItem {
var id: Int { get }
var title: String { get }
func details() -> String
}
Теперь сделаем struct Book, который соответствует протоколу. Обратите внимание: мы не обязаны хранить details как свойство — протокол требует метод.
import Foundation
struct Book: LibraryItem {
let id: Int
let title: String
let author: String
func details() -> String {
"\(title) — \(author)"
}
}
print(Book(id: 1, title: "Dune", author: "Frank Herbert").details())
// Dune — Frank Herbert
А теперь добавим class Member (читатель/участник), чтобы увидеть, что классы тоже могут играть роль LibraryItem, если нам вдруг хочется хранить «кто взял книгу» как элемент списка событий или истории (сценарий условный, но полезный для техники).
import Foundation
final class Member: LibraryItem {
let id: Int
let title: String
init(id: Int, name: String) {
self.id = id
self.title = name
}
func details() -> String {
"Member: \(title)"
}
}
Ключевая мысль: протокол описывает возможности. Он не запрещает, что «книга — struct, читатель — class». Снаружи мы всё равно сможем обращаться к ним через общий контракт.
8. Таблица соответствий: чем обычно выполняют требования протокола
Когда вы только начинаете, помогает иметь перед глазами простую карту: что чем реализуется. Потому что мозг на старте любит путать «протокол требует свойство» и «значит я обязан хранить stored property». Нет, не обязан.
Ниже — практическая шпаргалка (без попытки охватить все тонкости языка):
| Требование протокола | Можно выполнить в |
Можно выполнить в |
Можно выполнить в |
|---|---|---|---|
|
, , |
, , |
(часто через ) |
|
, |
, |
обычно (если есть где «хранить» через associated values/другую модель) |
|
метод с точными labels и типами | то же | то же, часто через |
|
|
просто |
(если меняем ) |
|
с той же сигнатурой |
часто |
выбирает |
9. Типичные ошибки при конформансе к протоколам
Ошибка №1: «Я реализовал метод, но компилятор не считает».
Чаще всего причина — несовпадение сигнатуры: перепутали label (to: vs _:), тип параметра (Int vs Int?), или возвращаемое значение. В Swift это не косметика, а часть контракта. Хорошая привычка — копировать сигнатуру требования из протокола и уже потом заполнять тело.
Ошибка №2: попытка выполнить { get set } через let.
Это очень распространённая ловушка у новичков: хочется неизменяемости, и вы ставите let, а протокол при этом требует возможность записи. Здесь нет компромисса: либо протокол действительно должен разрешать изменение (тогда делаем var), либо протокол слишком «жирный» и ему достаточно { get }.
Ошибка №3: забыли mutating в реализации struct/enum.
Если протокол требует mutating func, то struct обязан реализовать метод как mutating, иначе он не сможет менять свои поля, а компилятор не примет соответствие. Это не придирка — это защита value‑семантики. В классе, наоборот, mutating писать не нужно и нельзя, поэтому не пытайтесь «унифицировать» код механически.
Ошибка №4: игнорирование required init в классах.
Когда протокол требует инициализатор, класс часто должен пометить его как required. Новички воспринимают это как «ещё одно странное слово», но смысл простой: любой подкласс тоже обязан соблюдать контракт. Если забыть required, вы либо получите ошибку, либо позже упрётесь в проблему при наследовании.
Ошибка №5: ожидание, что протокол заставляет «хранить данные».
Протокол не говорит «храни поле name». Он говорит: «дай мне возможность прочитать name». Поэтому computed property — честный способ выполнить контракт. Это особенно полезно, когда данные приходят из нескольких полей, либо когда вы строите представление вроде summary, не дублируя хранение.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ