Представьте, что вы работаете с типом Int. Он встроен в язык, вы не можете открыть его исходный код и дописать туда метод, который вам нужен именно сейчас: скажем, проверку, что число делится на три без остатка. То же самое со своими структурами: иногда хочется добавить к готовому типу пару удобных методов, не трогая его основное объявление — например, вынести форматирование или проверки в отдельное место, чтобы не раздувать один большой struct. Для обеих задач в Swift есть один инструмент — extension, расширение.
Что такое extension
Расширение — это отдельный блок кода, который добавляет новые возможности к уже существующему типу: своей структуре, классу, перечислению или даже к стандартному типу вроде Int, String или Array. Синтаксис простой — то же имя типа после ключевого слова extension, а дальше тело с методами и вычисляемыми свойствами:
struct Point {
var x: Int
var y: Int
}
extension Point {
var isOrigin: Bool {
return x == 0 && y == 0
}
}
let p = Point(x: 0, y: 0)
print(p.isOrigin)
Здесь у Point изначально было только два хранимых свойства. Расширение добавило вычисляемое свойство isOrigin, как будто оно всегда там и было — снаружи никакой разницы не видно, вызывается точно так же, через точку.
Расширяем свой тип
Самая частая причина расширять собственный тип — разложить код по смыслу. Например, у структуры может быть блок со свойствами и инициализацией в одном месте, а методы форматирования или сравнения — в отдельном extension рядом, а то и в отдельном файле. Компилятору всё равно, он соберёт все части в один тип, а вам читать становится проще.
struct Temperature {
var celsius: Double
}
extension Temperature {
var fahrenheit: Double {
return celsius * 9 / 5 + 32
}
func warmerThan(_ other: Temperature) -> Bool {
return celsius > other.celsius
}
}
Стало доступно Temperature(celsius: 20).fahrenheit и t1.warmerThan(t2) — оба добавлены расширением, но выглядят как обычные члены типа.
mutating не пропадает
Если метод расширения структуры должен менять её собственные свойства, перед ним по-прежнему нужен mutating — расширение не отменяет правило про изменение self у структур:
extension Temperature {
mutating func heat(by degrees: Double) {
celsius += degrees
}
}
Расширяем стандартный тип
Ровно тот же синтаксис работает и для типов из стандартной библиотеки. Вы не видите и не можете изменить исходники Int, но можете дописать к нему свои методы — они появятся у каждого значения этого типа в вашем проекте:
extension Int {
var isEven: Bool {
return self % 2 == 0
}
func clamped(to range: ClosedRange<Int>) -> Int {
if self < range.lowerBound { return range.lowerBound }
if self > range.upperBound { return range.upperBound }
return self
}
}
print(7.isEven)
print(120.clamped(to: 0...100))
Внутри тела расширения self — это то самое число, к которому применили метод, точно так же, как self внутри обычного метода структуры или класса.
Расширение и соответствие протоколу
Расширение можно использовать и для того, чтобы тип начал соответствовать протоколу — не обязательно писать это в самом объявлении структуры или класса. Такой приём удобен, когда протокол описывает отдельную грань поведения типа и логично держать её в своём блоке:
protocol Summable {
func summary() -> String
}
struct Order {
var item: String
var quantity: Int
}
extension Order: Summable {
func summary() -> String {
return "\(quantity) x \(item)"
}
}
Order объявлен без единого упоминания протокола, а соответствие Summable добавлено отдельным расширением. Компилятор проверит, что в расширении реализованы все требования протокола, ровно так же, как если бы это было написано прямо в struct Order: Summable { ... }.
Что extension не может делать
У расширений есть чёткая граница возможностей:
- Нельзя добавить хранимое свойство (
var name: Stringбез вычисления) — только вычисляемые свойства черезget/set. - Нельзя переопределить уже существующий метод или свойство типа — расширение только добавляет новое, а не заменяет старое.
- Нельзя добавить
designated-инициализатор классу — только выпускающий (convenience) или инициализаторы для структур без своих init.
Причина простая: расширение не хранит собственное состояние типа, оно лишь добавляет поведение поверх уже существующей памяти объекта.
Частые ошибки
- Попытка добавить хранимое свойство в extension.
extension Int { var counter = 0 }не скомпилируется — компилятор не знает, где физически хранить это значение у каждогоInt. Нужно либо сделать свойство вычисляемым, либо хранить состояние в отдельном типе. - Забытый mutating. Метод расширения структуры, который меняет
selfили его свойства, безmutatingпросто не соберётся — та же логика, что и для обычных методов. - Расширение вместо основного объявления. Не стоит выносить в extension вообще все свойства и методы типа ради моды — если расширений становится больше, чем смысла в разделении, читать код становится сложнее, а не проще. Разделяйте по смысловым блокам.
- Путаница с областью видимости. Расширение видит только то, что доступно снаружи типа (публичные и внутренние члены модуля), если только оно не находится в том же файле, что и объявление типа — приватные свойства из другого файла из расширения не достать.
Итоги
- extension добавляет новые вычисляемые свойства и методы к уже существующему типу — своему или стандартному.
- Расширения помогают разложить большой тип на смысловые блоки и добавлять поведение к типам, исходники которых недоступны.
- Через extension тип может получить соответствие протоколу, не меняя исходное объявление.
- Расширение не может добавить хранимое свойство, переопределить существующий член или добавить designated-инициализатор классу.
- Мutating для методов структуры внутри extension работает по тем же правилам, что и в обычном объявлении.
Проверьте себя
3 вопроса
Прямоугольник через extension
Дана структура Rectangle с хранимыми свойствами width и height типа Double. Допишите её расширение так, чтобы оно добавляло:
- вычисляемое свойство
area— площадь прямоугольника (width * height); - метод
scaled(by:), который возвращает новыйRectangleс обеими сторонами, умноженными на переданный коэффициент.
Саму структуру Rectangle менять не нужно — работайте только внутри extension.
Расширяем Int
Допишите расширение стандартного типа Int двумя методами:
digitSum()— возвращает сумму цифр числа. Для отрицательных чисел считайте цифры по модулю числа (знак минус не входит в сумму).hasRepeatingDigit()— возвращаетtrue, если среди цифр числа есть хотя бы одна повторяющаяся, иfalse, если все цифры разные. Для отрицательных чисел тоже используйте цифры по модулю.
Например, 123.digitSum() равно 6, а 121.hasRepeatingDigit() равно true, потому что цифра 1 встречается дважды.