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

Меня зовут Светлана Гладышева, я iOS-разработчик в Тинькофф. Расскажу, какие возможности есть у Reflection в Swift, в чем ограничения и подводные камни. Рассмотрим его применение на примерах и узнаем, для чего его можно использовать в повседневной работе. А еще поговорим о том, как можно отключить Reflection в проекте и на что это может повлиять.

Просмотр содержимого разных типов

Для просмотра содержимого сущностей в Swift используется специальная структура — Mirror. Использовать Mirror можно для любых типов: структур, классов, перечислений, коллекций и так далее. Создать Mirror можно так:

let mirror = Mirror(reflecting: item)

У Mirror есть поле Children, с помощью которого можно просматривать содержимое сущности. Children — это коллекция, состоящая из элементов Child. У каждого Child есть Label, название элемента, и Value — значение, которое в нем содержится. С помощью Children можно просматривать содержимое разных типов.

 Для структур. Предположим, у нас есть структура User и экземпляр этой структуры:

struct User {
    let name: String
    let age: Int
}

let user = User(name: "Nikita", age: 25)

Мы можем посмотреть, что находится внутри User, с помощью специальной структуры — Mirror:

let mirror = Mirror(reflecting: user)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

В консоль выведется:

Мы узнали, что внутри User есть поле Name со значением Nikita и поле Age со значением 25. Важно, что мы можем посмотреть только поля, — а методы объектов нет. Зато можем увидеть все поля — и публичные, и приватные.

Для классов. Предположим, у нас есть класс Validator с двумя приватными полями:

class Validator {
    private let mode: ValidatorMode
    private let transformer: Transformer
    // …
}

ValidatorMode — это Enum с двумя вариантами: Simple и Complex, а Transformer — какой-то другой класс. Мы можем вывести информацию об объекте этого класса:

let mirror = Mirror(reflecting: validator)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

 В консоль выведется:

Так же как для структур, мы не можем посмотреть методы объектов, зато видим содержимое всех полей. Если внутри объекта лежит какой-то другой объект, выводится только тип объекта, но не содержимое. Если нужно узнать, что внутри такого объекта, можно создать Mirror для этого объекта и просмотреть у него Children.

Для Enum. Предположим, есть Enum ValidationType с двумя вариантами:

enum ValidationType {
    case email
    case phoneNumber(format: String, city: String)
}

Проверим код для кейса Email:

let mirror = Mirror(reflecting: ValidationType.email)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

В консоль не выведется ничего. Потому что для Enum в качестве Children у Mirror — Associated Values. У кейса Email нет ни одного Associated Value, поэтому коллекция Children для него пустая.

У кейса PhoneNumber есть два Associated Values: Format и City. Если мы запустим такой же код для этого кейса, в консоль выведется строка:

В качестве Label у Enum — название кейса, а Value — Tuple, содержащий все Associated Values в формате «имя: значение». Если же у кейса есть только один параметр, то в Value выводится он, без имени.

Остальные типы. Для массива можно просмотреть его элементы: в качестве Label будет Nil, а Value — значения элементов массива. Для Dictionary в качестве Label будут ключи, а в качестве Value — значения. В качестве примера я перечислила всего два типа, но так можно просматривать все.

Разные типы полей. Статические и computed-поля нельзя посмотреть с помощью Mirror, а вот значения Lazy полей можно. Предположим, у какого-то класса есть ленивое поле Transformer:

private lazy var transformer: Transformer = Transformer()

Если мы напишем так:

mirror.superclassMirror?.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

В консоли выведется:

К ленивым полям в child.name добавляется префикс ‘lazy_storage’.

Просмотр типов объектов и другие возможности

С помощью Mirror можно узнать не только содержимое объекта, но и его тип. Для этого у Mirror есть subjectType. Для структуры User из предыдущего раздела subjectType вернет тип User, а для класса Validator – тип Validator.

Еще у Mirror есть displayType — поле, которое показывает, как именно этот Mirror будет отображаться. DiplayStyle — это Enum, способный принимать значение Struct, Class, Enum, Tuple, Optional, Collection, Dictionary, Set и так далее. С помощью displayType мы можем узнать, как именно будет отображаться сущность: как класс, как структура или как что-то другое.

Если класс наследуется от другого класса, Children нам покажет только те поля, которые объявлены внутри самого этого класса. Если мы хотим посмотреть значения полей, которые объявлены в базовом классе, это можно сделать с помощью SuperclassMirror:

mirror.superclassMirror?.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

Может случиться, что у базового класса есть свой базовый класс. Получить его поля можно, используя конструкцию mirror.superclassMirror?.superclassMirror?.children.  

Чтобы получить абсолютно все поля класса, можно написать код:

var mirror: Mirror? = Mirror(reflecting: item)
repeat {
    mirror?.children.forEach { child in
        print("Label: \(child.label), value: \(child.value)")
    }
    mirror = mirror?.superclassMirror
} while mirror != nil

Поиск с помощью метода Descendant. Необязательно писать какой-то сложный код, когда нужно найти у объекта определенное поле. У Mirror есть метод Descendant, который позволяет получить значение поля с помощью MirrorPath. В качестве MirrorPath можно передать либо строку — название поля, либо Int — порядковый номер поля.

Предположим, есть две структуры:

struct User {
    let name: Name
    let age: Int
}

struct Name {
    let firstName: String
    let secondName: String
}

Создав Mirror для экземпляра User, мы можем получить значение SecondName с помощью метода Descendant:

let secondName = mirror.descendant("name", "secondName")

Или с использованием порядковых номеров нужных нам полей:

let secondName = mirror.descendant(0, 1)

Изменение представления об объекте

В Swift есть протокол CustomReflectable, который позволяет подменять mirror-объект на кастомный. Это делают, если объект Mirror по каким-то причинам не устраивает.

Нужно сделать так, чтобы сущность соответствовала протоколу CustomReflectable, чтобы изменить представление о ней. Для CustomMirror создается свой объект Mirror и в нем указывается Children со словарем вида ["label": value].

Для примера возьмем структуру Point:

struct Point {
    let x: Int
    let y: Int
}

Мы можем сделать так, чтобы эта структура соответствовала протоколу CustomReflectable, и добавить ей СustomMirror:

extension Point: CustomReflectable {
    var customMirror: Mirror {
        Mirror(self, children: ["z": x + y])
    }
}

Создадим и выведем в консоль информацию о каком-то экземпляре этой структуры:

let point = Point(x: 10, y: 12)
let mirror = Mirror(reflecting: point)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

Результат в консоли:

В консоль вывелось именно то, что было указано в СustomMirror. При этом информацию про настоящие поля структуры x и y мы не получили.

При вызове Mirror(reflecting: point) для типа, который соответствует протоколу CustomReflectable, будет использован именно СustomMirror из CustomReflectable. Так, используя Reflection в Swift, мы можем получать не совсем правдивую информацию про имеющиеся у объекта поля.

Можно при создании CustomMirror указать DisplayStyle и AncestorRepresentation. С помощью AncestorRepresentation можно создать CustomMirror, который будет отображаться в качестве SuperclassMirror. А еще с его помощью можно запретить этому Mirror отображать информацию о суперклассах.

Примеры использования Reflection

С валидацией. Предположим, у нас есть класс Item, который содержит в себе много полей типа String:

struct Item {
    let field1: String
    let field2: String
    let field3: String
    let field4: String
    // …
}

Мы хотим проверить, что в объекте Item все поля не пустые, то есть ни в одном из полей нет пустой строки. Напишем для этого валидатор:

class ItemValidator {
    func validate(item: Item) -> Bool {
        return !item.field1.isEmpty && !item.field2.isEmpty && !item.field3.isEmpty && !item.field4.isEmpty // …
    }
}

Если полей у Item много, проверка внутри метода Validate будет длинной и громоздкой. Более того, при добавлении в Item нового поля можно забыть про необходимость правки метода Validate, и тогда валидатор будет работать неправильно.

Перепишем метод Validate с использованием Reflection:

func vaildate(item: Item) -> Bool {
    let mirror = Mirror(reflecting: item)
    for child in mirror.children {
        if let stringValue = child.value as? String {
            if stringValue.isEmpty {
                return false
            }
        }
    }
    return true
}

Теперь в методе Validate нет длинной проверки и при добавлении в Item нового поля не нужно будет изменять этот метод.

У подхода с переписыванием метода есть и минусы. Такая проверка неочевидна другим разработчикам. Поэтому другой разработчик, не зная про использование Reflection в методе валидации, может внести изменения, которые приведут к неправильной работе программы. Например, он может добавить в Item новое поле, которое не нужно проверять, но в методе Validate оно будет проверяться.

С тестами. Предположим, у нас есть MainViewConroller, в котором есть кнопка saveButton:

class MainViewConroller: UIViewController {

    private let saveButton: UIButton

    // ...

    func updateView() {
        // ...
        saveButton.isEnabled = false
    }
}

Напишем тест на метод updateView, в котором проверим, что кнопка становится задизейбленной:

func testUpdateView() {
    let mainViewContoller = MainViewConroller()
    mainViewContoller.updateView()

    XCTAssertFalse(mainViewConroller.saveButton.isEnabled)
}

Но этот код не скомпилируется, потому что поле saveButton приватное. Можно сделать saveButton не приватным, и тогда тест будет работать, но делать поля публичными только для тестов — не лучшее решение.

Напишем метод, который будет получать нужную нам кнопку с помощью Reflection:

func getSaveButton(from mainViewConroller: MainViewConroller) -> UIButton? {
    let mirror = Mirror(reflecting: mainViewConroller)
    if let saveButton = mirror.descendant("saveButton") as? UIButton?  {
        return saveButton
    }
    return nil
}

Перепишем тест, используя написанный метод:

func testUpdateView() {
    let mainViewContoller = MainViewConroller()
    mainViewContoller.updateView()

    let saveButton = getSaveButton(from: mainViewContoller)
    XCTAssertEqual(saveButton?.isEnabled, false)
}

Мы смогли написать тест, оставив кнопку приватной.

Другие примеры использования. Reflection может быть полезен в тех случаях, когда нужно узнать тип объекта, значение приватных полей или получить доступ ко всем полям объекта.

С помощью Reflection можно узнать, как устроены объекты из подключенных библиотек, чтобы лучше разобраться в том, как они работают. Например, можно посмотреть, какие приватные поля есть у объектов из Foundation, UIKit или SwiftUI.

Отключение Reflection

Для отключения Reflection в настройках проекта нужно изменить значение Reflection Metadata Level. Можно в Build Settings изменить значение пункта Reflection Metadata Level или в файле plist.info указать нужное значение для SWIFT_REFLECTION_METADATA_LEVEL.

В Reflection Metadata Level есть три уровня:

All — все включено. Without Names — имена полей не будут отображаться, значения Label всегда будут Nil. None — Reflection полностью выключен
All — все включено. Without Names — имена полей не будут отображаться, значения Label всегда будут Nil. None — Reflection полностью выключен

Можно отключить Reflection с помощью флагов -disable-reflection-metadata и -disable-reflection-names.

При отключении Reflection важно знать о том, что дебаггер использует этот механизм для своей работы. Например, функция Dump использует Reflection, чтобы отображать содержимое сущностей. LLDB использует Reflection Metadata для получения информации о типах. Memory Graph Debugger также использует Reflection Metadata для своих нужд. Из-за этого при отключении Reflection все перечисленные инструменты могут перестать корректно работать.

Отключение Reflection может привести и к другим последствиям. Например, перестает корректно работать @Published в SwiftUI. Меняется описание объекта при использовании String(reflecting: ...) и String(describing: ...). Перестает корректно работать интерполяция значений Enum: при попытке напечатать в консоль значение с помощью конструкции print("\(someEnum)") вместо названия кейса мы получим результат вида "SomeEnum(rawValue: 2131241)".

К счастью, можно устанавливать разный Reflection Metadata Level для разных таргетов в проекте. Например, можно включить Reflection в таргете для тестов, а в основном таргете выключить. Есть возможность включать или выключать Reflection для разных модулей в многомодульном приложении.

Настройка Reflection Metadata Level содержит слово metadata потому, что Swift Runtime хранит метаданные для каждого типа, используемого в программе. В них содержится полезная информация об этом типе. Например, для классов и структур в их метаданных можно найти имена полей и их типы. Из этих метаданных Reflection и получает всю нужную информацию, с помощью которой определяется содержимое объектов.

Заключение

Reflection в Swift работает как read-only. Невозможно изменять сущности в Runtime — можно только просматривать их содержимое. По сравнению с другими языками программирования, например Java, такие возможности Reflection кажутся сильно ограниченными. Но ограничение было введено намеренно, поскольку разработчикам языка Swift было важно, чтобы этот язык был безопасным.

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

Пара полезных ссылок на прощание: 

Комментарии (3)


  1. Barhishtan
    18.04.2024 05:41

    Спасибо за статью :)

    А вы сами в проекте пользуетесь рефлексией в тестах, для доступа к приватным полям, как написано в статье?

    Слышал мнение, что это грязноватый подход.


    1. Aksiomka Автор
      18.04.2024 05:41

      Мы в проекте не пользуемся Reflection в тестах. Соглашусь с тем, что этот подход можно назвать грязноватым. Поэтому, как я уже писала в статье, каждый решает сам, использовать ли Reflection в своем проекте.


      1. Barhishtan
        18.04.2024 05:41

        Как раз дискутируем с командой на этот счет)
        Спасибо за ответ ☺️