0Pricing
Swift Academy · Урок

@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 — локальная установка не требуется.

Все уроки этого курса

  1. Основы KeyPath
  2. Использование KeyPaths в коллекциях
  3. Записываемые и ссылочные KeyPaths
  4. @dynamicMemberLookup
← Назад к Swift Academy