F
Fckn Coding (Swift Notes)
@fckncoding519 подп.
629просмотров
30 декабря 2025 г.
Score: 692
DynamicMemberLookup Недавно я занимался конфигурациями сборки в Tuist и заметил, как прикольно у них сделана работа с окружением. Все переменные окружения, соответствующие шаблону TUIST_XXX, будут доступны как обычные свойства в Environment: // TUIST_APP_NAME Environment.appName.getString(default: "TuistServer") // TUIST_STATIC_LINKING Environment.staticLinking.getBoolean(default: false) Если вам, как и мне, сразу стало интересно, как можно связать динамические значения из окружения и обращения к ним через enum, давайте разбираться. Когда я впервые увидел этот API, я подумал, что тут используется какая-то кодгенерация. Но поскольку Tuist нигде больше не требует явного указания ключей, сгенерировать правильный список свойств здесь попросту невозможно. Но как тогда эти свойства могут быть сформированы? На самом деле — никак: здесь нет никаких проверок на этапе компиляции, и это вовсе не настоящие свойства, а синтаксический сахар, который работает через @dynamicMemberLookup: @dynamicMemberLookup public enum Environment { // ... public static subscript(dynamicMember member: String) -> Value? { value(for: member, environment: ProcessInfo.processInfo.environment) } // ... } Поэтому любое обращение вида Environment.someKey неявно конвертируется в вызов subscript(dynamicMember:). Из-за этого компилятор не может: - проверять существование ключа на этапе компиляции; - вывести конкретный тип для каждого ключа; - выдавать autocomplete по доступным ключам. При этом у @dynamicMemberLookup есть ещё одна — строго типизированная — форма, которая лишена недостатков описанных выше: static subscript<Root, Value>(dynamicMember keyPath: KeyPath<Root, Value>) -> Value? { // ... } В примере с Environment её можно использовать для кастомных переменных: @dynamicMemberLookup public enum Environment { // ... public static subscript(dynamicMember keyPath: KeyPath<Custom, String>) -> String? { let value = value(for: keyPath, environment: ProcessInfo.processInfo.environment) return if case let .string(value) = value { value } else { nil } } public static subscript(dynamicMember keyPath: KeyPath<Custom, Bool>) -> Bool? { let value = value(for: keyPath, environment: ProcessInfo.processInfo.environment)?.value return if ["1", "true", "TRUE", "yes", "YES"].contains(value) { true } else if ["0", "false", "FALSE", "no", "NO"].contains(value) { false } else { nil } } static func value( for keyPath: KeyPath<some Any, some Any>, environment: [String: String] = ProcessInfo.processInfo.environment ) -> Environment .Value? { // debugDescription is used only for example purposes let debugName = keyPath.debugDescription.split(separator: ".").last! return value(for: String(debugName), environment: environment) } // ... } public extension Environment { enum Custom { var appName: String { fatalError("Unreachable") } var staticLinking: Bool { fatalError("Unreachable") } } } Такой вариант позволяет включить дополнительные проверки компилятора: он сможет проверять типы и начнёт подсказывать доступные свойства. Но резолв конкретных значений всё ещё остаётся в runtime.
629
просмотров
3213
символов
Нет
эмодзи
Нет
медиа

Другие посты @fckncoding

Все посты канала →
DynamicMemberLookup Недавно я занимался конфигурациями сборк — @fckncoding | PostSniper