JavaRush /Курсы /Swift SELF /Конформанс struct / class / enum к протоколу

Конформанс struct / class / enum к протоколу

Swift SELF
40 уровень , 1 лекция
Открыта

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». Нет, не обязан.

Ниже — практическая шпаргалка (без попытки охватить все тонкости языка):

Требование протокола Можно выполнить в
struct
Можно выполнить в
class
Можно выполнить в
enum
var x: T { get }
let x
,
var x
,
var x: T { ... }
let x
,
var x
,
var x: T { ... }
var x: T { ... }
(часто через
switch self
)
var x: T { get set }
var x
,
var x: T { get set }
var x
,
var x: T { get set }
обычно
var x: T { get set }
(если есть где «хранить» через associated values/другую модель)
func f(a:) -> R
метод с точными labels и типами то же то же, часто через
switch self
mutating func f()
mutating func f()
просто
func f()
mutating func f()
(если меняем
self
)
init(...)
init
с той же сигнатурой
часто
required init
init
выбирает
case

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, не дублируя хранение.

1
Задача
Swift SELF, 40 уровень, 1 лекция
Недоступна
Карточка города
Карточка города
1
Задача
Swift SELF, 40 уровень, 1 лекция
Недоступна
Переименовать заметку
Переименовать заметку
1
Задача
Swift SELF, 40 уровень, 1 лекция
Недоступна
Пропуск участника
Пропуск участника
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ