Русский
Русский
English
Статистика
Реклама

Swift Копируй-изменяй


Часто бывает так, что нам нужно скопировать объект, изменив некоторые его свойства, но сохранив остальные неизменными. Для этой задачи существует функция copy().

Это отрывок описания метода copy() из документации Kotlin. На нашем родном языке Swift это означает примерно такую возможность:


struct User {    let id: Int    let name: String    let age: Int}let steve = User(id: 1, name: "Steve", age: 21)// Копируем экземпляр, изменив свойства `name` и `age`let steveJobs = steve.changing { newUser in    newUser.name = "Steve Jobs"    newUser.age = 41}

Выглядит вкусно, не так ли?


Увы, в Swift отсутствует подобный функционал "из коробки". Это небольшое руководство поможет реализовать его самостоятельно.


В чемпроблема


Почему бы просто не делать свойства изменяемыми, объявляя их ключевым словом var вместо let?


struct User {    let id: Int    var name: String    var age: Int}let steve = User(id: 1, name: "Steve", age: 21)...var steveJobs = stevesteveJobs.name = "Steve Jobs"steveJobs.age = 41

Тут есть несколько проблем:


  • Изменение таких полей будет невозможным, если не объявить мутабельным и новый экземпляр структуры, а это лишает гарантии, что он не модифицируется где-то еще.
  • Сложнее сделать изменения "атомарными". К примеру, в случае наблюдаемых свойств блоки willSet и didSet вызываются при изменении каждого поля.
  • Субъективно, но такой код нельзя назвать лаконичным и изящным.

Да, есть еще один вариант создавать новый экземпляр, передавая в инициализатор структуры полный набор его параметров:


// Создаем новый экземпляр, изменяя свойство `name`let steveJobs = User(    id: steve.id,     name: "Steve Jobs",    age: steve.age)

Но такое решение выглядит совсем неудобным, особенно, когда нужно неоднократно создавать измененную копию. Для больших структур это и вовсе неприемлемо из-за громоздких конструкций, где изменяемое свойство сходу не разглядеть.


Впрочем, в нашей реализации тоже будет присутствовать вызов инициализатора, но он будет "плоским" и написан один раз для типа, к тому же его легко автоматизировать кодогенерацией.


Как реализовать


План довольно прост:


  • Сначала напишем универсальную обертку для копии, все свойства которой будут мутабельными и повторять контент копируемого типа.
  • Далее добавим протокол Changeable с реализацией по-умолчанию, который позволит копировать экземпляры с измененными свойствами, используя универсальную обертку.
  • В итоге останется подписать типы под этот протокол, реализовав инициализацию из копии.


Структура изменяемой обертки


Так как обертка должна быть универсальной, а поля конкретного типа нам неизвестны, то потребуется некоторая интроспекция. С этим поможет динамический доступ к свойствам через Key-Path выражения, а фича Key-Path Dynamic Member Lookup из Swift 5.1 сделает все красивым и удобным.


Используя эти синтаксические возможности, получаем небольшую generic-структуру:


@dynamicMemberLookupstruct ChangeableWrapper<Wrapped> {    private let wrapped: Wrapped    private var changes: [PartialKeyPath<Wrapped>: Any] = [:]    init(_ wrapped: Wrapped) {        self.wrapped = wrapped    }    subscript<T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T {        get {             changes[keyPath].flatMap { $0 as? T } ?? wrapped[keyPath: keyPath]         }        set {            changes[keyPath] = newValue        }    }}

Особое внимание здесь заслуживает сабскрипт, который позволяет считать и записать значения свойств через динамические ключи KeyPath. В геттере мы сначала извлекаем подходящее значение из словаря изменений, а если ничего не нашлось, то возвращаем значение оригинального свойства. Сеттер же всегда пишет новое значение в словарь изменений.


При извлечении значения из словаря недостаточно просто написать changes[keyPath] as? T, потому что в случае опционального типа T мы получим уже двойную опциональность. Тогда геттер будет возвращать nil, даже если свойство не менялось, и в оригинальном экземпляре у него есть значение. Чтобы этого избежать, достаточно приводить тип с помощью метода flatMap(:), который выполнится, только если в словаре changes есть значение для ключа.

Сигнатура нашего сабскрипта и атрибут @dynamicMemberLookup позволяют работать с оберткой так, будто это оригинальная структура, в которой все свойства объявлены переменными через var.



При этом для обертки доступны все блага Xcode в виде автодополнения и документации свойств. А строгость типов и проверки на этапе компиляции гарантируют корректность обращений к свойствам: неверные значения и опечатки в названиях не пройдут.


Протокол Changeable


Теперь, чтобы легко копировать экземпляры с измененными свойствами, напишем простой протокол Changeable с реализацией метода копирования:


protocol Changeable {    init(copy: ChangeableWrapper<Self>)}extension Changeable {    func changing(_ change: (inout ChangeableWrapper<Self>) -> Void) -> Self {        var copy = ChangeableWrapper<Self>(self)        change(&copy)        return Self(copy: copy)    }}

Метод changing(:) получает в параметрах замыкание, которое вызывается со ссылкой на изменяемую копию, далее из модифицированной копии создается новый экземпляр оригинального типа.


Кроме метода копирования с изменениями, протокол объявляет требование для инициализатора из копии, который должен быть реализован в каждом типе для соответствия протоколу Changeable:


extension User: Changeable {    init(copy: ChangeableWrapper<Self>) {        self.init(            id: copy.id,            name: copy.name,            age: copy.age        )    }}

Подписав тип под протокол и реализовав этот инициализатор, мы получаем то, что хотели копирование измененных экземпляров:


let steve = User(id: 1, name: "Steve", age: 21)let steveJobs = steve.changing { newUser in    newUser.name = "Steve Jobs"    newUser.age = 30}

Но это еще не все, есть один момент, который требует маленькой доработки



Вложенные свойства


Сейчас метод changing(:) удобен, когда изменяются свойства первого уровня, но часто хочется копировать экземпляры с изменениями в более глубокой иерархии, например:


struct Company {    let name: String    let country: String}struct User {    let id: Int    let company: Company}let user = User(    id: 1,     company: Company(        name: "NeXT",         country: "USA"    ))

Чтобы в этом примере скопировать экземпляр user, изменив поле company.name, придется написать не самый приятный код:


let appleUser = user.changing { newUser in    newUser.company = newUser.company.changing { newCompany in        newCompany.name = "Apple"    }}

И чем глубже находится изменяемое свойство, тем больше уровней и строк займет его изменение.


Спокойно. Решение есть и очень простое-необходимо лишь добавить перегрузку сабскрипта в структуру ChangeableWrapper:


subscript<T: Changeable>(    dynamicMember keyPath: KeyPath<Wrapped, T>) -> ChangeableWrapper<T> {    get {        ChangeableWrapper<T>(self[dynamicMember: keyPath])    }    set {         self[dynamicMember: keyPath] = T(copy: newValue)    }}

Этот дополнительный сабскрипт вызывается только для свойств, тип которых соответствуют протоколу Changeable. Swift достаточно умен и в нашем случае не потребует каких-либо уточнений для выбора перегрузки. Поэтому мы получаем значение свойства через основной сабскрипт и возвращаем его, завернув в изменяемую обертку.


Такое небольшое дополнение позволяет изменять свойства на любом уровне вложенности, используя обычный синтаксис доступа через точку:


let appleUser = user.changing { newUser in    newUser.company.name = "Apple"}

Так вся конструкция копирования получается очень компактной и удобной, и вот теперь нашу реализацию можно назвать завершенной.



Подводя итог


Безусловно, лучше иметь подобный функционал копирования в составе самого языка, и, надеюсь, это произойдет в скором будущем. Сейчас же синтаксическая мощь Swift позволяет реализовать достаточно красивое решение самостоятельно, и оно лишено всех озвученных проблем своих альтернатив.


Единственным неудобством является ручная реализация инициализатора из копии. И если моделей в проекте много, их структура постоянно меняется, то имеет смысл автоматизировать этот труд. На этот случай есть готовый Stencil-шаблон для Sourcery, который доступен по ссылке.


Финальный код представленного решения, шаблон для кодогенерации и другие полезные вещи собраны в репозитории фреймворка, который легко интегрируется в любой проект, на Swift 5.1 и выше.


На этомвсе. Буду рад обратной связи в комментариях. Пока!

Источник: habr.com
К списку статей
Опубликовано: 22.07.2020 10:16:05
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании headhunter

Swift

Программирование

Разработка мобильных приложений

Разработка под ios

Xcode

Ios

Development

Autochangeable

Категории

Последние комментарии

  • Имя: Murshin
    13.06.2024 | 14:01
    Нейросеть-это мозг вселенной.Если к ней подключиться,то можно получить все знания,накопленные Вселенной,но этому препятствуют аннуннаки.Аннуннаки нас от неё отгородили,установив в головах барьер. Подр Подробнее..
  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru