Проблема
Как и многие iOS разработчики, я столкнулся с дилеммой: какой объект использовать для построения архитектуры проекта. Взять для примера реализацию паттерна фасад. Этот объект должен принять некоторое количество сущностей и реализовать методы для упрощенного доступа к ним. Если не вдаваться в подробности, то подойдет и класс, и структура: оба могут инкапсулировать объекты и функции. Так что же выбрать?
А что говорит Apple?
Если обратиться к статье на сайте Apple Choosing Between Structures and Classes, то следуя пункту "Use structures by default.", нужно использовать структуры в нашем случае. Если конечно не нужна Objective-C совместимость.
Тестовые объекты
Объекты наполнения для целевых объектов:
protocol ObjectForProperties { var queue: DispatchQueue { get } // reference type var bool: Bool { get } // value type } final class ClassForProperty: ObjectForProperties { var queue: DispatchQueue = .main var bool: Bool = true } struct StructForProperty: ObjectForProperties { var queue: DispatchQueue = .main var bool: Bool = true } var classForProperty = ClassForProperty() // reference type var structForProperty = StructForProperty() // value type var array = [1] // value type with Copy-on-Write
Целевые объекты:
// Используем класс без использования протоколов final class UseClassWithoutProtocols { var ref: ClassForProperty var value: StructForProperty var array: [Int] init(ref: ClassForProperty, value: StructForProperty, array: [Int]) { self.ref = ref self.value = value self.array = array } } // Используем класс с использованием протоколов final class UseClassWithProtocols { var ref: ObjectForProperties var value: ObjectForProperties var array: [Int] init(ref: ObjectForProperties, value: ObjectForProperties, array: [Int]) { self.ref = ref self.value = value self.array = array } } // Используем структуру без использования протоколов struct UseStructWithoutProtocols { var ref: ClassForProperty var value: StructForProperty var array: [Int] } // Используем структуру с использованием протоколов struct UseStructWithProtocols { var ref: ObjectForProperties var value: ObjectForProperties var array: [Int] } let useClassWithoutProtocols = UseClassWithoutProtocols( ref: classForProperty, value: structForProperty, array: array ) let useClassWithProtocols = UseClassWithProtocols( ref: classForProperty, value: structForProperty, array: array ) var useStructWithoutProtocols = UseStructWithoutProtocols( ref: classForProperty, value: structForProperty, array: array ) var useStructWithProtocols = UseStructWithProtocols( ref: classForProperty, value: structForProperty, array: array )
Размещение в памяти объектов
Структуры передаются копированием значения. У классов вместо копирования используется ссылка на существующий экземпляр. Для некоторых стандартных типов значения реализован механизм Copy-on-Write, который позволяет передавать объект передавая адрес и копировать только если объект изменяется. Но Copy-on-Write не реализован у структур по-умолчанию, то есть StructForProperty будет передаваться копированием в любом случае.
Проверим размещение объектов в памяти (опустим закрытые протоколом значения для чистоты эксперимента):
func address<T>(of o: UnsafePointer<T>) -> String { String(format: "%p", Int(bitPattern: o)) } func address<T: AnyObject>(of classInstance: T) -> String { String(format: "%p", unsafeBitCast(classInstance, to: Int.self)) } // classForProperty addresses print(address(of: classForProperty)) // 0x6000020e9600 print(address(of: useClassWithoutProtocols.ref)) // 0x6000020e9600 print(address(of: useStructWithoutProtocols.ref)) // 0x6000020e9600 // structForProperty addresses print(address(of: &structForProperty)) // 0x111a1bca8 print(address(of: &useClassWithoutProtocols.value)) // 0x600002edb6d8 print(address(of: &useStructWithoutProtocols.value)) // 0x111a1bcd8 // array addresses print(address(of: &array)) // 0x600002edb2f0 print(address(of: &useClassWithoutProtocols.array)) // 0x600002edb2f0 print(address(of: &useStructWithoutProtocols.array)) // 0x600002edb2f0
Что и требовалось доказать:
Ссылочный объект имеет только один экземпляр в памяти.
Тип значения имеет столько экземпляров в памяти, сколько раз он передавался.
Тип значения с Copy-on-Write имеет только один экземпляр в памяти.
Занимаемая память
В большинстве случаев структуры полностью хранятся в Stack, а классы хранятся в Heap, и ссылка на объект хранится в Stack. Stack имеет ограниченный размер. Heap не имеет ограничений по размеру. Из чего следует вывод, что память в Stack нужно беречь. Большая статья про память.
MemoryLayout.size(ofValue: useClassWithoutProtocols) // Размер в Stack 8 class_getInstanceSize(UseClassWithoutProtocols.self) // Общий размер 48 MemoryLayout.size(ofValue: useClassWithProtocols) // Размер в Stack 8 class_getInstanceSize(UseClassWithProtocols.self) // Общий размер 104 MemoryLayout.size(ofValue: useStructWithoutProtocols) // Размер в Stack 32 MemoryLayout.size(ofValue: useStructWithProtocols) // Размер в Stack 88
Если беречь память Stack, то очевидно нужно использовать классы. Если брать общий размер, то оптимальным решением будет использование структур без использования протоколов. Но в таком случае страдает тестируемость, принципы SOLID и конечно Протокольно-ориентированное программирование. Так же не стоит забывать про копирование: размер занимаемой память структурой прямо пропорционален количеству присваиваний объекта.
Время доступа
В теории время доступа к значениям класса дольше, так как переход по ссылкам требует большего времени. В то время как структуры находятся здесь и сейчас.
Замер получения булевой переменной в разных конфигурациях:
func countTime(_ workItem: () -> Void) -> Double { let start = CFAbsoluteTimeGetCurrent() workItem() return CFAbsoluteTimeGetCurrent() - start } // для примера // повторить для разных конфигураций countTime { let _ = useClassWithoutProtocols.ref.bool }
Я провел несколько тестов и получил следующие средние значения:
Время доступа к ссылочному типу | Время доступа к типу значения | |
useClassWithoutProtocols | 0,000009597 | 0,000007105 |
useClassWithProtocols | 0,000009501 | 0,000008715 |
useStructWithoutProtocols | 0,000008059 | 0,000008597 |
useStructWithProtocols | 0,000007011 | 0,000007714 |
Значения примерно равнозначны. Однако стоит отметить, что использования структуры со значениями ссылочных типов, закрытых протоколами, незначительно экономит время доступа.
Выводы
Stack | Heap | Время | Принципы | |
useClassWithoutProtocols | + | - | - | - |
useClassWithProtocols | + | - | + | |
useStructWithoutProtocols | - | + | _ | |
useStructWithProtocols | - | + | + | + |
Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.
Использовать классы хорошо для Stack.
Так как важно следить за Stack и соблюдать принципы, а время доступа незначительно отличается, я бы советовал использовать классы и протоколы для построения архитектуры.
