Використовуємо замикання у Swift по повній

Незважаючи на те, що в Objective-C 2.0 є замикання (відомі якблоки), раніше еппловський API використовував їх неохоче. Можливо, тому багато програмісти під iOS із задоволенням експлуатували сторонні бібліотеки, на кшталт AFNetworking, де блоки застосовуються повсюдно. З виходом Swift, а також додаванням нової функціональності в API, працювати із замиканнями стало надзвичайно зручно. Давайте розглянемо, які особливості має їх синтаксис у Swift, і які трюки можна з ними «витворювати».

повній

Просуватимемося від простого до складного, від нудного до веселого. Заздалегідь вибачаюсь за рясне використання мантр «функція», «параметр» і «Double», але з пісні слів не викинеш.

1.1. Об'єкти першого класу

Для початку зміцнимося з думкою, що у Swift функції є носіями гордого статусу об'єктів першого класу. Це означає, що функцію можна зберігати в змінній, передавати як параметр, повертати як результат роботи іншої функції. Вводиться поняття «типу функції». Цей тип описує як тип повертається значення, а й типи вхідних аргументів. Припустимо, у нас є дві схожі функції, які описують дві математичні операції складання та віднімання:

Їх тип описуватиметься так:

Прочитати це можна так: «Перед нами тип функції з двома вхідними параметрами типуDoubleі значенням типуDouble, що повертається.» Ми можемо створити змінну такого типу:

Код, описаний вище, виведе в консолі:3.0 -1.0

1.2. Замикання

Використовуємо ще один привілей об'єкта першого класу. Повертаючись до попереднього прикладу, ми могли б створити таку нову функцію, яка б приймала однуіз наших старих функцій типу(Double, Double) -> Doubleяк останній параметр. Ось так вона виглядатиме:

Розберемо заплутаний синтаксис на складові. ФункціяperformOperationприймає три параметри:

  • op1типуDouble(op - скорочене від «операнд»)
  • op2типуDouble
  • operationтипу(Double, Double) -> Double
У своєму тіліperformOperationпросто повертає результат виконання функції, що зберігається в параметріoperation, передаючи в нього перші два свої параметри. Поки що виглядає заплутано і, можливо, навіть не зрозуміло. Трохи терпіння, панове.

Давайте тепер передамо як третій аргумент не змінну, а анонімну функцію, уклавши її у фігурні<дужки. Переданий таким чином параметр і називатиметьсязамиканням:

Уривок коду(op1: Double, op2: Double) -> Double in — це, так би мовити, заголовок замикання. Складається він із:

  • псевдонімів
op1,op2типуDoubleдля використання всередині замикання
  • значення замикання, що повертається-> Double
  • ключового словаin
  • Ще раз про те, що сталося, за пунктами:(1)Оголошено функціюperformOperation(2)Ця функція приймає три параметри. Два перші – операнди. Останній – функція, яка буде виконана над цими операндами.(3)performOperationповертає результат виконання операції.(4)Як останній параметр уperformOperationбула передана функція, описана замиканням.(5)У тілі замикання вказується, яка операція виконуватиметься надоперандами.

    Частина 2. Весела. Синтаксичний цукор та несподівані «плюшки»

    Автори Swift доклали чимало зусиль, щоб користувачі мови могли писати якнайменше коду і якнайбільше витрачати свій дорогоцінний час на читання Хабра роздуми про архітектуру проекту. Взявши за основу приклад з арифметичними операціями, подивимося, до якого стану ми зможемо його «розкрутити».

    2.1. Позбавляємося типів під час виклику.

    По-перше, можна не вказувати типи вхідних параметрів у замиканні явно, оскільки компілятор знає про них. Виклик функції тепер виглядає так:

    2.2. Використовуємо синтаксис "хвостового замикання".

    По-друге, якщо замикання передається як останній параметр у функцію, то синтаксис дозволяє скоротити запис, і код замикання просто прикріплюється до хвоста виклику:

    2.3. Не використовуємо ключове слово "return".

    Приємна (у деяких випадках) особливість мови полягає в тому, що якщо код замикання вміщується в один рядок, то результат виконання цього рядка автоматично буде повернутий. Таким чином, ключове слово «return» можна не писати:

    2.4. Використовуємо стенографічні назви для параметрів.

    Йдемо далі. Цікаво, що Swift дозволяє використовувати так звані стенографічні (shorthand) імена для вхідних параметрів у замиканні. Тобто. кожному параметру за замовчуванням надається псевдонім у форматі$n, деn— порядковий номер параметра, починаючи з нуля. Таким чином нам, виявляється, навіть не потрібно вигадувати імена для аргументів. У такому разі весь «заголовок» замикання вже не несе в собі ніякого смислового навантаження, і його можна опустити:

    Погодьтеся, цей запис вже зовсім не схожий на той, який був на самому початку.

    2.5. Хід конем: операторні функції.

    Все це були ще квіточки. Нині буде ягідка. Давайте подивимося на попередній запис і поставимо питання, що вже знає компілятор про замикання? Він знає кількість параметрів (2) та їх типи (DoubleтаDouble). Знає тип значення, що повертається (Double). Так як у коді замикання виконується лише один рядок, він знає, що йому потрібно повертати як результат його виконання. Чи можна спростити цей запис якось? Виявляється, можна. Якщо замикання працює лише з двома вхідними аргументами, як замикання дозволяється передати операторну функцію, яка виконуватиметься над цими аргументами (операндами). Тепер наш виклик буде виглядати так:

    Краса! Тепер можна виконувати елементарні операції над нашими операндами залежно від деяких умов, написавши при цьому мінімум коду.

    До речі, Swift також дозволяє використовувати операції порівняння як операторну функцію. Виглядатиме це приблизно так:

    Або бітові операції:

    UPD.: Реальне застосування

    Деякі користувачі висловили невдоволення через відсутність прикладів із реального життя. Буквально вчора натрапив на завдання, яке може бути елегантно вирішене з використанням коротких замикань.

    Якщо вам потрібно створити чергу з пріоритетом, можна використати двійкову купу (binary heap). Як відомо, це може бути як MinHeap, і MaxHeap, тобто. купи, де в корені дерева є мінімальний або максимальний елемент відповідно. Базова реалізація MinHeap від MaxHeap відрізнятиметься по суті лише перевірочними порівняннями при відновленні інваріанту двійкової купи після додавання/видалення елемента.

    Таким чином, ми моглистворити базовий класBinaryHeap, який міститиме властивістьcomparisonтипу(T, T) -> Bool. А конструктор цього класу прийматиме спосіб порівняння і потім використовувати його в методахheapify. Прототип базового класу виглядав би так:

    Тепер для того, щоб створити класиMinHeapтаMaxHeapнам достатньо успадкуватися відBinaryHeap, а в їх конструкторах просто явно вказати, яке порівняння застосовувати. Ось так виглядатимуть наші класи:

    Хардкорна конфа за С++. Ми запрошуємо лише профі.