Вам нужно сохранить конкретный тип результата метода клонирования при наследовании. Какую гарантию даёт Self в возвращаемом типе протокольного метода?
protocol Clonable {
func clone() -> Self
}
class Report: Clonable {
let title: String
required init(title: String) {
self.title = title
}
func clone() -> Self {
Self(title: title)
}
}
class AuditReport: Report {}
let copy = AuditReport(title: "A").clone()
print(type(of: copy))
Self означает конкретный тип экземпляра, на котором вызван метод, а не просто тип, объявивший метод. Поэтому AuditReport(...).clone() возвращает AuditReport, сохраняя точный статический тип результата без приведения к Report.
Обычный возвращаемый тип вроде Report описывает только базовый класс и при наследовании теряет информацию о более конкретном типе. Self нужен для API, где результат должен оставаться того же конкретного типа, что и исходный экземпляр.
Такой подход особенно полезен для протоколов клонирования, фабричных методов и fluent-интерфейсов. Он позволяет выразить связь между типом получателя метода и типом результата непосредственно в контракте.
Если заменить Self на Report, вызов через AuditReport будет иметь результат типа Report. Сам объект может фактически оказаться AuditReport, но статический тип переменной уже не позволит безопасно использовать специфичные свойства и методы наследника без приведения.
При использовании Self реализация обязана возвращать экземпляр того же конкретного типа, для которого выполняется вызов. Нельзя вернуть произвольный экземпляр другого подкласса или всегда создавать только базовый Report.
В протоколе func clone() -> Self означает: каждый конкретный тип, соответствующий протоколу, возвращает собственный тип. Для AuditReport результат вызова имеет тип AuditReport, потому что получателем является экземпляр этого класса.
В классе выражение Self(title: title) создаёт экземпляр динамического конкретного типа. Поэтому при вызове у наследника будет создан AuditReport, а не безусловно Report.
Инициализатор title объявлен как required, поскольку реализация должна иметь возможность создать неизвестный заранее подкласс через Self(...). Наследник обязан унаследовать или предоставить совместимую реализацию такого инициализатора.
Self отличается от имени конкретного класса. Если написать Report(title: title), метод всегда создавал бы базовый тип и нарушал бы ожидаемую семантику клонирования подклассов. Если указать -> Report, тип результата также будет обобщён до базового класса.
У протокола с требованием, использующим Self, есть ограничение при работе через existential-тип any Clonable: конкретный тип результата заранее неизвестен. Поэтому такой метод нельзя использовать так, будто он возвращает обычный заранее известный тип; для сохранения типа обычно применяют обобщённую функцию или type erasure.
В библиотеке отчётов базовый класс предоставляет clone(). Вариант с -> Report проще: он не требует required-инициализатора и подходит, если потребителю достаточно базового интерфейса. Недостаток — теряется статический тип подкласса, а специфичные данные приходится восстанавливать приведением.
Вариант с -> Self сохраняет тип наследника и уменьшает количество приведений. Компромисс — более строгий контракт: конструктор должен поддерживать создание через Self, а работа с объектами как с any Clonable становится менее удобной.
Для API, где клонирование должно сохранять расширенный тип отчёта, выбран Self. В результате код, работающий с AuditReport, получает AuditReport, а не Report, и компилятор проверяет это соответствие на этапе компиляции.
Минимальная иллюстрация различия:
Почему в примере нужен required init?
Реализация вызывает Self(title: title), но Self может обозначать подкласс, неизвестный в объявлении Report. Swift должен гарантировать, что у любого такого подкласса существует совместимый инициализатор. Поэтому инициализатор базового класса помечается required, а наследник обязан его унаследовать или реализовать.
Что изменится, если написать func clone() -> Report?
Контракт будет разрешать возвращать любой экземпляр Report, включая подкласс, но статический тип результата всегда станет Report. Например, результат AuditReport(...).clone() нельзя будет использовать как AuditReport без явного приведения. Это слабее гарантии Self: связь между типом получателя и типом результата исчезает.
Почему вызов clone() через any Clonable проблематичен?
Для значения any Clonable конкретный тип скрыт. Компилятор не знает, какой именно тип должен обозначать Self в результате: Report, AuditReport или другой соответствующий тип. Поэтому код, которому нужен точный результат Self, следует строить через обобщённый параметр, например func duplicate<T: Clonable>(_ value: T) -> T, либо явно проектировать type-erased обёртку.