Проблема

Как и многие 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

-

+

+

+

  1. Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.

  2. Использовать классы хорошо для Stack.

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