Проблема
Как и многие 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 и соблюдать принципы, а время доступа незначительно отличается, я бы советовал использовать классы и протоколы для построения архитектуры.