Псевдоним типа в Swift

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

Рассмотрим различные практические применения typealias:

  1. Повышение удобства восприятия кода. Для большей ясности параметризованному типу словаря присваивается новое название.
  2. Упрощение сигнатур сложных типов. Чтобы упростить сигнатуру функции, для замыкания, которым принимается тип Result, создается псевдоним.
  3. Представление типов функций. Чтобы упростить передачу сетевого запроса как параметра, для функции сетевого запроса определяется тип.
  4. Создание кода целевой платформы. Для цвета color целевой платформы с помощью условной компиляции определяется псевдоним типа.
  5. Работа с кортежами. Чтобы сделать кортеж с координатами точки информативнее, его типу присваивается название.
  6. Рефакторинг или миграция кода. Чтобы осуществить миграцию кода, для обработчиков завершения старых и новых API определяются псевдонимы типов.
  7. Указание ограничений. Чтобы сократить повторы, типу словаря присваивается название с конкретными ограничениями.
  8. Псевдоним для замыканий со сложными параметрами. Типу замыкания с несколькими параметрами, например для работы с данными, присваивается информативное название.
  9. Упрощение типов ключей словаря. Для общего типа словаря, используемого с объектами JSON, создается псевдоним.
  10. Псевдоним типа для композиции протоколов. Для использования в функциях или определениях классов протоколы объединяются в один тип.
  11. Связанные типы в протоколах. Чтобы создать контейнер с конкретным типом, для протокола с помощью typealias указывается связанный тип.

В следующих примерах показано, как с помощью typealias создаются четкие, лаконичные, гибкие структуры кода, благодаря чему в Swift совершенствуется процесс разработки в целом:

// в Swift псевдоним типа используется в следующих практических целях:

// 1. Повышение удобства восприятия кода
typealias StringDictionary<T> = Dictionary<String, T>

// 2. Упрощение сигнатур сложных типов
typealias CompletionHandler = (Result<String, Error>) -> Void

// 3. Представление типов функций
typealias NetworkRequestFunction = (URLRequest, @escaping CompletionHandler) -> Void

// 4. Создание кода целевой платформы
#if os(macOS)
typealias Color = NSColor
#else
typealias Color = UIColor
#endif

// 5. Работа с кортежами
typealias GridPoint = (x: Int, y: Int)

// 6. Рефакторинг или миграция кода
// Старый API
typealias OldAPICompletion = (String) -> Void
// Новый API
typealias NewAPICompletion = (Result<String, Error>) -> Void

// 7. Указание ограничений
typealias StringArrayDictionary = Dictionary<String, [String]>

// 8. Псевдоним для замыканий со сложными параметрами
typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void

// 9. Упрощение типов ключей словаря
typealias JSON = [String: Any]

// 10. Псевдоним типа для композиции протоколов
protocol Drawable {}
protocol Animatable {}
typealias DrawableAndAnimatable = Drawable & Animatable

// 11. Связанные типы в протоколах
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

// Затем связанный тип указывается с помощью «typealias»
typealias IntContainer = Container where Item == Int

1. Повышение удобства восприятия кода

// Без псевдонима типа
func fetchUser(completion: (Result<(name: String, age: Int, email: String), Error>) -> Void) {
// ... реализация функции
}

// С псевдонимом типа для повышения удобства восприятия кода
typealias UserData = (name: String, age: Int, email: String)
typealias FetchUserCompletion = (Result<UserData, Error>) -> Void

func fetchUser(completion: FetchUserCompletion) {
// ... реализация функции
}

// Пример использования
fetchUser { result in
switch result {
case .success(let userData):
print("User Name: \(userData.name), Age: \(userData.age), Email: \(userData.email)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

2. Упрощение сигнатур сложных типов

// Без псевдонима типа сигнатура функции сложна, особенно если используется в нескольких местах
func performNetworkRequest(url: URL, completion: @escaping (Result<(data: Data, response: URLResponse), Error>) -> Void) {
// ... реализация сетевого запроса
}

// С псевдонимом типа сигнатура функции намного проще и понятнее
typealias NetworkResponse = (data: Data, response: URLResponse)
typealias NetworkCompletion = (Result<NetworkResponse, Error>) -> Void

func performNetworkRequest(url: URL, completion: @escaping NetworkCompletion) {
// ... реализация сетевого запроса
}

// Пример использования
let url = URL(string: "https://example.com")!
performNetworkRequest(url: url) { result in
switch result {
case .success(let networkResponse):
print("Data: \(networkResponse.data), Response: \(networkResponse.response)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

3. Представление типов функций

// Без псевдонима типа тип функции для сетевого запроса перегружен, неудобен для восприятия
func performRequest(request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
// ... реализация сетевого запроса
}

// С псевдонимом типа тип функции представлен четким, лаконичным названием
typealias RequestCompletion = (Data?, URLResponse?, Error?) -> Void

func performRequest(request: URLRequest, completion: @escaping RequestCompletion) {
// ... реализация сетевого запроса
}

// Пример использования
let request = URLRequest(url: URL(string: "https://example.com")!)
performRequest(request: request) { data, response, error in
if let error = error {
print("Error: \(error)")
} else {
print("Data: \(String(describing: data)), Response: \(String(describing: response))")
}
}

4. Создание кода целевой платформы

// Без псевдонима типа каждый раз, когда используется тип, нужна условная компиляция
#if os(macOS)
func changeBackgroundColor(to color: NSColor) {
// ...
}
#else
func changeBackgroundColor(to color: UIColor) {
// ...
}
#endif

// С псевдонимом типа, определив тип целевой платформы один раз, вы затем используете его во всем коде
#if os(macOS)
typealias PlatformColor = NSColor
#else
typealias PlatformColor = UIColor
#endif

func changeBackgroundColor(to color: PlatformColor) {
// ... используем «color», который теперь подходит для платформы
}

// Пример использования
let color: PlatformColor = .blue
changeBackgroundColor(to: color)

5. Работа с кортежами

// Без псевдонима типа функции загромождаются кортежами
func calculateDistance(start: (x: Int, y: Int), end: (x: Int, y: Int)) -> Double {
let xDist = Double(end.x - start.x)
let yDist = Double(end.y - start.y)
return sqrt((xDist * xDist) + (yDist * yDist))
}

// С псевдонимом типа кортежу присваивается содержательное название, отчего сигнатура функции становится понятнее
typealias Point = (x: Int, y: Int)

func calculateDistance(start: Point, end: Point) -> Double {
let xDist = Double(end.x - start.x)
let yDist = Double(end.y - start.y)
return sqrt((xDist * xDist) + (yDist * yDist))
}

// Пример использования
let startPoint: Point = (x: 0, y: 0)
let endPoint: Point = (x: 10, y: 10)
let distance = calculateDistance(start: startPoint, end: endPoint)
print("The distance is \(distance)")

Другой практический пример:

// Без псевдонима типа нет четкого понимания кортежей в параметрах функции
func addVectors(v1: (Double, Double), v2: (Double, Double)) -> (Double, Double) {
return (v1.0 + v2.0, v1.1 + v2.1)
}

// С псевдонимом типа у каждого кортежа имеется информативное название, из-за чего повышается удобство восприятия кода
typealias Vector2D = (x: Double, y: Double)

func addVectors(v1: Vector2D, v2: Vector2D) -> Vector2D {
return (v1.x + v2.x, v1.y + v2.y)
}

// Пример использования
let vectorA: Vector2D = (x: 3.0, y: 2.0)
let vectorB: Vector2D = (x: 1.0, y: 4.0)
let resultVector = addVectors(v1: vectorA, v2: vectorB)
print("Resultant Vector: \(resultVector)")

Еще пример:

// Без псевдонима типа нет четкого понимания кортежа в возвращаемом типе функции
func getMinMax(numbers: [Int]) -> (min: Int, max: Int)? {
guard let minNumber = numbers.min(), let maxNumber = numbers.max() else {
return nil
}
return (minNumber, maxNumber)
}

// С псевдонимом типа возвращаемому типу присваивается название, по которому становится понятным его назначение
typealias MinMax = (min: Int, max: Int)

func getMinMax(numbers: [Int]) -> MinMax? {
guard let minNumber = numbers.min(), let maxNumber = numbers.max() else {
return nil
}
return (minNumber, maxNumber)
}

// Пример использования
let numbers = [8, 3, 9, 4, 6]
if let bounds: MinMax = getMinMax(numbers: numbers) {
print("The minimum is \(bounds.min) and the maximum is \(bounds.max).")
}

6. Рефакторинг или миграция кода

// Псевдоним типа и функция старого API для простого обработчика завершения
typealias OldAPIHandler = (String) -> Void

func oldAPI(completion: @escaping OldAPIHandler) {
// Извлечение данных и их возвращение через старый обработчик завершения
let data = "Data from old API"
completion(data)
}

// Псевдоним типа и функция нового API для типа «Result» в случае успеха или неудачи
typealias NewAPIHandler = (Result<String, Error>) -> Void

func newAPI(completion: @escaping NewAPIHandler) {
// Извлечение данных и их возвращение через новый обработчик завершения
let data = "Data from new API"
completion(.success(data))
}

// Выполняем рефакторинг функции для объединения функций старого и нового API
func useNewAPIWithOldHandler(oldCompletion: @escaping OldAPIHandler) {
newAPI { result in
switch result {
case .success(let data):
oldCompletion(data) // Вызываем старый обработчик завершения с данными
case .failure(let error):
print("Error: \(error.localizedDescription)")
// Ошибку при необходимости обрабатываем или адаптируем ее для пользователей старого API
}
}
}

// Пример использования
useNewAPIWithOldHandler { data in
print("Migrated data: \(data)") // Здесь будет выведено: «Migrated data: Data from new API»
}

Другой пример для этого случая применения:

// Сценарий: имеется набор функций API, которые возвращают данные JSON в разных форматах, стандартизируем их.

// Функция старого API, возвращающая данные JSON в виде словаря
typealias OldAPIDataFormat = [String: Any]
func fetchProfile(completion: @escaping (OldAPIDataFormat) -> Void) {
// Моделируем извлечение данных профиля
let profileData: OldAPIDataFormat = ["name": "John", "age": 30]
completion(profileData)
}

// Функция нового API, возвращающая данные JSON в виде декодируемой структуры
typealias NewAPIDataFormat = Result<Profile, Error>
func fetchUserProfile(completion: @escaping (NewAPIDataFormat) -> Void) {
// Моделируем извлечение данных профиля
let profile = Profile(name: "John", age: 30)
completion(.success(profile))
}

// Декодируемая структура, используемая с новым API
struct Profile: Decodable {
var name: String
var age: Int
}

// Выполняем рефакторинг функции для объединения функций старого и нового API
func migrateToNewProfileAPI(oldCompletion: @escaping (OldAPIDataFormat) -> Void) {
fetchUserProfile { result in
switch result {
case .success(let profile):
// Преобразуем структуру «Profile» в старый формат данных
let oldFormatData: OldAPIDataFormat = ["name": profile.name, "age": profile.age]
oldCompletion(oldFormatData)
case .failure(let error):
print("Error: \(error.localizedDescription)")
// Ошибку соответственно обрабатываем
}
}
}

// Пример использования
migrateToNewProfileAPI { oldDataFormat in
print("User name: \(oldDataFormat["name"] ?? ""), Age: \(oldDataFormat["age"] ?? "")")
}

7. Указание ограничений

// Если каждый раз указывать ограничения, то без псевдонима типа код становится перегруженным ими
func mergeDictionaries<K, V>(dict1: Dictionary<K, V>, dict2: Dictionary<K, V>) -> Dictionary<K, V> where K: Hashable, V: Equatable {
var merged = dict1
for (key, value) in dict2 {
if let existingValue = merged[key], existingValue == value {
continue
}
merged[key] = value
}
return merged
}

// С псевдонимом типа ограничения определяются один раз, затем используются повторно
typealias HashableDictionary<K: Hashable, V: Equatable> = Dictionary<K, V>

func mergeDictionaries<K, V>(dict1: HashableDictionary<K, V>, dict2: HashableDictionary<K, V>) -> HashableDictionary<K, V> {
var merged = dict1
for (key, value) in dict2 {
if let existingValue = merged[key], existingValue == value {
continue
}
merged[key] = value
}
return merged
}

// Пример использования
let dict1: HashableDictionary<String, Int> = ["a": 1, "b": 2]
let dict2: HashableDictionary<String, Int> = ["b": 3, "c": 4]
let mergedDict = mergeDictionaries(dict1: dict1, dict2: dict2)
print("Merged Dictionary: \(mergedDict)")

Другой практический пример для этого случая применения:

// Без псевдонима типа указывать ограничения для функции, которой фильтруется словарь, непросто
func filterDictionary<Key, Value>(dictionary: [Key: Value], byPredicate predicate: (Key, Value) -> Bool) -> [Key: Value] where Key: Hashable {
var filteredDictionary = [Key: Value]()
for (key, value) in dictionary where predicate(key, value) {
filteredDictionary[key] = value
}
return filteredDictionary
}

// С псевдонимом типа, чтобы упростить сигнатуры функций, определяется и используется с ограничениями параметризованный тип словаря
typealias HashableDictionary<Key: Hashable, Value> = [Key: Value]

func filterDictionary<Key, Value>(dictionary: HashableDictionary<Key, Value>, byPredicate predicate: (Key, Value) -> Bool) -> HashableDictionary<Key, Value> {
var filteredDictionary = HashableDictionary<Key, Value>()
for (key, value) in dictionary where predicate(key, value) {
filteredDictionary[key] = value
}
return filteredDictionary
}

// Пример использования
let scores: HashableDictionary<String, Int> = ["Alice": 90, "Bob": 85, "Charlie": 95]
let passingScores = filterDictionary(dictionary: scores) { _, score in score >= 90 }
print("Passing Scores: \(passingScores)")

8. Псевдоним для замыканий со сложными параметрами

// Рассмотрим асинхронную функцию загрузки изображений со сложными параметрами замыкания.

// Без псевдонима типа сигнатура замыкания получается длинной и менее понятной.
func downloadImage(from url: URL, completion: @escaping (_ image: UIImage?, _ error: Error?, _ cacheUsed: Bool) -> Void) {
// Логика загрузки изображений
}

// С псевдонимом типа замыкание удобнее для восприятия и понятнее.
typealias ImageDownloadCompletion = (_ image: UIImage?, _ error: Error?, _ cacheUsed: Bool) -> Void

func downloadImage(from url: URL, completion: @escaping ImageDownloadCompletion) {
// Логика загрузки изображений
}

// Пример использования
let imageUrl = URL(string: "https://example.com/image.png")!
downloadImage(from: imageUrl) { image, error, cacheUsed in
if let error = error {
print("Error occurred: \(error.localizedDescription)")
} else if let image = image {
print("Image downloaded: \(image)")
print("Cache used: \(cacheUsed)")
}
}

Другой практический пример для этого применения:

// Рассмотрим функцию обработки данных, которой в качестве параметра принимается сложное замыкание.

// Без псевдонима типа сигнатура замыкания для обработки данных получается длинной и сложной.
func processData(input: Data, completion: @escaping (_ output: Data?, _ error: Error?, _ metrics: [String: Any]?) -> Void) {
// Логика обработки данных
}

// С псевдонимом типа сигнатура замыкания упрощается, она удобнее для восприятия и использования.
typealias DataProcessingCompletion = (_ output: Data?, _ error: Error?, _ metrics: [String: Any]?) -> Void

func processData(input: Data, completion: @escaping DataProcessingCompletion) {
// Логика обработки данных
}

// Пример использования
let rawData = Data()
processData(input: rawData) { output, error, metrics in
if let error = error {
print("Error during processing: \(error.localizedDescription)")
} else if let output = output {
print("Processed data: \(output)")
if let metrics = metrics {
print("Processing metrics: \(metrics)")
}
}
}

9. Упрощение типов ключей словаря

// Без псевдонима типа работа с конкретным типом словаря повторяется и подвержена ошибкам
func parseJSON(json: [String: Any]) -> [String: Any]? {
// Логика парсинга JSON
}

// С псевдонимом типа тип словаря упрощается, код становится удобнее для восприятия
typealias JSONDictionary = [String: Any]

func parseJSON(json: JSONDictionary) -> JSONDictionary? {
// Логика парсинга JSON
}

// Пример использования
let jsonString = "{\"name\":\"John\", \"age\":30}"
if let data = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let json = jsonObject as? JSONDictionary,
let parsedJson = parseJSON(json: json) {
print("Parsed JSON: \(parsedJson)")
}

Другой практический пример для этого применения:

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

// Без псевдонима типа, когда в функции указывается тип словаря, она загромождается
func updateUserSettings(settings: [String: Any]) {
// Обновляем логику пользовательских настроек
}

// С псевдонимом типа тип словаря упрощается, удобство восприятия кода повышается
typealias UserSettings = [String: Any]

func updateUserSettings(settings: UserSettings) {
// Обновляем логику пользовательских настроек
}

// Пример использования
let settings: UserSettings = ["theme": "dark", "notificationsEnabled": true]
updateUserSettings(settings: settings)

10. Псевдоним типа для композиции протоколов

// Определяем протоколы
protocol ProtocolA {
func doSomethingA()
}

protocol ProtocolB {
func doSomethingB()
}

// Без псевдонима типа, когда протоколы объединяются в тип-параметр, функция становится перегруженной ими
//func performAction(on object: AnyObject & ProtocolA & ProtocolB) {
// // Реализация на основе соответствия протоколам «ProtocolA» и «ProtocolB»
// object.doSomethingA()
// object.doSomethingB()
//}

// С псевдонимом типа протоколы ради простоты объединяются в один тип
typealias Actionable = ProtocolA & ProtocolB

// Реализуем функцию, которой принимается объект, соответствующий обоим протоколам
func performAction(on object: Actionable) {
// Здесь вызываются методы, определенные в обоих протоколах
object.doSomethingA()
object.doSomethingB()
}

//// Пример класса, который соответствует обоим протоколам: «ProtocolA» и «ProtocolB»
//class ExampleClass: ProtocolA, ProtocolB {
// func doSomethingA() {
// print("Performed action A")
// }
//
// func doSomethingB() {
// print("Performed action B")
// }
//}
//
//// Пример использования
//let exampleObject = ExampleClass()
//performAction(on: exampleObject)

/// Пример класса, соответствующего единому псевдониму типа «Actionable»
class ExampleClass: Actionable{
func doSomethingA() {
print("Performed action A")
}

func doSomethingB() {
print("Performed action B")
}
}

// Пример использования
let exampleObject = ExampleClass()
performAction(on: exampleObject)

Другой практический пример для этого применения:

// Протоколы для сущностей «Drawable» и «Animatable»
protocol Drawable {
func draw()
}
protocol Animatable {
func animate()
}

// Без псевдонима типа параметры функции перегружены объединяемыми в них протоколами
func animateAndDraw(object: AnyObject & Drawable & Animatable) {
object.draw()
object.animate()
}

// С псевдонимом типа для композиции протоколов создается информативное, лаконичное название
typealias DrawableAndAnimatable = Drawable & Animatable

// Теперь этой функцией принимается один тип, который соответствует и «Drawable», и «Animatable»
func animateAndDraw(object: DrawableAndAnimatable) {
object.draw()
object.animate()
}

//// Пример класса, который соответствует и «Drawable», и «Animatable»
//class Sprite: Drawable, Animatable {
// func draw() {
// print("Drawing the sprite")
// }
//
// func animate() {
// print("Animating the sprite")
// }
//}
//
//// Пример использования
//let sprite = Sprite()
//animateAndDraw(object: sprite)

// Пример класса, соответствующего единому псевдониму типа «DrawableAndAnimatable»
class Sprite: DrawableAndAnimatable {
func draw() {
print("Drawing the sprite")
}

func animate() {
print("Animating the sprite")
}
}

// Пример использования
let sprite = Sprite()
animateAndDraw(object: sprite)

11. Связанные типы в протоколах

// Определяем протокол со связанным типом
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

// Реализуем этот протокол с конкретным связанным типом
struct IntStack: Container {
// Точно указываем этот связанный тип
typealias Item = Int

var items = [Item]()

mutating func append(_ item: Item) {
items.append(item)
}

var count: Int {
return items.count
}

subscript(i: Int) -> Item {
return items[i]
}
}

// Пример использования
var stack = IntStack()
stack.append(1)
stack.append(2)
print("Stack count: \(stack.count)") // Вывод: емкость стека: 2
print("First item: \(stack[0])") // Вывод: первый элемент: 1

Другой практический пример для этого применения:

// Определяем протокол со связанным типом
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

// Реализуем этот протокол для универсального контейнера
struct GenericContainer<T>: Container {
// Точно указываем этот связанный тип, используя параметр дженерик-типа
typealias Item = T

var items = [Item]()

mutating func append(_ item: Item) {
items.append(item)
}

var count: Int {
return items.count
}

subscript(i: Int) -> Item {
return items[i]
}
}

// Пример использования
var stringContainer = GenericContainer<String>()
stringContainer.append("Hello")
stringContainer.append("World")

print("Container count: \(stringContainer.count)") // Вывод: емкость контейнера: 2
print("First item: \(stringContainer[0])") // Вывод: первый элемент: «Hello»

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Cong Le: Typealias in Swift

Предыдущая статьяВстроенные инструменты Golang
Следующая статьяИнженерия данных — не только для инженеров!