В Swift ключевое слово typealias
— это мощный функционал, с помощью которого разработчики переименовывают типы, создают более сопровождаемый и удобный для восприятия код.
Рассмотрим различные практические применения typealias
:
- Повышение удобства восприятия кода. Для большей ясности параметризованному типу словаря присваивается новое название.
- Упрощение сигнатур сложных типов. Чтобы упростить сигнатуру функции, для замыкания, которым принимается тип
Result
, создается псевдоним. - Представление типов функций. Чтобы упростить передачу сетевого запроса как параметра, для функции сетевого запроса определяется тип.
- Создание кода целевой платформы. Для цвета
color
целевой платформы с помощью условной компиляции определяется псевдоним типа. - Работа с кортежами. Чтобы сделать кортеж с координатами точки информативнее, его типу присваивается название.
- Рефакторинг или миграция кода. Чтобы осуществить миграцию кода, для обработчиков завершения старых и новых API определяются псевдонимы типов.
- Указание ограничений. Чтобы сократить повторы, типу словаря присваивается название с конкретными ограничениями.
- Псевдоним для замыканий со сложными параметрами. Типу замыкания с несколькими параметрами, например для работы с данными, присваивается информативное название.
- Упрощение типов ключей словаря. Для общего типа словаря, используемого с объектами JSON, создается псевдоним.
- Псевдоним типа для композиции протоколов. Для использования в функциях или определениях классов протоколы объединяются в один тип.
- Связанные типы в протоколах. Чтобы создать контейнер с конкретным типом, для протокола с помощью
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»
Читайте также:
- Подробно об акторах в Swift
- iOS/Swift: подробное руководство по модульным и UI-тестам. Часть 2
- Реактивное программирование с Combine
Читайте нас в Telegram, VK и Дзен
Перевод статьи Cong Le: Typealias in Swift