Розширення
Розширення додають нову функціональність до існуючого класу, структури, перечислення чи протоколу. Це включає можливість розширити тип, до вихідного коду якого у вас немає доступу (це називають ретроактивним моделюванням). Розширення є подібними до категорій в Objective-C, однак на відміну від категорій в Objective-C, розширення у Swift не носять назв.
Розширення у Swift можуть:
- Додавати властивості екземпляру або типу, що обчислюються
- Визначати методи екземпляру або типу
- Створювати нові ініціалізатори
- Визначати індекси
- Визначати та використовувати нові вкладені типи
- Підпорядковувати розширений тип до протоколу
У Swift, можна навіть розширити протокол, реалізувавши його вимоги чи додавши додаткову функціональність, якою можуть користуватись підпорядковані до цього протоколу типи. Детальніше з цим можна ознайомитись у підрозділі Розширення протоколів.
Примітка
Розширення можуть додавати нову функціональність до типу, але вони не можуть заміщувати існуючу функціональність.
Синтаксис розширень
Розширення оголошуються за допомогою ключового слова extension
:
extension SomeType {
// тут йде функціональність, що додається до типу SomeType
}
Розширення може розширити існуючий тип, підпорядковуючи його до одного або кількох протоколів. В таких випадках, назви протоколів записуються точно так само, як і для класу або структури:
extension SomeType: SomeProtocol, AnotherProtocol {
// тут йде реаліазація вимог протоколів
}
Підпорядкування протоколу через розширення детально описано в підрозділі Підпорядкування протоколу за допомогою розширення.
Примітка
Якщо оголосити розширення та додати нову функціональність до існуючого типу, нова функціональність буде доступною для всіх існуючих екземплярів цього типу, навіть якщо їх було створено до оголошення розширення.
Властивості, що обчислюються
Розширення можуть додавати до існуючих типів властивості екземпляру, що обчислюються, та властивості типу, що обчислюються. У наступному прикладі, до вбудованого у Swift’s типу Double
додаються п’ять властивостей, що обчислюються, що реалізовують базову підтримку роботи з одиницями довжини:
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("Один дюйм - це \(oneInch) метрів")
// Надрукує "Один дюйм - це 0.0254 метрів"
let threeFeet = 3.ft
print("Три фути - це \(threeFeet) метрів")
// Надрукує "Три фути - це 0.914399970739201 метрів"
Ці властивості, що обчислюються, вважають значення Double
виміром довжини в певних одиницях. Хоч вони й реалізовані як властивості, що обчислюються, назви цих властивостей можна додати до літерала числа з плаваючою комою через синтаксис крапки, і таким способом використовувати літерали для конвертації довжини.
У цьому прикладі вважається, що значення 1.0
типу Double
виражає “один метр”. Тому властивість m
повертає self
— вираз 1.m
повинен обчислити значення 1.0
.
Інші одиниці довжини потрібно конвертувати для вираження їх в метрах. Один кілометр – це 1000 метрів, і тому властивість km
множить значення на 1_000.00
для вираження його в метрах. Аналогічно, в одному метрі 3.28084 футів, і тому властивість ft
ділить значення Double
на 3.28084
для перетворення футів у метри.
Ці властивості є властивостями тільки для читання, і тому для лаконічності їх можна записувати без ключового слова get
. Значення, що повертають ці властивості, мають тип Double
, і тому їх можна використовувати в математичних розрахунках, усюди, де приймається тип Double
:
let aMarathon = 42.km + 195.m
print("Марафон має довжину \(aMarathon) метрів")
// Надрукує "Марафон має довжину 42195.0 метрів"
Примітка
Розширення можуть додавати нові властивості, що обчислюються, але вони не можуть додавати властивостей, що зберігаються, або додавати спостерігачі за існуючими властивостями.
Ініціалізатори
Розширення можуть додавати нові ініціалізатори до існуючих типів. Це дозволяє, наприклад, розширити інші типи для того, щоб вони приймали ваші власні типи як параметри ініціалізації, або додати додаткових можливостей ініціалізації, котрі не буди включені в оригінальну реалізацію типу.
Розширення можуть додавати нові ініціалізатори класу для зручності, але вони не можуть додавати нових призначених ініціалізаторів чи деініціалізаторів класу. Призначені ініціалізатори та деініціалізатори завжди повинні визначатись оригінальною реалізацією класу.
Примітка
Якщо використовувати розширення для додавання ініціалізатору до типу-значення, що має значення за замовчанням для усіх своїх властивостей, що зберігаються, і не визначає ніяких ініціалізаторів явно, тоді всередині ініціалізатора в розширенні можна викликати ініціалізтор за замовчанням та почленний ініціалізатор.
Це не працювало б, якщо записати ініціалізатор всередині оригінальної реалізації типу-значення, як описано в підрозділі Делегування ініціалізації у типах-значеннях.
У прикладі нижче визначено структуру 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()
}
Оскільки структура Rect
задає значення за замовчанням усім своїм властивостям, вона автоматично отримує ініціалізатор за замовчанням та почленний ініціалізатор, як описано в підрозділі Ініціалізатори за замовчанням. Ці ініціалізатори можна використовувати для створення нових екземплярів структури Rect
:
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
Можна розширити структуру 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)
}
}
Цей новий ініціалізатор починається з обчислення початкової точки (origin
) по наданих центральній точці (center
) та розміру (size
). Після цього ініціалізатор викликає автоматично створений почленний ініціалізатор init(origin:size:)
, котрий зберігає нові значення 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)
Примітка
При додаванні нового ініціалізатора через розширення, все ще потрібно дбати про те, щоб екземпляр був повністю проініціалізованим на момент завершення виконання ініціалізатора.
Методи
Розширення можуть додавати нові методи екземпляру та методи типу до існуючих типів. У наступному прикладі до вбудованого типу Int
додається новий метод екземпляру на ім’я repetitions
:
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
Метод repetitions(task:)
приймає єдиний аргумент типу () -> Void
, котрий є функцією, що не має параметрів та не повертає значення.
Після оголошення цього розширення, можна викликати метод repetitions(task:)
на будь-якому цілому числі для виконання якоїсь задачі відповідну кількість разів:
3.repetitions {
print("Hello!")
}
// Hello!
// Hello!
// Hello!
Мутуючі методи екземплярів
Методи екземпляру, що додаються за допомогою розширення, можуть змінювати, (або мутувати) сам екземпляр. Методи структур та перечислень, що змінюють self
або її властивості, повинні бути поміченими ключовим словом mutating
, так само як і мутуючі методи в оригінальній реалізації типів-значень.
У наступному прикладі до вбудованого у Swift’s типу Int
додається новий метод на ім’я square
, котрий підносить число до квадрата:
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt тепер дорівнює 9
Індекси
Розширення може додавати нові індекси до існуючого типу. У прикладі нижче до вбудованого у Swift’s типу Int
додаються цілочисельний індекс. Цей індекс [n]
повертає цифру на n
-тій позицій справа в десятковому записі числа:
123456789[0] повертає 9
123456789[1] повертає 8
…і так далі:
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
Якщо значення Int
не має достатньо цифр по запитаному індексу, реалізація індексу повертає 0
, так, ніби до числа були дописані незначущі нулі зліва:
746381295[9]
// повертає 0, так ніби число записувалось, як:
0746381295[9]
Вкладені типи
Розширення можуть додавати вкладені типи до існуючих класів, структур та перечислень:
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
. Конкретніше, це перечислення виражає, чи є число додатнім, від’ємним чи нулем.
У цьому прикладі до типу Int
також додається властивість, що обчислюється, на ім’я kind
, котра повертає відповідний елемент перечислення Kind
для цього цілого.
Вкладене перечислення тепер можна використовувати з будь-яким значенням типу 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(_:)
, приймає на вхід масив цілочисельних значень, та ітерує їх. Для кожного значення в масиві, функція розглядає властивість kind
цього числа, та друкує відповідний опис.
Примітка
Оскільки відомо, що
number.kind
має типInt.Kind
, у випадках інструкціїswitch
можна не писатиInt.Kind
для кожного елементу перечислення, і користуватись короткою формою:.negative
замістьInt.Kind.negative
.