JavaRush /Курси /Swift SELF /Namespacing: вкладені типи і typealias

Namespacing: вкладені типи і typealias

Swift SELF
Рівень 42 , Лекція 3
Відкрита

1. Namespacing через вкладені типи

Коли ви пишете невелику програму на 50 рядків, імена живуть дружно: Book, Library, parse, printHelp. Але варто коду підрости — і починається вечірка збігів: у вас з’являються кілька різних Config, три різні Error, два Parser, і раптом навіть слово Result починає здаватися підозрілим. У підсумку мозок витрачає сили не на логіку, а на розшифрування: «Що це за Config і чому він тут?»

Swift у цьому сенсі чесний: він не намагається «магічно» розгрібти хаос імен. Натомість він дає інструменти, щоб ви самі побудували структуру: вкладені типи як простір імен і typealias як псевдоніми. Якщо користуватися ними помірно, код починає читатися як охайна шафа, а не як коробка з дротами «про всяк випадок».

Вкладені типи: Outer.Inner

Вкладені типи — це коли один тип оголошено всередині іншого. Ідея проста: якщо сутність логічно належить іншій сутності, то й імʼя має це відображати. Не «десь у глобальному просторі імен існує Defaults», а «у LibraryCLI є Defaults». У Swift це виглядає як LibraryCLI.Defaults.pageSize, і вже за іменем видно контекст: звідки взялося, до чого належить і чому не конфліктує з іншими Defaults у проєкті.

Уявіть, що в нас є невеликий консольний застосунок «бібліотека», де ми зберігатимемо книги в памʼяті — поки що. Почнемо з мінімальної моделі.

import Foundation

struct Book {
    let title: String
    let year: Int
}

struct Library {
    var books: [Book] = []
}

Поки все добре. Але щойно ви захочете додати «налаштування за замовчуванням», у вас з’явиться спокуса написати struct Defaults { ... } десь поруч. І саме тут namespacing починає рятувати: Defaults — занадто загальне імʼя, воно обов’язково з кимось конфліктуватиме.

«Порожній enum» як контейнер імен

Один із найпопулярніших прийомів для namespacing у Swift — зробити «контейнер», який неможливо випадково створити. Часто для цього використовують enum без кейсів: у нього не буває екземплярів, але всередині можна зберігати вкладені типи, typealias, константи та функції. Це схоже на «папку» в проєкті, тільки на рівні коду.

У нашому мінізастосунку заведімо такий «контейнер імен» LibraryCLI і покладемо туди значення за замовчуванням.

import Foundation

enum LibraryCLI { }

extension LibraryCLI {
    enum Defaults {
        static let welcomeMessage = "Ласкаво просимо до LibraryCLI!"
        static let maxTitleLength = 80
    }
}

print(LibraryCLI.Defaults.welcomeMessage) // Ласкаво просимо до LibraryCLI!

Тут важливо вловити думку: Defaults більше не «гуляє» глобальним простором імен. Вона живе всередині LibraryCLI, а отже шанс конфлікту майже нульовий, і читач коду одразу розуміє походження. Це й є namespacing: простір імен тепер обмежено контейнером.

Команди та парсинг усередині LibraryCLI

Вкладені типи корисні не лише для констант. Вони чудово працюють, коли у вас є невеликий набір сутностей, які логічно належать одному модулю або частині застосунку: команди, помилки, режими, формати виведення. І навіть якщо поки весь проєкт в одному файлі, це все одно дає структуру та дисципліну.

Додаймо список команд, які підтримуватимемо: help, list, add. Ми вже вміємо enum з асоційованими значеннями, тож можна акуратно зробити команду add(title:year:).

import Foundation

enum LibraryCLI {
    enum Command {
        case help
        case list
        case add(title: String, year: Int)
    }
}

let cmd: LibraryCLI.Command = .add(title: "Swift Basics", year: 2026)
print(cmd) // add(title: "Swift Basics", year: 2026)

Зверніть увагу на тип: LibraryCLI.Command. Він читається як «команда нашого CLI», а не як «якась Command взагалі». Якщо у вашому проєкті з’явиться ще, скажімо, Network.Command в іншому контексті — конфлікту не буде, і мозок не плутатиметься.

Тепер зробімо найпростіший парсер команд. Зараз ми не будуємо повноцінний CLI-парсер — це окрема велика тема. Нам важливий саме ефект від namespacing.

import Foundation

extension LibraryCLI {
    static func parseCommand(_ line: String) -> Command {
        let parts = line.split(separator: " ")
        guard let first = parts.first else { return .help }

        switch first {
        case "list": return .list
        case "help": return .help
        default:     return .help
        }
    }
}

let parsed = LibraryCLI.parseCommand("list")
print(parsed) // list

Поки add ми не розбираємо, щоб не роздувати приклад, але ідея ясна: parseCommand живе там, де йому логічно місце, — у «модулі» LibraryCLI.

Якщо переписати це без namespacing, ви отримаєте окремі Command, parseCommand, Defaults, можливо Error, і за кілька тижнів самі почнете підозрювати, що це писав хтось інший (спойлер: це були ви).

3. typealias для читабельності

Псевдонім типу і чому це не новий тип

typealias — це другий засіб від «перевантаження мозку» у Swift. Він дозволяє дати наявному типу інше імʼя. Ключове: це не новий тип, а просто псевдонім. Тобто typealias UserID = Int не створює «особливий UserID, який не можна переплутати з роком». Це все той самий Int, просто з більш промовистою назвою.

Ця річ особливо корисна, коли тип стає довгим або коли ви хочете підкреслити сенс ролі: «це частотна карта», «це індекс», «це предикат для фільтрації».

Для нашого застосунку заведімо псевдоніми: рік книги та список книг.

import Foundation

enum LibraryCLI {
    typealias Year = Int
    typealias BookList = [Book]
}

let y: LibraryCLI.Year = 2026
print(y) // 2026

Так, це все ще Int. Але в сигнатурах функцій це починає працювати як коментар, який компілятор не видалив. Наприклад, порівняйте відчуття від таких сигнатур:

  • func addBook(title: String, year: Int)
  • func addBook(title: String, year: LibraryCLI.Year)

Друга трохи ясніша: «це рік», а не «якеся число».

Невелика, але важлива ремарка: typealias не може «протягнути» деякі атрибути типів. Наприклад, зробити typealias X = Int! не можна — це заборонено, і такі обмеження в мові зумовлені тим, що ! у Swift — це не просто «частина типу», а дещо складніша історія.

typealias для словників і замикань

Зазвичай typealias починає справді окуповуватися там, де типи стають шумом. Особливо це помітно на словниках і замиканнях.

Уявімо, що ми хочемо порахувати, скільки книг якого року є в нашій бібліотеці (так, це дивна статистика, але програмісти люблять дивні статистики — інакше як виправдати існування деяких звітів).

Без псевдоніма тип виглядатиме так: [Int: Int]. Можна здогадатися, що це «рік → кількість», але не одразу. Давайте зробимо промовистий typealias.

import Foundation

extension LibraryCLI {
    typealias YearCountMap = [Year: Int]
}

func countBooksByYear(_ books: [Book]) -> LibraryCLI.YearCountMap {
    var result: LibraryCLI.YearCountMap = [:]
    for b in books { result[b.year, default: 0] += 1 }
    return result
}

Тепер YearCountMap — це не «якийсь словник», а конкретна роль. І коли ви побачите його в коді через місяць, не доведеться розшифровувати, що є ключем, а що — значенням.

Із замиканнями та сама історія. Припустімо, ми хочемо відфільтрувати книги за якоюсь умовою: «рік не менший», «назва містить», «що завгодно». Тип замикання буде (Book) -> Bool. Він не страшний, але коли таких параметрів кілька, код починає виглядати як математичний трактат.

import Foundation

extension LibraryCLI {
    typealias BookPredicate = (Book) -> Bool
}

func filterBooks(_ books: [Book], using predicate: LibraryCLI.BookPredicate) -> [Book] {
    books.filter(predicate)
}

Сигнатура стала читатися як «використовуючи предикат книги», а не як «використовуючи функцію, що приймає книгу і повертає булеве значення». Компілятору байдуже, а людині — приємно.

Комбінуємо namespacing і typealias

Найприємніший ефект виходить, коли ви поєднуєте обидва прийоми: namespacing через вкладені типи та typealias усередині цих просторів імен. Тоді ви отримуєте «кишенькові словники» просто в коді: LibraryCLI.Types, LibraryCLI.Messages, LibraryCLI.Parsing.

Зробімо охайну організацію: усередині LibraryCLI заведімо enum Types для псевдонімів і enum Messages для рядків, які друкуємо користувачу.

import Foundation

enum LibraryCLI {
    enum Types {
        typealias Year = Int
        typealias BookPredicate = (Book) -> Bool
    }

    enum Messages {
        static let help = "Команди: help, list, add <title> <year>"
        static let unknown = "Невідома команда. Введіть help."
    }
}

print(LibraryCLI.Messages.help) // Команди: help, list, add <title> <year>

Чому це зручно: ви не створюєте тисячу глобальних імен (helpMessage, unknownCommandMessage, BookPredicate, Year…), а зберігаєте їх в одному місці. При цьому назви стають коротшими та точнішими. LibraryCLI.Messages.help виглядає як «документація в рантаймі».

4. Як вибирати між nested types, typealias і новим типом

Коли ви бачите проблему «занадто багато імен» або «занадто складний тип», хочеться застосувати все й одразу. Але краще мати маленьку ментальну шпаргалку, щоб вибирати інструмент без фанатизму.

Ситуація Що використовувати Чому це доречно Мініприклад
Константи, повідомлення або налаштування належать одному модулю Вкладений тип (часто
enum
без кейсів)
Не захаращує глобальні імена, показує належність
LibraryCLI.Messages.help
Кілька сутностей із однаковими загальними іменами (Defaults, Config, Error) Namespacing через Outer.Inner Усуває конфлікти й робить контекст явним Parser.Error vs Storage.Error
Довгий тип заважає читати сигнатури typealias Скорочує шум, додає сенсу ролі
typealias BookPredicate = (Book) -> Bool
Потрібна типобезпека, щоб не переплутати Year і UserID Не typealias typealias не створює новий тип, а лише перейменовує старий (У цьому курсі ідею нових типів будемо застосовувати через окремі моделі, але не сьогодні.)

Зверніть увагу, що останній рядок спеціально попереджає: typealias — про читабельність, а не про жорсткий захист від помилок. Це як наліпки на коробках: корисно, але коробки від цього не стають різними за формою.

5. Типові помилки при namespacing і typealias

У цій темі помилки рідко мають вигляд «червоний компілятор кричить». Частіше вони виглядають так: через місяць код став важче читати, хоча технічно все працює. Тобто це помилки архітектури та смаку — а отже, найпідступніші. Поговорімо про основні граблі так, щоб ви натрапили на них у навчальній аудиторії, а не в продакшені.

Помилка №1: «Зроблю один мегаконтейнер і складу туди взагалі все».
Інколи студент заводить enum App { ... } і всередину кидає і повідомлення, і парсер, і модель, і алгоритми сортування, і «про всяк випадок» ще десять утиліт. Формально це namespacing, але за відчуттями — комора без полиць. Namespacing добрий, коли всередині є маленькі тематичні «теки» (Messages, Defaults, Types), а не одна велика купа.

Помилка №2: надто глибока вкладеність на кшталт A.B.C.D.E.
Вкладені типи мають допомагати читати, а не перетворювати кожне звернення на «адресу в бюрократії». Зазвичай достатньо 1–2 рівнів: LibraryCLI.Messages.help читається нормально, а LibraryCLI.Configuration.Runtime.Messages.Help.shortVersion вже змушує замислитися, чи не простіше структуру перейменувати та спростити.

Помилка №3: очікувати від typealias типобезпеки.
Якщо ви написали typealias Year = Int, компілятор не відрізнятиме Year від будь-якого іншого Int. Тому let year: Year = 2026 і let x: Int = year — це один і той самий тип. typealias покращує читабельність, але не «замикає двері». Якщо вам потрібне саме «не можна переплутати», це розв’язується іншими інструментами проєктування типів, а не псевдонімами.

Помилка №4: typealias заради скорочення, але без сенсу.
Поганий typealias — це коли він приховує сенс, а не розкриває його. Наприклад, typealias A = [Int: String] — не допомагає, бо імʼя A нічого не говорить. Хороший typealias відповідає на запитання: «Яку роль відіграє цей тип?» typealias YearCountMap = [Year: Int] вже зрозуміліший, навіть якщо ви вперше відкрили файл.

Помилка №5: конфлікт імен усередині namespace.
Namespacing не скасовує здорового глузду. Можна примудритися зробити LibraryCLI.Types.Types (так, так теж буває) або Messages.Message. У результаті формально все рознесено, але читається гірше. Імена всередині namespace все одно мають бути нормальними, просто тепер ви можете дозволити собі трохи загальніші слова, бо контекст уже вказано ліворуч (LibraryCLI.Messages).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ