Основы Swift / 16.3. Расширения


Видео


Расширения
Расширения добавляют новую функциональность существующему классу, структуре, перечислению или типу протокола. Это включает возможность расширять типы, для которых Вы не имеете доступ к их исходному коду (известно как ретроактивное моделирование). Расширения схожи с категориями в Objective-C. (В отличие от категорий Objective-C расширения в Swift не имеют имён.)

Расширения в Swift могут:
  • Добавлять вычисляемые свойства объектов и типов
  • Определять новые методы объектов и методы типов
  • Предоставлять новые инициализаторы
  • Определять сабскрипты
  • Определять и использовать новые вложенные типы
  • Сделать существующий тип соответствующим протоколу
В Swift Вы можете даже расширить протокол для предоставления реализации его требований или предоставления дополнительной функциональности для типов, которые ему удовлетворяют.

Расширения могут добавить новую функциональность типу, но они не могут перезаписать имеющуюся функциональность.
Синтаксис расширений
Объявите расширения с помощью ключевого слова extension:
@{16.3\1}
class SomeType {}

extension SomeType {
    // новая функциональность для типа SomeType будет объявлена здесь
}
Если Вы объявляете расширение для добавления новой функциональности существующему типу, то новая функциональность будет доступна на всех существующих объектах данного типа, даже если они созданы до объявления расширения.
Вычисляемые свойства
Расширения могут добавить вычисляемые свойства объектов и типа для существующих типов. Этот пример добавляет пять вычисляемых свойств объекта для встроенного типа Double для предоставления базовой поддержки для работы с единицами дистанции:
@{16.3\2\1}
extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// Выведет "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Выведет "Three feet is 0.914399970739201 meters"
Эти вычисляемые свойства выражают то, что значение Double следует рассматриваться в качестве единицы длины. И хотя они реализованы как вычисляемые свойства, имена этих свойств могут быть добавлены к литералу чисел с плавающей точкой через точечный синтаксис, что позволить использовать такие литералы для выполнения преобразования дистанции.

В этом примере Double значением 1.0 представляет "один метр". Потому вычисляемое свойство m возвращает self: выражение 1.m будет вычислено в значение Double, равно 1.0.

Другие единицы требуют больших преобразований для выражения измерений в метрах. Один километр равен 1,000 метрам, так что вычисляемое свойство km умножает значение на 1_000.00 для преобразования в число, выраженное в метрах. Аналогично, в метре содержится 3.28084 футов, так что вычисляемое свойство fr делит содержащееся значение Double на 3.28084 для преобразования из футов в метры.
@{16.3\2\2}
Эти вычисляемые свойства доступны только для чтения, так что они выражаются без ключевого слова get для краткости. Их возвращаемой значение имеет тип Double и может быть использовано в рамках математических вычислений, в которых принимается значение Double:
let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// Выведет "A marathon is 42195.0 meters long"
Расширения могут добавлять новые вычисляемые свойства, но они не могут добавлять новых хранимые свойства или наблюдатели свойств для существующих свойств.
Инициализаторы
Расширения могут добавлять новые инициализаторы для существующих типов. Это позволяет Вам расширять другие типы для принятия Ваших собственных типов в качестве параметров конструкторов или предоставления дополнительных опций инициализации, которая не была включена в оригинальную реализацию типа.

Расширения могут добавить новые вспомогательные конструкторы классу, но они не могут добавить новые основные инициализаторы или деструкторы классу. Основные инициализаторы и деинициализатор должны всегда предоставляться оригинальной реализацией класса.

Если Вы используете расширение для добавления инициализатора типу-значению, которое предоставляет дефолтные значения для всех своих хранимых свойств и не определяет никаких специальных конструкторов, Вы можете вызвать инициализатор по-умолчанию и полученный конструктор для этого типа-значения изнутри конструктора в расширении.

Этого не произойдёт, если Вы напишите конструктор в качестве части оригинальной реализации типа значения.
@{16.3\3\1}
Пример ниже определяет пользовательскую структуру Rect для представления геометрической фигуры. Пример также определяет две поддерживающих структуры с названием Size и Point, каждая из которых предоставляет значение по-умолчанию для всех своих свойств:
struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
}
@{16.3\3\2}
Так как структура Rect предоставляет значения по-умолчанию для всех своих свойств, то она получает конструктор по-умолчанию и почленный инициализатор автоматически. Эти инициализаторы могут быть использованы для создания объектов Rect:
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))
@{16.3\3\3}
Вы можете расширить структуру Rect для предоставления дополнительного инициализатора, который принимает конкретную точку центра и размер:
extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}
@{16.3\3\4}
Новый инициализатор начинает своё выполнения с вычисления подходящей точки начала координат, основываясь на предоставленном значении точки center и значении value. Затем инициализатор вызывает автоматический почленный инициализатор init(origin:size:), которые сохраняет соответствующие значения начала координат и длины в свойства:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// начало centerRect теперь равно (2.5, 2.5) и его размеры равны (3.0, 3.0)
Если Вы предоставите новый инициализатор с помощью расширения, то Вы всё ещё будете ответственны за то, чтобы каждый объект был полностью инициализирован по завершению выполнения инициализатора.
Методы
Расширения могут добавлять новые методы экземпляров и типов для существующих типов. Следующий пример добавляет новый метод объекта с названием repetitions типу Int:
@{16.3\4\1}
extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..< self {
            task()
        }
    }
}
Метод repetitions(task:) принимает единственный аргумент типа () -> Void, который указывает на то, что функция не имеет параметров и не может возвращать значения.
@{16.3\4\2}
После определения этого расширения, Вы можете вызвать метод repetitions(task:) на любом целом числе для выполнения задачи заданной число раз:
3.repetitions {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!
Мутирующие методы объектов
Методы объектов, добавляемые в расширении также могут модифицировать (или мутировать) объект. Методы структур и перечислений, которые изменяют self или его свойства должны быть помечены как mutating, как и оригинальные методы мутирующие из основной реализации.
@{16.3\5}
Пример ниже добавляет новый мутирующий метод с названием square типу Int, который возводит в квадрат оригинальное значение:
extension Int {
    mutating func square() {
        self = self * self
    }
}
var someInt = 3
someInt.square()
// someInt теперь равен 9
Сабскрипты
Расширения могут добавлять новые сабскрипты существующему типу. Этот пример добавляет целочисленный сабскрипт встроенному типу Int. Этот сабскрипт [n] возвращает десятичную цифру на расстоянии n знаков от правой стороны числа:
1. 123456789[0] вернёт 9
2. 123456789[1] returns 8

…и так далее:
@{16.3\6\1}
extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..< digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]
// вернёт 5
746381295[1]
// вернёт 9
746381295[2]
// вернёт 2
746381295[8]
// вернёт 7
@{16.3\6\2}
Если значение типа Int не имеет достаточно знаков для запрашиваемого индекса, то реализация сабскрипта вернёт 0, как если бы число было бы дополнено на такое число нулей слева:
746381295[9]
// вернёт 0, как если бы Вы запросили:
0746381295[9]
Вложенные типы
Расширения могут добавлять новые типы для существующих классов, структур и перечислений:
@{16.3\7\1}
extension Int {
    enum Kind {
        case negative, zero, positive
    }
    var kind: Kind {
        switch self {
        case 0:
            return .zero
        case let x where x > 0:
            return .positive
        default:
            return .negative
        }
    }
}
Это пример добавляет новое вложенное перечисление в Int. Это перечисление с названием Kind выражает вид числа, которое представляется отдельным целым числом. Конкретно, он выражает, является ли оно отрицательным, положительным или нулём.

Этот пример также добавляет новое вычисляемое свойство объекта для Int с названием kind, которое возвращает соответствующий кейс перечисления для этого целого числа.
@[16.3\7\2}
Вложенное перечисление теперь может использоваться с любым значением Int:
func printIntegerKinds(_ numbers: [Int]) {
    for number in numbers {
        switch number.kind {
        case .negative:
            print("- ", terminator: "")
        case .zero:
            print("0 ", terminator: "")
        case .positive:
            print("+ ", terminator: "")
        }
    }
    print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// Выведет "+ + - 0 - 0 +"
Эта функция printIntegerKinds(_:) принимает входной массив значение Int и итерируется через эти значения по очереди. Для каждого целого в массиве функция определяет значение вычисляемого свойства kind, а затем выводит соответствующее описание.

number.kind заранее имеет тип Int.Kind. Ввиду этого все значения кейсов Int.Kind могут быть записаны в краткой форме внутри инструкции switch, например как .negative вместо Int.Kind.negative.