Boost your Swift with @dynamicMemberLookup
Swift is a powerful programming language that combines type safety and expressiveness. However, despite its strong typing, the language provides developers with the ability to use dynamic access to object properties using the attribute dynamicMemberLookup
. This can be useful, for example, for working with dynamic data or when creating a DSL (Domain-Specific Language). Using this attribute, we can access the properties of a type instance, even if these properties are not explicitly defined in it.
When working with this attribute, it is important to understand that it only applies to types (struct
, enum
, class
, actor
, protocol
), so, for example, this code will cause a compilation error:
class MyClass { }
@dynamicMemberLookup extension MyClass { } // '@dynamicMemberLookup' attribute cannot be applied to this declaration
For use dynamicMemberLookup
There are only two things you need to do:
Mark the type with the appropriate attribute (
@dynamicMemberLookup
)To implement
subscript
through which we will receive the data we are interested in
Simplifying work with dynamic data
Attribute dynamicMemberLookup
is well suited for working with dynamic data structures, that is, those whose internal structure is formed according to some rule, but the number of elements, their relative positions and relationships can dynamically change during program execution (for example Dictionary
). Usage dynamicMemberLookup
allows you to access an object's properties as if they were statically defined. This makes the code more readable and convenient.
Let's consider the use of an attribute through this basic example of interpreting a dictionary into a JSON structure with the ability to use dot notation to obtain a value by key:
@dynamicMemberLookup
struct JSON {
// Внутренний словарь для хранения ключей и значений
private var data: [String: Any]
init(from data: [String : Any]) {
self.data = data
}
// Необходимый для использования атрибута сабскрипт
subscript(dynamicMember member: String) -> Any? {
data[member]
}
}
Despite the fact that the object JSON
no open properties, thanks to the use of dynamicMemberLookup
we can get a value by key from the internal dictionary as follows:
let json = JSON(from: ["name": "Malil", "age": 21])
print(json.name) // "Malil"
print(json.age) // 21
Hidden text
In this case, there will be no code completion because the properties name
And age
are not defined for the object and are retrieved dynamically. Therefore, when requesting a key, it is possible to make a naming error.
For the most part, this is all syntactic sugar. subscript
we defined a type argument String
which we take from the dictionary data
value and return it. The compiler simply gives us a way to extract data more beautifully, so these two entries will be equivalent in result:
json[dynamicMember: "name"] // "Malil"
json.name // "Malil"
API flexibility and extensibility
By using dynamicMemberLookup
We have the ability to easily add new properties or change existing ones without having to change the interface of our types. This allows us to create more flexible and extensible APIs.
Let's imagine that we are writing a service that must have some initial configuration, the parameters of which will to some extent influence how this service performs its work. Let's skip the details of the logic that might be in it and describe the class of such a service and its configuration model in a basic way:
// Структура с параметрами конфигурации сервиса
struct ServiceConfiguration {
var maxResuls: Int
}
// Класс сервиса
class ServiceImpl {
var configuration: ServiceConfiguration
init(configuration: ServiceConfiguration) {
self.configuration = configuration
}
}
Let's assume that we want to be able to change the parameters set in the initial configuration after the service has been created. To do this, we now need to perform a simple action:
let service = ServiceImpl(configuration: ...)
service.configuration.maxResuls = 30
At first glance, everything looks great. However, if you delve into the details, it becomes obvious that instead of directly accessing the service, we are forced to use an intermediary – the property configuration
in the call chain. It would be more convenient to just tell the service: “Now the maximum number of results you can return is X.”
To make the API of this service more intuitive and convenient, we will use the attribute dynamicMemberLookup
. For secure access to the properties of the object that interest us ServiceConfiguration
we will apply WritableKeyPath
which will allow you to not only safely access properties, but also write values to them (if you are interested in learning more about what is KeyPath
and how to work with it, be sure to check out documentation). As a result we get the following:
@dynamicMemberLookup
class ServiceImpl {
private var configuration: ServiceConfiguration
init(configuration: ServiceConfiguration) {
self.configuration = configuration
}
// Сабскрипт для чтения и изменения свойств `configuration` через `WritableKeyPath`
subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T {
get { configuration[keyPath: keyPath] }
set { configuration[keyPath: keyPath] = newValue }
}
}
Now the property configuration
can be marked as private
making it completely inaccessible. Instead, we can directly access any property ServiceConfiguration
directly from the service instance, and thanks to the use of KeyPath
We also have code autocompletion, which makes it completely safe:
let service = ServiceImpl(configuration: ...)
// Эта запись изменяет `maxResuls` у свойства `configuration` внутри `ServiceImpl`
service.maxResuls = 30
However, since you, dear readers, are developers of high culture, you certainly avoid using concrete service implementations as dependencies and prefer to work with abstractions in the form of protocols (Dependency Inversion). In this regard, an interesting question arises: how to add the ability to dynamically access properties to an object when interacting with it through a protocol?
The solution is actually very simple. We already know what is needed to implement the capabilities dynamicMemberLookup
. All we need to do in this case is to mark the protocol itself with this attribute and add the one we need to its contract. subscript
. Thus, the service interface and its implementation may look like this:
// Протокол сервиса
@dynamicMemberLookup
protocol Service: AnyObject {
init(configuration: ServiceConfiguration)
subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T { get set }
}
// Реализация сервиса
class ServiceImpl: Service {
private var configuration: ServiceConfiguration
required init(configuration: ServiceConfiguration) {
self.configuration = configuration
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T {
get { configuration[keyPath: keyPath] }
set { configuration[keyPath: keyPath] = newValue }
}
}
As a result, even if our dependency is of protocol type, we can still dynamically access properties ServiceConfiguration
from a service instance, as in the previous example:
let service: Service = ServiceImpl(configuration: ...)
service.maxResuls = 30
Conclusion
Attribute dynamicMemberLookup
in Swift opens up interesting possibilities for working with types, allowing us to dynamically extract properties and create more expressive APIs. This makes code easier to read and understand, and makes it more elegant. However, as with any functionality, it is important to use this attribute wisely to avoid unnecessarily complicating our types.
If you are interested in going into more detail, I recommend checking out the offer SE-0195where you will find the motivation and context behind the addition of this attribute to our beloved language.