@dynamicMemberLookup
Создавайте динамический типобезопасный доступ к членам.
«@dynamicMemberLookup» — бесплатный урок Swift Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.
Что такое динамический поиск членов
Атрибут @dynamicMemberLookup позволяет типу обрабатывать доступ к члену через точечный синтаксис, даже если этот член не объявлен как настоящее свойство. Когда Вы пишете value.something, Swift направляет обращение к специальному индексатору, который Вы определяете.
Благодаря этому типы-обёртки выглядят естественно, а логика доступа остаётся в одном месте.
@dynamicMemberLookup
struct Settings {
private var storage: [String: String] = ["theme": "dark"]
subscript(dynamicMember key: String) -> String? {
storage[key]
}
}
let s = Settings()
print(s.theme ?? "none")Обязательный индексатор
Тип, помеченный атрибутом @dynamicMemberLookup, должен реализовать subscript(dynamicMember:). Если параметр имеет тип String, введённое Вами имя члена во время выполнения становится этой строкой.
Поэтому config.timeout вызывает индексатор с ключом "timeout".
@dynamicMemberLookup
struct Config {
let values: [String: Int]
subscript(dynamicMember member: String) -> Int {
values[member] ?? 0
}
}
let config = Config(values: ["timeout": 30, "retries": 3])
print(config.timeout)
print(config.retries)Поиск по строке не обеспечивает проверку типов
Динамический поиск членов на основе строк гибок, но лишает программу проверки во время компиляции. Если Вы ошиблись в имени члена, код всё равно скомпилируется, и Вы получите результат, который индексатор возвращает для отсутствующего ключа.
В примере ниже неизвестный член спокойно возвращает значение по умолчанию, а не приводит к ошибке сборки.
@dynamicMemberLookup
struct Config {
let values: [String: Int]
subscript(dynamicMember member: String) -> Int {
values[member] ?? -1
}
}
let config = Config(values: ["timeout": 30])
print(config.tmout)Типобезопасный поиск с помощью ключевых путей
Для безопасности можно сделать так, чтобы индексатор принимал KeyPath, а не строку. Теперь компилятор проверяет наличие члена в обёрнутом типе, возвращая полную проверку типов и сохраняя перенаправление доступа.
Этот шаблон особенно хорошо подходит для типов-обёрток, хранящих строго типизированное значение.
struct User { let name: String; let age: Int }
@dynamicMemberLookup
struct Boxed {
let user: User
subscript<T>(dynamicMember path: KeyPath<User, T>) -> T {
user[keyPath: path]
}
}
let boxed = Boxed(user: User(name: "Ada", age: 36))
print(boxed.name)
print(boxed.age)Почему вариант с ключевым путём безопаснее
Поскольку индексатор принимает KeyPath<User, T>, разрешены только действительно существующие члены User. Теперь опечатка вроде boxed.naem приведёт к ошибке компиляции, в отличие от варианта со строкой.
Вы получаете удобство точечного синтаксиса без связанных с ним рисков.
struct User { let name: String; let age: Int }
@dynamicMemberLookup
struct ReadOnly {
let value: User
subscript<T>(dynamicMember path: KeyPath<User, T>) -> T {
value[keyPath: path]
}
}
let r = ReadOnly(value: User(name: "Lin", age: 22))
print(r.name, r.age)Динамические члены, доступные для записи
Если индексатор предоставляет средство записи и принимает WritableKeyPath, можно также присваивать значения перенаправленным членам. Обёртка напрямую передаёт запись обёрнутому значению.
Так получается прозрачная изменяемая обёртка.
struct Profile { var name: String; var score: Int }
@dynamicMemberLookup
struct Wrapper {
var value: Profile
subscript<T>(dynamicMember path: WritableKeyPath<Profile, T>) -> T {
get { value[keyPath: path] }
set { value[keyPath: path] = newValue }
}
}
var w = Wrapper(value: Profile(name: "Mo", score: 0))
w.score = 99
print(w.value.score)Перенаправление к обёрнутому объекту
Динамический поиск членов отлично подходит для лёгких прокси, которые в основном перенаправляют обращения к внутреннему значению, но добавляют немного поведения. Индексатор хранит всю логику перенаправления в одном месте.
Здесь обёртка для журналирования перенаправляет чтение и выводит, к какому члену был выполнен доступ.
struct Account { let id: Int; let balance: Double }
@dynamicMemberLookup
struct Logged {
let account: Account
subscript<T>(dynamicMember path: KeyPath<Account, T>) -> T {
let result = account[keyPath: path]
print("accessed a member")
return result
}
}
let logged = Logged(account: Account(id: 7, balance: 100))
print(logged.balance)Сочетание с идеями динамических вызовов
Динамический поиск членов хорошо сочетается с обобщениями. Один обобщённый индексатор может перенаправлять свойства любого типа, поэтому одна обёртка подходит для значений разной формы.
Здесь один и тот же Proxy работает независимо от того, является ли член строкой или целым числом.
struct Item { let title: String; let count: Int }
@dynamicMemberLookup
struct Proxy<Wrapped> {
let wrapped: Wrapped
subscript<T>(dynamicMember path: KeyPath<Wrapped, T>) -> T {
wrapped[keyPath: path]
}
}
let proxy = Proxy(wrapped: Item(title: "Book", count: 4))
print(proxy.title, proxy.count)Строковый поиск для данных в формате JSON
У строкового варианта тоже есть своё применение: представление слабо структурированных данных, например разобранного JSON, в котором члены неизвестны во время компиляции. Каждый доступ превращается в поиск в словаре.
Здесь небольшое значение, подобное JSON, предоставляет свои поля через точечный синтаксис.
@dynamicMemberLookup
struct JSON {
let fields: [String: String]
subscript(dynamicMember key: String) -> String {
fields[key] ?? ""
}
}
let json = JSON(fields: ["city": "Oslo", "country": "Norway"])
print(json.city)
print(json.country)Ограничения, о которых стоит помнить
Динамический поиск членов влияет только на синтаксис доступа к членам — настоящих хранимых свойств он не добавляет. Такие средства, как автодополнение, работают для варианта с ключевыми путями, но не для произвольных строковых ключей.
Предпочитайте вариант с ключевыми путями, когда структура лежащих в основе данных известна, а строковый вариант оставляйте для действительно динамических данных.
struct Point { let x: Int; let y: Int }
@dynamicMemberLookup
struct Safe {
let point: Point
subscript<T>(dynamicMember path: KeyPath<Point, T>) -> T {
point[keyPath: path]
}
}
let safe = Safe(point: Point(x: 3, y: 4))
print(safe.x + safe.y)Практический декоратор
Объединим всё вместе: ниже приведена инструментированная обёртка, которая перенаправляет чтение каждого свойства внутреннему значению и одновременно подсчитывает обращения. Реальные платформы используют эту идею для наблюдения и трассировки.
Точечный синтаксис обёртки выглядит так же, как непосредственная работа со значением.
struct Sensor { let temperature: Double; let humidity: Double }
@dynamicMemberLookup
class Tracked {
let sensor: Sensor
var reads = 0
init(_ s: Sensor) { sensor = s }
subscript<T>(dynamicMember path: KeyPath<Sensor, T>) -> T {
reads += 1
return sensor[keyPath: path]
}
}
let t = Tracked(Sensor(temperature: 21.5, humidity: 0.4))
print(t.temperature)
print(t.humidity)
print(t.reads)Быстрая проверка: типобезопасный динамический поиск
Вспомните, какой параметр индексатора делает динамический поиск членов типобезопасным.
Повторение: динамический поиск членов
Вы узнали, как работает @dynamicMemberLookup:
- Атрибут перенаправляет обращения к необъявленным членам через точечный синтаксис в
subscript(dynamicMember:). - Параметр типа
Stringпредоставляет гибкий доступ без проверки типов, что особенно удобно для данных, подобных JSON. - Параметр типа
KeyPathвозвращает полную типобезопасность и автодополнение. WritableKeyPathс установщиком позволяет перенаправлять записи через оболочку.- Это синтаксическое упрощение, которое естественно сочетается с шаблонами прокси и декоратора.
Теперь у Вас есть полный набор средств для работы с ключевыми путями: чтение, запись, использование в коллекциях и динамическое перенаправление.
struct User { let name: String }
@dynamicMemberLookup
struct Wrap {
let user: User
subscript<T>(dynamicMember path: KeyPath<User, T>) -> T {
user[keyPath: path]
}
}
print(Wrap(user: User(name: "Final")).name)Часто задаваемые вопросы
Урок «@dynamicMemberLookup» бесплатный?
Да — полный текст урока «@dynamicMemberLookup» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.
Чему я научусь в уроке «@dynamicMemberLookup»?
Создавайте динамический типобезопасный доступ к членам. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Swift Academy?
Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «@dynamicMemberLookup»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Swift Academy?
Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы KeyPath
- Использование KeyPaths в коллекциях
- Записываемые и ссылочные KeyPaths
- @dynamicMemberLookup