JavaRush /Курсы /Swift SELF /protocol: требования к методам и свойствам

protocol: требования к методам и свойствам

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

1. Зачем нужны протоколы, если и так можно написать struct Book

Представьте, что вы пишете приложение, и у вас появляется несколько сущностей: книга, журнал, аудиокнига, пользователь, заказ… Каждая из них живёт своей жизнью, но иногда вам нужно общее поведение: «у этого есть название», «это можно искать по строке», «это можно красиво показать человеку». Протокол как раз и нужен, чтобы описывать что тип обязан уметь, не привязываясь к тому, какой это конкретно тип и как он внутри устроен. В итоге код становится менее хрупким и меньше похож на «комок проводов под столом».

Если очень по-человечески, то protocol — это контракт. Контракт не говорит, как именно вы будете выполнять работу, он говорит, какая работа должна быть выполнена. Тип, который «подписал контракт», обязан предоставить указанный API.

Схематично это можно представить так:

flowchart LR
    A[Код, который хочет возможность] --> B[protocol: контракт]
    C[Конкретный тип: struct/class/enum] -->|выполняет требования| B
    A -->|работает только через API протокола| C

В этой лекции мы будем учиться писать и читать контракт, то есть формулировать требования к свойствам и методам. Реализацию (соответствие протоколу) подробно разберём следующей лекцией — иначе у нас получится «сериал, где сначала финал, потом завязка».

2. Синтаксис protocol: что именно можно требовать

Когда вы впервые видите protocol, он выглядит подозрительно просто — и это хорошая новость. Внутри протокола мы обычно перечисляем требования: какие свойства должны быть у типа и какие методы он должен предоставлять. Важно привыкнуть к мысли, что протокол — это не «кусок готового кода», а скорее список обязанностей, как чек-лист перед полётом: «шасси есть? топливо есть? крылья на месте?».

Минимальный протокол может выглядеть так:


import Foundation

protocol Named {
    var name: String { get }
}

Здесь нет реализации, нет тела свойства, нет значения по умолчанию. Протокол буквально говорит: «Тип обязан иметь name, который можно прочитать, и это String».

А вот протокол с методом:

import Foundation

protocol Resettable {
    func reset()
}

Опять же: тела метода нет. Есть только сигнатура, то есть форма того, что должен уметь тип.

Ключевая привычка дня: протокол — это описание API, а не хранения данных. Если вы видите var name: String { get }, это не означает «обязательно хранить name в памяти». Это означает: «я могу попросить у объекта name, и он мне его отдаст». Хоть из stored property, хоть вычисляя на лету.

3. Требования к свойствам: { get } и { get set } — два разных договора

Свойства в протоколе описываются через var и аксессоры в фигурных скобках. Именно здесь новички чаще всего «спотыкаются», потому что мозг пытается прочитать протокол как обычный struct, а это не struct. Мы не задаём значение — мы задаём возможность: можно ли читать, можно ли менять.

Свойство только для чтения: { get }

import Foundation

protocol Titled {
    var title: String { get }
}

Такой контракт говорит: «у типа есть title, его можно прочитать». При этом тип не обязан разрешать менять title извне.

Это удобно, когда вы хотите гарантировать, что значение доступно, но не хотите (или не можете) обещать запись. Например, заголовок может быть вычисляемым, может зависеть от других данных, или может быть намеренно неизменяемым после создания.

Свойство для чтения и записи: { get set }

import Foundation

protocol EditableTitle {
    var title: String { get set }
}

Это уже другой контракт: «у типа есть title, и я имею право его менять». Если вы потребовали { get set }, то ваш протокол сразу становится более «обязывающим»: не любой тип сможет (и должен) это поддерживать.

Чтобы не держать всё в голове, полезна маленькая таблица-памятка:

Запись в протоколе Что обещает тип Когда это уместно
var x: T { get }
значение можно прочитать данные «только для чтения», derived/computed значения, безопасный API
var x: T { get set }
значение можно читать и менять настройки, редактируемые поля, состояние, которое разрешено менять извне

И важный нюанс (его часто не проговаривают, а потом люди страдают): даже если в протоколе написано var, это не означает, что в реализации это обязательно будет var stored. Протокол требует доступ, а не способ хранения.

4. Требования к методам: сигнатура и labels — часть контракта

Методы в протоколе — это, по сути, «формат звонка». Если у вас в контракте прописано «позвонить можно по такому номеру», но вы поменяли одну цифру — никто не дозвонится. В Swift роль «цифр номера» часто играют argument labels (внешние имена параметров). И да, они являются частью контракта: если перепутать label — это уже другой метод.

Метод без параметров

import Foundation

protocol PrintableSummary {
    func summary() -> String
}

Контракт: «тип обязан уметь вернуть краткое описание строкой».

Метод с параметрами: labels — часть требований

import Foundation

protocol Renamable {
    mutating func rename(to newName: String)
}

Здесь сразу два момента:

Во‑первых, метод называется rename, но его контракт включает label to. Это значит, что ожидаемый вызов выглядит как rename(to: "…"), а не как rename("…").

Во‑вторых, мы видим mutating. Пока просто отметим: это тоже часть контракта.

Чтобы почувствовать разницу labels, можно сравнить два разных протокола:

import Foundation

protocol RenameToStyle {
    mutating func rename(to newName: String)
}

protocol RenameNoLabelStyle {
    mutating func rename(_ newName: String)
}

Это два разных контракта. И это не придирка языка. В Swift labels — часть читаемости вызовов, поэтому они включены в сигнатуру и в требования протокола.

Возвращаемое значение — тоже часть контракта

import Foundation

protocol Searchable {
    func matches(query: String) -> Bool
}

Здесь контракт говорит: «тип умеет проверить, подходит ли он под строку поиска». Возвращаемый Bool — не опциональная деталь, а важная часть требований.

5. mutating в требованиях протокола: зачем он нужен

На этом месте обычно звучит вопрос: «Почему протоколу не всё равно? Пусть struct сам решает, можно ли менять себя». А вот и нет. Протокол описывает контракт, и иногда по смыслу метод обязан менять состояние. Например, «увеличить счётчик», «добавить тег», «пометить как прочитанное». У struct изменение своих свойств требует mutating, и протокол должен это заранее учитывать — иначе value-типы окажутся «заперты» и не смогут выполнить контракт корректно.

Пример протокола «счётчик»:

import Foundation

protocol CounterLike {
    var count: Int { get }
    mutating func increment()
}

Читаем это как договор: «у типа есть count, его можно прочитать, и есть метод increment(), который увеличивает счётчик и поэтому меняет состояние».

А вот пример, ближе к реальной жизни нашего будущего CLI-приложения (условно назовём его LibraryCLI): отметка сущности как «выбранной» или «архивированной».

import Foundation

protocol Archivable {
    var isArchived: Bool { get }
    mutating func archive()
}

Обратите внимание: isArchived — только чтение. Это часто хороший стиль: «снаружи можно узнать состояние, но менять его можно только через понятный метод (archive)». Тогда правила становятся явными, и вы меньше рискуете получить код в стиле «кто-то где-то поменял флажок, а потом мы два дня ищем кто».

Важно также помнить: mutating в требовании не делает тип автоматически «мутабельным». Это всего лишь разрешение и ожидание, что метод может менять self, если это value-type.

6. Протоколы для учебного CLI: контракт важнее типа

До этого мы смотрели протоколы как отдельные мини-примеры. Теперь сделаем шаг к нашему приложению (CLI-библиотека), но не будем пока писать реализации — только сформулируем контракты. Это похоже на момент, когда вы ещё не построили дом, но уже нарисовали план: где дверь, где окна, где кухня, и почему туалет не должен открываться прямо в холодильник (иногда архитекторы тоже люди).

Контракт «у этого есть идентификатор»

Для CLI-приложения удобно, чтобы сущности имели ID: так их легко выбирать командами вроде show 12, remove 7 и так далее.

import Foundation

protocol HasID {
    var id: Int { get }
}

Мы требуем только чтение: ID обычно не должен «прыгать» туда-сюда после создания.

Контракт «у этого есть заголовок»

import Foundation

protocol HasTitle {
    var title: String { get }
}

Это пригодится и для книг, и для статей, и для заметок. Самое приятное: протокол максимально тонкий — только то, что реально нужно.

Контракт «это можно искать»

import Foundation

protocol SearchableItem {
    func matches(query: String) -> Bool
}

Заметьте, мы специально пишем query: с label, чтобы вызов читался нормально: matches(query: "swift"). В CLI-логике вы потом будете благодарны себе за такие мелочи: код читается как человеческое предложение, а не как заклинание.

Контракт «это можно вывести одной строкой»

Для CLI вам почти всегда нужны «короткие строки» для списков.

import Foundation

protocol ListRowRepresentable {
    func listRow() -> String
}

И вот теперь мы можем составить «идеальный образ сущности библиотеки» как набор контрактов. Но важно: сегодня мы не объединяем их в композицию и не используем any — это отдельные темы следующих лекций. Пока наша цель — научиться грамотно формулировать требования.

7. Как выбирать требования: протокол должен быть маленьким

Когда вы проектируете протокол, есть вечный соблазн написать «универсальный протокол всего»: LibraryThing с двадцатью свойствами, десятью методами и обязательным умением варить кофе. Потом выясняется, что половина типов не может ему соответствовать без странных костылей, а другая половина тащит лишние обязанности «потому что так требует контракт».

Хороший стиль для новичка (и не только): начинать с минимального набора требований. Если вам нужно вывести сущность в списке — требуйте listRow(). Если вам нужно искать — требуйте matches(query:). Если вам нужно и то, и другое — это не повод склеивать всё в одного монстра прямо сейчас, это повод аккуратно подумать, где эти обязанности реально используются.

Здесь помогает простая ментальная проверка. Если вы можете честно сказать: «Любой тип, который будет соответствовать этому протоколу, действительно обязан уметь вот это» — значит, контракт хороший. Если же получается: «Ну… вообще-то не обязан, но мне так удобнее сегодня» — протокол скорее всего раздут.

Иногда полезно нарисовать маленькую «карту обязанностей» (особенно если вы чувствуете, что начинаете усложнять):

flowchart TD
    A[Нужно вывести список] --> B[ListRowRepresentable]
    C[Нужно искать] --> D[SearchableItem]
    E[Нужен ID для команды remove/show] --> F[HasID]
    G[Нужно название] --> H[HasTitle]

Так вы явно видите, что каждое требование имеет «клиента» (код, который этого требует). Протокол без клиента часто превращается в «мы добавили, потому что можно», а это первый шаг к печальному коду.

Пример из стандартной библиотеки: протоколы тоже состоят из требований

Когда кажется, что protocol — это «учебная теория», полезно помнить: стандартная библиотека Swift сама устроена так же. Например, протоколы для чисел описывают набор требуемых операций и свойств; это такой же контракт, только значительно более серьёзный и продуманный. В обсуждениях Swift Evolution протоколы вроде FloatingPoint тоже рассматриваются именно как набор требований к операциям и свойствам.

Нам не нужно сейчас углубляться в математику и внутренности стандартной библиотеки. Здесь важна идея: протоколы — это не «декорация». Это один из главных способов описывать возможности типов в Swift, начиная от ваших Book и заканчивая фундаментальными вещами уровня чисел.

8. Типичные ошибки при формулировании требований к методам и свойствам

Ошибка №1: требовать { get set } «на всякий случай», а потом удивляться, что типы не подходят.
Очень частая история: вы пишете var title: String { get set }, потому что «вдруг пригодится редактирование», а потом хотите, чтобы тип с неизменяемым заголовком тоже соответствовал протоколу — и начинается борьба. Если запись не нужна прямо сейчас (по логике вашего кода), начинайте с { get }. Протокол — это публичное обещание, а обещания лучше не раздавать щедрее, чем вы готовы выполнять.

Ошибка №2: забывать, что labels — часть контракта.
Новичку легко кажется, что rename(to:) и rename(_:) — «одно и то же, ну подумаешь двоеточие». Для Swift это разные сигнатуры, а для протокола — разные требования. Если ваш контракт требует matches(query:), то реализация должна совпасть по форме вызова. Это не занудство: labels делают код читаемым и предсказуемым.

Ошибка №3: ожидать, что протокол заставляет «хранить поле», а не «уметь отдавать значение».
var id: Int { get } не говорит «внутри должен лежать stored property id». Он говорит «снаружи я могу спросить id». Это может быть вычисляемое свойство, значение из другого поля, константа, что угодно. Если помнить эту разницу, вы перестаёте «воевать» с протоколами и начинаете использовать их как инструмент дизайна.

Ошибка №4: не ставить mutating в требовании, когда метод по смыслу меняет состояние.
Если протокол описывает поведение «изменить себя» (увеличить счётчик, архивировать, добавить элемент), то для value-типов (struct, enum) без mutating контракт становится невыполнимым или вынуждает к странным обходным путям. Лучше один раз честно признать: да, этот метод меняет состояние — значит, в требовании должно быть mutating.

Ошибка №5: делать один «толстый протокол» вместо нескольких маленьких.
Когда протокол превращается в «обязательный набор всего», вы теряете гибкость: типы вынуждены реализовывать лишнее, а код становится сложнее сопровождать. Гораздо спокойнее иметь несколько маленьких контрактов и требовать их там, где они реально нужны. Это снижает связанность и делает дизайн более прозрачным — особенно в CLI-приложениях, где логика команд часто требует разных «умений» от сущностей.

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