Мудрецу, который спрятал свое лучшее изречение, следует отсечь руки: ибо он – вор, и украл чужую мудрость.
autoreleasepool
@NSCopying
IndexSet
NSNull
lldb
Thread
@_specialize
while-let
и while-var
if-var
Foundation припас для нас множество скрытых возможностей, которые мы дотащили до текущего урока.
В общем случае главная функция приложения помещает в себя хотя бы один пул автовысвобождения. Но есть набор ситуаций, когда Вам потребуется вручную создать такой пул. Сам по себе он представляет участок кода, объекты верхнего уровня в котором будут подвержены проверке ARC мгновенно.
Рассмотрим пример:
AppKit
func test() {
let url = URL(string: "http://www.hotelroomsearch.net/im/city/saint-anne-seychelles-0.jpg")!
//Создаём ссылку на файл картинки с островами
for _ in 0...1000 {
let image = NSImage(contentsOf: url)
}
//Создаем саму картинку в цикле
print("Near to the end")
//На эту инструкцию повесьте breakpoint
}
test()
Запустите приложение.
На этом участке кода Вы потеряете 192 мегабайта оперативной памяти. Заметьте особенность - объекты уже не доступны, но ещё не удалены из памяти. Это не утечка памяти - память будет корректно очищена при выходе из функции. Казалось бы, величина небольшая, но это полезная память, которая тратится впустую.
Проблема решится, если обернуть цикл в;
autoreleasepool {
}
Потребление памяти сразу же сократится:
Закомментируйте вызов функции прежде, чем идти дальше.
Существует ещё один специальный атрибут для декларации переменных, при использовании которого происходит не создание указателя на объект при присваивании, а его копирование.
Сперва реализуем класс для применения свойства:
Dog
для отображения животногоname
для имениclass Dog {
var name: String
init(name: String) {
self.name = name
}
}
А в расширении реализуйте уже знакомый протокол NSCopying
для копирования объекта:
extension Dog: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return Dog(name: name)
}
}
class Human {
@NSCopying var dog: Dog?
}
let human = Human()
//Человек
let dog = Dog(name: "Vasya")
//Собака
human.dog = dog
//Присваиваем собаку человеку
dog.name = "Petya"
//И меняем имя исходного объекта
print(human.dog?.name) //Vasya
Как видно из вывода, конечный объект остался без изменений, так как благодаря атрибуту было произведено копирование с созданием полностью нового объекта, а не добавление очередной ссылки.
Помимо индексного пути и интервалов существует возможность задавать индексы через множество:
let arr = ["zero", "one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten"]
var ixs = IndexSet()
ixs.insert(integersIn: Range(1...4))
ixs.insert(integersIn: Range(8...10))
//Массив индексов содержит числа 1, 2, 3, 4, 8, 9, 10
let arr2 = (arr as NSArray).objects(at: ixs)
// ["one", "two", "three", "four", "eight", "nine", "ten"]
//А значит и результат будет таким же
Он позволяет через NSArray
получить объект на позициях из множества. Отличие от промежутка состоит в том, что числа в индекс-множестве не должна образовывать непрерывную последовательность.
Допустим, у нас есть коллекций, в которой нужно показать отсутствие значений, но мы не можем этого сделать, так как в него нельзя добавить nil
.
Самый простой пример - NSArray
при бриджинге в Swift даёт нешаблонный класс с элементом типа Any
. А на место Any
мы не можем добавить nil
:
let array: NSArray = [1, 2, 3, 4, nil] //Ошибка
Закомментируйте эту строку.
Но проблема решается с помощью NSNull
:
let array: NSArray = [1, 2, 3, 4, NSNull()]
print(array)
Он используется для мест, где ожидается объект, но его нет.
Это специальный синглтон-объект, что значит, что создание объекта не создаёт новых объектов - во всём приложении будет существовать только один такой объект:
print(NSNull() === NSNull()) //true
Относитесь к нему, как к некой форме nil
, например, при фильтрации массива для оставления лишь только действительных объектов:
print(array.filter { $0 as? NSNull == nil}) //[1, 2, 3, 4]
Здесь мы отобразим несколько новых команд отладчика lldb, которые могут оказаться полезными.
Для начала поставьте breakpoint
на строке: print(NSNull() === NSNull()) //true
.
Запустите программу: Теперь в консоли можно ввести команды:
fr variable array
Эта команда снимет кадр переменной в текущий момент времени.
(NSArray) array = 0x0000606000063b00 5 elements {
[0] = 0x0000000000000137 Int64(1)
[1] = 0x0000000000000237 Int64(2)
[2] = 0x0000000000000337 Int64(3)
[3] = 0x0000000000000437 Int64(4)
[4] = 0x00007fff97a4ee30
}
При этом отобразится информация об адресе объектов и их истинном типе и значении.
При вызове без аргументов возвращаются снимки всех объектов в текущем контексте.
Можно получить полную декларацию любого типа с помощью ty loo
(или его полного аналога):
type lookup Human
Получим следующее:
class Human {
@NSCopying var dog: <@null>
@objc deinit
init()
}
Заметьте, что был создан автоматический деструктор deinit
, экспозируемый в рантайм objc
.
Можно вычислять выражения прямо в строке управления с помощью команды p
, e
, expr
или expression
:
(lldb) e
Enter expressions, then terminate with an empty line to evaluate:
1 array = []
2
Вы можете пользоваться любые данными из текущего контекста, а значит и готовьтесь к побочным эффектам - нажмите продолжить выполнение и Вы получите уже просто пустой массив, а не отфильтрованный, так как мы заменили изначальный на пустой:
[]
po
- распечатывает объект в соответствии с его description
или debugdescription
:
(lldb) po array
▿ 5 elements
- 0 : 1
- 1 : 2
- 2 : 3
- 3 : 4
- 4 : <@null>
Удобная же штука однако!
Чтобы получить такое отображение в программе возможно использовать рефлексию:
print(String(reflecting: array))
Здесь используется зеркальное (Mirror
) отображение объекта. Которое отображается в отладчике или игровой площадке.
Вы можете наделить свой класс данной способностью с помощью протокола
CustomReflectable
. С помощью рефлексии возможно выполнить обратное преобразование из зеркала в объект, однако на практике это очень дорогая операция. В случаях, где Вам нужна рефлексия - часто будет достаточно обычной сериализации и динамического диспатча.
Так же есть команды next
и nexti
, позволяющие перемещаться по ходу выполнения программы. Введя одну из них, Вы перейдёте к инструкции по фильтрации массива.
Давайте рассмотрим ситуацию: у нас есть указатель на область из трёх объектов CGFloat
:
let b = UnsafeMutablePointer<@CGFloat>.allocate(capacity: 3)
Теперь заполним его:
b.initialize(from: [0.1, 0.2, 0.3])
У таких указателей есть сабскрипты, которые позволяют получить объект по заданной позиции:
print(b[2]) //0.3
Но они не содержат никаких проверок на наличие индекса - это не массив!
print(b[4]) //Propbably 4.17201261968425e-308
Результат может быть любым - мы просто сделали смещение на 2 объекта относительно последнего по указателю и попробовали собрать из байт на ячейке памяти наш объект - для простых объектов это очень часто удаётся.
Но что ещё хуже - мы можем записать в эту область что-то своё:
b[4] = 100
print(b[4]) //100
Всё замечательно, наш объект есть на месте. Но... участок памяти не принадлежит нам. Здесь могут быть две плохие ситуации:
Отследить эти проблемы можно с помощью диагностики Adress Sanitizer
. Включается он в разделе с остальными диагностиками. Сделайте это!
Другой способ решения проблемы - использование
UnsafeMutableBufferPointer
, который специально предназначен для хранения объектов-массивов.
Теперь при запуске программы ещё на этапе чтения памяти мы получим ошибку:
//AddressSanitizer: heap-buffer-overflow on address
Программа будет остановлена и никаких последствий не произойдёт!
И хотя данная система хорошо отслеживает невалидные адреса при использовании слишком динамической арифметики указателей и малом наборе тестов, Вы всё равно сохраните за собой риск получения неопределённого поведения. Лучший выход - не использовать такой низкоуровневый код: извлекаемая из этого производительность не стоит затрат ресурсов.
Закомментируйте код этого параграфа.
У таймера существует понятие толерантности или времени, которое может добавить система к стартовой дате для целей оптимизации потребления ресурсов.
В приложениях с графическим интерфейсом или игровой площадке постоянно существует цикл, который принимает события и подхватывает создаваемые таймеры. Получить к нему доступ можно с помощью RunLoop.main
. Однако в консольном приложении данный цикл по-умолчанию находится в неактивном состоянии. Его запуск приведёт к блокировке остального кода после его запуска, потому мы создадим свой ран-луп:
var runloop: RunLoop?
Чтобы избежать блокировки, мы создадим отдельный поток. Раньше мы использовали для этого очередь, но в целях расширения кругозора мы создадим поток явно с помощью класса Thread
:
Thread {
}.start()
while true {
}
start
асинхронно выполняет поток в выполнениеОтличие потоков от очередей в том, что он может занимать лишь один физический поток, тогда как балансировщик нагрузки сам генерирует создание потоков и их синхронизацию.
Теперь будем последовательно добавлять внутрь метода код.
Давайте создадим объект таймера, который будет выводить время, прошедшее с изначального:
let startDate = Date()
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
print(startDate.timeIntervalSinceNow)
}
timer.tolerance = 20
Несмотря на то, что изначальное значение равно
0
, система может добавлять свои значения толерантности к времени старта, не меняя самого свойства, неявно.
А теперь получим цикл выполнения текущего потока:
runloop = RunLoop.current
Именно для этого мы и создавали внешнюю переменную - без неё объект цикла был бы уничтожен с выходом из потока, а так будет создана сильная ссылка на него.
А теперь добавим в него таймер и запустим выполнение:
runloop?.add(timer, forMode: .defaultRunLoopMode)
runloop?.run()
Теперь каждый запуск приложения будет выводить Вам разные значения старта таймера, так как мы позволили ему более вариативный запуск.
Закомментируйте код этого параграфа.
В Swift шаблонные типы или дженерики формируются не так, как во многих языках: в C++, к примеру, они представляют собой набор кода в заголовочных файлах, а потому реально код создаётся для шаблонных типов в процессе компиляции. Потому Вы можете создать шаблонные типы на основе типов лишь в заголовочных файлов, что сильно увеличивает объем кода в них.
В Swift дженерики создаются в процессе выполнения с помощью динамического диспатча - с помощью виртуализации и прочих механизмов тип или отдельная функция создаются при обращении к нему.
Можно добавить немного оптимизации в систему, используя атрибут @_specialize
, явно указав, какие вариации функции должен создать компилятор при сборке кода.
Этот атрибут является внутренним. И хотя он полностью функционален в
Swift 4.1
, Apple не даёт никаких гарантий, что с ним ничего не случится в будущем. Однако он не относится и к приватномуAPI
, что запретило бы его использование в программах для распространения через официальные каналы.
Создадим сперва простую шаблонную функцию, складывающую два числа одного типа:
func sum<@T: Numeric>(_ lhs: T, _ rhs: T) -> T {
return lhs + rhs
}
А над ней допишем:
@_specialize(exported: false, kind: full, where T == Int)
false
where
, где и задаются аргументы инициализации. Это такой же блок, как и обычный блок where
.print(sum(1, 2)) //3
print(sum(1.0, 2.0)) //3.0
Разница между этими двумя функциями в том, что первая будет создана во время компиляции, а вторая с помощью динамического диспатча.
Помимо опускания лейбла аргумента, можно опустить его внутренне значение, или же и то, и другое:
func doSomethingWith(_: Int) {
print("Joke. I don't do anything")
}
doSomethingWith(1)
Такой аргумент не будет доступен внутри функции, однако это может повысить читаемость функции. Хотя если Вы используете такой подход, то это скорее сигнал о проблемах с архитектурой приложения.
Может быть ситуация, когда две функции имеют одинаковую сигнатуру, но разный тип возвращаемых значений. Попробуйте создать две таких функции самостоятельно:
doSomething
1
с явным типо Int
2.0
с явным типом Double
func doSomething() -> Int {
return 1
}
func doSomething() -> Double {
return 2.0
}
Как же система сможет разделить их? Да никак:
let result0 = doSomething() //Ambiguous use of 'doSomething()'
Закомментируйте строку.
Можно решить проблему, явно задав тип для функции:
let result1: Int = doSomething() //1
Если нет надобности создавать промежуточные значения, то можно использовать приведение типов оператором as
:
print(doSomething() as Double) //2.0
Помимо if-let
существует возможность создания цикла while
, проверяющего свои объекты и извлекающие их, если они не-nil
.
Для использования этого цикла извлечем итератор из массива:
var iterator = array.makeIterator()
Это специальный объект, который позволяет обойти массив, вызывая метод next
,
Однако сам метод возвращает опциональное значение (на конце массива элемента может не быть, ровно как его может не быть вообще), потому очень логично совершать цикл, пока элементы есть:
while let item = iterator.next() {
print(item)
}
Это именно метод, а не свойство, так как оно изменяет структуру своего итератора.
Вы уже знакомы со множеством конструкций по извлечению объектов из опционально значения в неопциональные константы, но есть так же возможность произвести извлечение в переменные.
Создадим опциональное число:
let number: Float80? = 1
Мы использовали новый для нас тип данных Float80
, отражающий число повышенной точности (80-битная разрядность)
Извлечём число:
if var rawNumber = number {
rawNumber = rawNumber.nextUp
//И заменим его следующим возможным числом
print(rawNumber > number!) //true
}
Под возможным числом здесь понимается большее или меньшее число, между которым и заданным числом нельзя расположить другого.
Если вывести исходное число, то оно останется неизменным:
print(number) //1.0
В данном случае просто создаётся копия числа и исходный объект изменён не будет. Однако используйте переменное-связывание лишь когда это необходимо. В случаях с константным связыванием реальной копии не произойдёт.
Для типов-ссылок такой метод извлечения ничего по сути не меняет - Вы и так можете изменять объекты по указателю, однако в таком извлечении Вы сможете изменить сам объект, на который идёт ссылка. Необходимость в этом возникает крайне редко.
Таким же образом работает цикл while-var
.
Давайте представим ситуацию, когда объект типа содержит в себе поле с объектом такого же типа:
struct InHuman {
var parent: InHuman?
}
И уже на этом этапе мы получим ошибку:
Value type 'InHuman' cannot have a stored property that recursively contains it
Это связано с тем, что если бы такая реакция была бы дозволена, то каждый новый объект в неё копировался бы. Это породило бы сильную путаницу и усложнило бы отладку.