obo.dev

Расширения (Extension)

01 Dec 2022

Расширения (Extension)

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

Общая конструкция - синтаксис Расширений (Extension)

Для того, чтоб определить расширение, необходимо его объявить с помощью ключевого слова extension. Затем указать название типа данных, к которому будет применено расширение. Затем, в фигурных скобках указать добавляемый необходимый функционал:

extension DataType {
	// code
}

Здесь указано, что будет добавленно расширение для типа данных DataType.

Для применения расширения, тип данных, к которому назначается расширение, - должен быть определен.

Для примера создан класс Airplane, к которому по ряду причин, нет доступа к его изменению.

class Airplane {
    var altitude: Double = 0
 
    func setAltitude(feet: Double) {
        altitude = feet
    }
}

Разработчик решил измерить высоту самолета в футах. И всю логику работы класса построил на этих единицах измерения. Свойство altitude устанавливается в футах через метод setAltitude(feet: Double). Но, допустим, необходимо задавать ее в метрах.

Нельзя редактировать исходный код и нет необходимости создавать подклассы Airplane, поэтому вместо этого можно создать расширение.

Создание расширения:

extension Airplane {
    func setAltitude(meter: Double) {
        altitude = meter * 3.28084
    }
}

В этом расширении был добавлен новый метод setAltitude(meter:) в класс Airplane. Этот метод имеет то же название, но другие входящие параметры - выполнена перегрузка метода. Но теперь, метод setAltitude(meter:) принимает в качестве аргумента значение в метрах и назначает свойству altitude значение в футах, переводя метры (в качестве входящих параметров) в футы.

Теперь можно использовать метод setAltitude(meter:) в экземпляре класса Airplane как обычный метод:

let boeing = Airplane()
boeing.setAltitude(meter: 12000)
print(boeing.altitude)

// Output
// 39370.08

Когда код компилируется, любые созданные расширения добавляются в соответствующие классы. Исходный класс и его расширения по сути объединяются в одно целое. В результате можно расширить класс с помощью новых функций.

Вот что могут делать расширения в Swift:

  • Добавлять методы и вычисляемые свойства.
  • Добавлять статические константы.
  • Создавать новые инициализаторы.
  • Определять сабскрипты с помощью функции subscript().
  • Определять новые вложенные типы.
  • Добавлять соответствие протоколу.
  • Добавить реализацию по умолчанию с помощью расширений протокола.

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

Продолжая предыдущий пример, нельзя написать:

extension Airplane {
    var speed: Int = 0
}

Добавление нового свойства в класс коренным образом изменило бы структуру этого класса. Существует другой отличный способ изменить структуру класса — использовать подклассы.

Использование расширений

Далее разберем практические варианты использования расширений:

  • Можно разделять и группировать наш код.
  • Добавлять соответствие протоколу.
  • Добавлять статические константы - Свойства типа.
  • Добавлять вложенные типы.
  • Добавлять вычисляемые свойства.
  • Добавлять методы.
  • Добавлять расширения протокола.
  • Добавлять расширения к уже существующим базовым типам данных.

Разделение и группировка кода

Один из способов использования расширений — это разделение и группировка различных частей класса. Для этого используется базовый класс для основного кода, такого как его свойства, а затем добавляются новые функции с помощью расширений.

Создание класса Airplane:

class Airplane {
    var speed: Double = 0
    var altitude: Double = 0
    var bearing: Double = 0
}

Создание различные расширения. Во-первых, для полета на самолете:

extension Airplane {
    func changeAltitude(altitude: Double) {
        self.altitude = altitude
    }
 
    func changeBearing(degrees: Double) {
        self.bearing = degrees
    }
}

Затем определяются функции для взлета и посадки:

extension Airplane {
    func takeOff() {
    	// code
	}

    func land() {
    	// code
	}
}

Каждое расширение может иметь свой собственный файл Swift. Когда организуете класс на основе его функциональности и не помещаете все в один файл, можно лучше контролировать организацию своего кода.

Например, пишем контроллер представления (ViewController). Контроллер представления имеет две основные функции. Можно поместить каждую из этих функций в отдельное расширение и по-прежнему использовать контроллер представления как один законченный класс.

Разделение компонентов на расширения может помочь лучше организовать большую базу кода.

Добавление соответствия протокола с помощью расширений

Можно использовать расширения для соответствия протоколу:

class DetailViewController: UIViewController {

}
 
extension DetailViewController: CLLocationManagerDelegate {

}

В приведенном выше коде с помощью расширения класс DetailViewController теперь соответствует протоколу CLLocationManagerDelegate. Затем можно продолжить добавление кода для этого делегата в расширение, отделяя его от базового класса.

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

Добавление статических констант - Свойства типа

Нельзя добавлять свойства к расширению, но можно добавлять к расширениям статические константы.

extension Notification.Name {
    static let statusUpdated = Notification.Name("status_updated")
}

Приведенный выше код расширяет тип Notification.Name статической константой statusUpdated. Эта константа класса доступна в любом месте типа, что позволяет использовать ее вместе с NotificationCenter:

NotificationCenter.default.post(name: .statusUpdated, object: nil)

Функция post(name:object:) принимает параметр Notification.Name. Здесь определили константу statusUpdated для Notification.Name, поэтому Swift автоматически выводит ее тип, и можно использовать более короткий синтаксис — .statusUpdated.

Добавление вложенных типов

Можно определить подтип в расширении:

extension UserDefaults {
    struct Keys {
        static let useSync  = "use_sync"
        static let lastSync = "last_sync"
    }
}

Теперь можно использовать его следующим образом:

UserDefaults.default.set(true, forKey: UserDefaults.Keys.useSync)

У этого подхода есть два преимущества:

  • Подтип Keys доступен только для типа UserDefaults. Это означает, что не будет произвольной структуры, определенной на глобальном уровне в коде.
  • Использование констант означает, что будет меньше шансов сделать ошибку при вводе текста «use_sync» для ключа UserDefaults. Можно использовать эту концепцию везде, где можно использовать статические ключи.

Добавление вычисляемых свойств

Допустим, что у нас нет доступа к исходному коду класса Circle или мы хотите лучше организовать его функциональность.

Пример:

class Circle {
    var radius: Double = 0
}
 
extension Circle {
    var circumference: Double {
        return radius * .pi * 2
    }
}

В приведенном выше коде сначала объявляется класс Circle, а затем добавляется вычисляемое свойство circumference с помощью расширения. Теперь можно вычислить длину окружности:

let circle = Circle()
circle.radius = 10
print(circle.circumference)

// Output
// 62.83185307179586

В этом примере было использовано обращение к свойству circumference из расширения для получения длины окружности.

Добавление методов

Определим класс Temperature:

class Temperature {
  	var celsius: Double = 0

  	func setTemperature(celsius: Double) {
    	self.celsius = celsius
    	print("Celsius:", celsius)
  	}
}

Добавим расширение для конвертации единиц измерения: из градусов Цельсия в градусы Фаренгейта:

extension Temperature {

  	// add a new method to Temperature class
  	func convert() {
    	var fahrenheit = (celsius * 1.8) + 32
    	print("Fahrenheit:", fahrenheit)
	}
}

И инициализируем экземпляр класса Temperature:

let temp1 = Temperature()
temp1.setTemperature(celsius: 16)

// Output
// Celsius: 16.0

temp1.convert()

// Output
// Fahrenheit: 60.8

Это расширение добавляет следующие функции в Temperature:

  • convert() — метод, который просто преобразует температуру из градусов Цельсия в градусы Фаренгейта.
  • fahrenheit — переменная, объявленная внутри метода convert(), в которой хранится результат преобразования.

Затем создали объект с именем temp1 (экземпляр класса Temperature) и использовали его для доступа к методу, созданному внутри расширения.

Примечание. Свойства, определенные внутри класса (например, по Цельсию), также можно использовать внутри расширения.

Добавление расширения протокола

В отличие от соответствия протоколу, расширения протокола позволяют напрямую предоставлять реализацию протоколов по умолчанию.

Например, простой протокол:

protocol Edible {
    func eat()
}

Протокол Edible определяет функцию eat(), так что любой класс, который хочет соответствовать протоколу Edible, должен реализовать функцию eat():

class Fish: Edible  {
    func eat() {
        print("Я ем рыбу.")
    }
}

С расширениями протокола можно расширить протокол, включив в него реализацию по умолчанию:

extension Edible {
    func eat() {
        print("Я ем.")
    }
}

С помощью этого расширения протокола любой класс, который принимает протокол Edible, может использовать реализацию функции eat() по умолчанию:

class Apple: Edible {

}
 
let apple = Apple()
apple.eat()

// Output
// Я ем.

Добавление расширения к уже существующим базовым типам данных

Swift — многофункциональный язык программирования, но иногда в нем просто того, что вам нужно.

Так как базовые типы данных (такие как Int, Double, Float, String, Character, Bool) - это структуры, то к ним можно тоже написать расширение.

К примеру, до версии Swift 4.2 у массивов не было функции shuffled() для рандомизации. Поэтому можно было добавить эту вспомогательную функцию непосредственно к типу Array:

extension Array {
    func shuffled() {
        // code
    }
}

Еще пример расширения для опционального типа данных:

extension Optional where Wrapped == String {
    var isNilorEmpty: Bool {
        return self?.isEmpty ?? true
    }
}

var name: String?
if name.isNilorEmpty {
    print("1. Property name is empty or has a value 'nil'")
} else {
    print(name)
}

// Output
// 1. Property name is empty or has a value 'nil'

name = ""
if name.isNilorEmpty {
    print("2. Property name is empty or has a value 'nil'")
} else {
    print(name)
}

// Output
// 2. Property name is empty or has a value 'nil'

name = "Some text"
if name.isNilorEmpty {
    print("3. Property name is empty or has a value 'nil'")
} else {
    print(name)
}

// Output
// Optional("Some text")

В расширении к опциональному типу данных, который представляет из себя перечисление, добавленно вычисляемое свойство isNilorEmpty. В расширении указано дополнительное условие для его применения: тип данных в опционале должен быть строковый (String).

Свойство isNilorEmpty вернет булево (логическое) значение, если значение будет либо nil либо пустая строка (“”).


Еще полезные ссылки

Также информацию по расширениям можно получить на странице официальной документации.

Ссылки на официальную документацию: