$ sudo teach IT

Представьте, что вы работаете с типом 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 встречается дважды.