При разработке современных приложений — особенно в условиях быстро изменяющихся пользовательских интерфейсов и сложных моделей данных — нередко требуется сравнивать два объекта и находить различия между ними. Эта задача называется diffing, и один из мощных подходов к её реализации — reflection-based diffing algorithm, то есть диффинг на основе рефлексии. В этой статье мы разберем, что это за подход, как он работает, и покажем примеры его применения на языке Swift.
Что такое «diffing»?
Diffing — это процесс сравнения двух структур (объектов, массивов, словарей и т.д.) для определения различий между ними. Он широко используется, например:
- В виртуальных DOM, чтобы определить, какие части UI нужно перерисовать.
- В системах сериализации, чтобы сохранить только измененные данные.
- В тестировании, когда нужно проверить, как изменилось состояние модели.
- В анализе данных, когда важно отслеживать разницу между предыдущим и текущим состоянием.
Что такое рефлексия?
Рефлексия (reflection) — это механизм, позволяющий программе изучать и взаимодействовать со своей собственной структурой во время выполнения. В Swift она реализована через протокол Mirror, который позволяет получить доступ к полям и значениям экземпляра произвольного типа.
Что такое reflection-based diffing?
Reflection-based diffing algorithm — это алгоритм, использующий возможности рефлексии для сравнения двух объектов без знания их внутренней структуры на этапе компиляции. То есть программа во время выполнения «заглядывает» внутрь объектов, сравнивает их поля и фиксирует различия.
Пример: сравнение объектов на Swift через рефлексию
Рассмотрим простую модель:
struct User {
var id: Int
var name: String
var age: Int
}
Мы хотим сравнить два объекта типа User и получить список отличающихся полей.
Используем Mirror
Swift предоставляет протокол Mirror, с помощью которого мы можем изучить свойства структуры:
func diff<T>(_ lhs: T, _ rhs: T) -> [String: (Any, Any)] {
var differences: [String: (Any, Any)] = [:]
let mirror1 = Mirror(reflecting: lhs)
let mirror2 = Mirror(reflecting: rhs)
for (child1, child2) in zip(mirror1.children, mirror2.children) {
guard let label = child1.label else { continue }
if "\(child1.value)" != "\(child2.value)" {
differences[label] = (child1.value, child2.value)
}
}
return differences
}
Пример использования:
let oldUser = User(id: 1, name: "Alice", age: 30) let newUser = User(id: 1, name: "Alice", age: 31) let changes = diff(oldUser, newUser) print(changes) // Вывод: ["age": (30, 31)]
Преимущества reflection-based diffing
- Гибкость — работает с любыми типами без ручного описания логики сравнения.
- Меньше кода — не нужно писать отдельные функции сравнения для каждой структуры.
- Автоматизация — удобно использовать в фреймворках, сериализации и дифферах.
Недостатки и ограничения
Несмотря на удобство, такой подход имеет свои минусы:
1. Производительность
Рефлексия в Swift работает медленнее, чем прямой доступ к полям. Если вы сравниваете большое количество объектов в реальном времени — это может стать узким местом.
2. Ограниченные типы
Типы, не поддерживающие Mirror, или приватные свойства могут быть недоступны. Также значения сравниваются через приведение к строке ("\(value)" != "\(otherValue)"), что может быть не очень точно.
3. Глубокое сравнение
Если объект содержит вложенные объекты (например, Address внутри User), их нужно сравнивать рекурсивно. В простом diff выше это не реализовано.
Расширение для рекурсивного сравнения
Добавим поддержку вложенных структур:
func deepDiff<T>(_ lhs: T, _ rhs: T, prefix: String = "") -> [String: (Any, Any)] {
var result: [String: (Any, Any)] = [:]
let mirror1 = Mirror(reflecting: lhs)
let mirror2 = Mirror(reflecting: rhs)
for (child1, child2) in zip(mirror1.children, mirror2.children) {
guard let label = child1.label else { continue }
let key = prefix.isEmpty ? label : "\(prefix).\(label)"
let value1 = child1.value
let value2 = child2.value
let type1 = Mirror(reflecting: value1).displayStyle
let type2 = Mirror(reflecting: value2).displayStyle
if type1 == .struct || type1 == .class {
let nestedDiffs = deepDiff(value1, value2, prefix: key)
for (k, v) in nestedDiffs {
result[k] = v
}
} else if "\(value1)" != "\(value2)" {
result[key] = (value1, value2)
}
}
return result
}
Пример:
struct Address {
var city: String
var zip: String
}
struct UserProfile {
var name: String
var address: Address
}
let user1 = UserProfile(name: "Anna", address: Address(city: "Moscow", zip: "12345"))
let user2 = UserProfile(name: "Anna", address: Address(city: "Saint Petersburg", zip: "12345"))
let changes = deepDiff(user1, user2)
print(changes)
// Вывод: ["address.city": ("Moscow", "Saint Petersburg")]
Где применяется reflection-based diffing?
1. SwiftUI / DiffableDataSource
В SwiftUI изменения модели автоматически вызывают перерисовку интерфейса. Для этого используется сравнение данных — иногда на основе идентификаторов, а иногда через глубокий diff.
2. Синхронизация данных
Приложения, которые синхронизируют данные с сервером (например, мессенджеры или CRM-системы), могут использовать диффинг для отправки только измененных полей.
3. Логирование и аудит
При сохранении истории изменений можно использовать рефлексивный диффинг, чтобы автоматически сохранять, какие поля объекта были изменены.
Заключение
Reflection-based diffing algorithm — это мощный инструмент для анализа различий между объектами на лету. В языке Swift благодаря Mirror можно легко реализовать базовый и даже рекурсивный диффинг. Однако, как и любое универсальное решение, он требует осторожности: важно учитывать производительность, ограничения типов и возможные ошибки при сравнении сложных структур.
Для небольших моделей и вспомогательных задач (например, логирования, отладки или тестирования) такой подход является удобным и гибким. А в более сложных случаях может быть хорошей основой для создания собственных систем сравнения данных.

