Foundation 2 / 1.1. Интересные особенности Swift



Мудрецу, который спрятал свое лучшее изречение, следует отсечь руки: ибо он – вор, и украл чужую мудрость.

Мудрец с руками

В этом уроке


Trivia

Foundation припас для нас множество скрытых возможностей, которые мы дотащили до текущего урока.


autoreleasepool

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

Рассмотрим пример:

  1. Создайте консольное приложение для macOS
  2. В нём в главном файле импортируйте AppKit
  3. Теперь создадим функцию:
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 {

}

Потребление памяти сразу же сократится:

Закомментируйте вызов функции прежде, чем идти дальше.


@NSCopying

Существует ещё один специальный атрибут для декларации переменных, при использовании которого происходит не создание указателя на объект при присваивании, а его копирование.

Сперва реализуем класс для применения свойства:

  1. Класс Dog для отображения животного
  2. Строковое свойство name для имени
  3. Соответствующий инициализатор
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

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


IndexSet

Помимо индексного пути и интервалов существует возможность задавать индексы через множество:

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


NSNull

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

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


Adress Sanitizer

Давайте рассмотрим ситуацию: у нас есть указатель на область из трёх объектов 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

Всё замечательно, наш объект есть на месте. Но... участок памяти не принадлежит нам. Здесь могут быть две плохие ситуации:

  1. Мы решим воспользоваться им в будущем, а его перезапишет что-то другое.
  2. Или что ещё хуже - в участок уже чему-то принадлежал, а мы его испортили. Никаких ошибок, никаких видимых проблем - ровно до тех пор, пока владелец участка не обратиться к нему. Это определённо неопределённое поведение.

Отследить эти проблемы можно с помощью диагностики Adress Sanitizer. Включается он в разделе с остальными диагностиками. Сделайте это!

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

Теперь при запуске программы ещё на этапе чтения памяти мы получим ошибку:

//AddressSanitizer: heap-buffer-overflow on address

Программа будет остановлена и никаких последствий не произойдёт!

И хотя данная система хорошо отслеживает невалидные адреса при использовании слишком динамической арифметики указателей и малом наборе тестов, Вы всё равно сохраните за собой риск получения неопределённого поведения. Лучший выход - не использовать такой низкоуровневый код: извлекаемая из этого производительность не стоит затрат ресурсов.

Закомментируйте код этого параграфа.


Толерантность таймера

У таймера существует понятие толерантности или времени, которое может добавить система к стартовой дате для целей оптимизации потребления ресурсов.

В приложениях с графическим интерфейсом или игровой площадке постоянно существует цикл, который принимает события и подхватывает создаваемые таймеры. Получить к нему доступ можно с помощью RunLoop.main. Однако в консольном приложении данный цикл по-умолчанию находится в неактивном состоянии. Его запуск приведёт к блокировке остального кода после его запуска, потому мы создадим свой ран-луп:

var runloop: RunLoop?

Чтобы избежать блокировки, мы создадим отдельный поток. Раньше мы использовали для этого очередь, но в целях расширения кругозора мы создадим поток явно с помощью класса Thread:

Thread {

}.start()

while true {

}
  1. Данная команда создаёт новый поток.
  2. Команда start асинхронно выполняет поток в выполнение
  3. Так как дальше у нас нет кода, мы создаём бесконечную бесполезную нагрузку с помощью не менее бесполезного цикла

Отличие потоков от очередей в том, что он может занимать лишь один физический поток, тогда как балансировщик нагрузки сам генерирует создание потоков и их синхронизацию.

Теперь будем последовательно добавлять внутрь метода код.

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

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)
  1. Первый аргумент позволяет экспортировать специализацию во внешние модули. По-умолчанию - false
  2. Второй - выбор полной или частичной специализации - полная требует явного указания типов для всех аргументов шаблона, частичная позволяет частичную. По-умолчанию требуется полная специализация.
  3. Последним аргументом идёт блок 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)

Такой аргумент не будет доступен внутри функции, однако это может повысить читаемость функции. Хотя если Вы используете такой подход, то это скорее сигнал о проблемах с архитектурой приложения.


Особый тип перезгруки функций

Может быть ситуация, когда две функции имеют одинаковую сигнатуру, но разный тип возвращаемых значений. Попробуйте создать две таких функции самостоятельно:

  1. Название doSomething
  2. Первая возвращает число 1 с явным типо Int
  3. Вторая 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

while-let

Помимо if-let существует возможность создания цикла while, проверяющего свои объекты и извлекающие их, если они не-nil.

Для использования этого цикла извлечем итератор из массива:

var iterator = array.makeIterator()

Это специальный объект, который позволяет обойти массив, вызывая метод next,

Однако сам метод возвращает опциональное значение (на конце массива элемента может не быть, ровно как его может не быть вообще), потому очень логично совершать цикл, пока элементы есть:

while let item = iterator.next() {
    print(item)
}

Это именно метод, а не свойство, так как оно изменяет структуру своего итератора.


*-var

Вы уже знакомы со множеством конструкций по извлечению объектов из опционально значения в неопциональные константы, но есть так же возможность произвести извлечение в переменные.

Создадим опциональное число:

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

Это связано с тем, что если бы такая реакция была бы дозволена, то каждый новый объект в неё копировался бы. Это породило бы сильную путаницу и усложнило бы отладку.