Поскольку это довольно строгий статически компилируемый язык, с первого взгляда может показаться, что Swift мало чего может предложить в плане кастомизации синтаксиса, но на самом деле это далеко не так. Благодаря таким фичам, как настраиваемые и перегруженные операторы, key paths, function/result builders и т. д., у нас есть множество возможностей для настройки синтаксиса Swift под конкретные сценарии использования.
Конечно, можно определенно утверждать, что к любому виду кастомизации синтаксиса следует подходить с большой осторожностью, поскольку нестандартный синтаксис может легко стать источником путаницы, если мы не будем достаточно осторожны. Но в определенных ситуациях этот компромисс может вполне того стоить и может позволить нам создавать что-то вроде «микро-DSL», которые на самом деле могут помочь нам сделать наш код более понятным, а не наоборот.
Инвертированные логические key paths
Чтобы рассмотреть один из таких случаев, предположим, что мы работаем над приложением для управления, фильтрации и сортировки статей, которое имеет следующую модель данных (Article
):
struct Article {
var title: String
var body: String
var category: Category
var isRead: Bool
...
}
Теперь предположим, что очень распространенной задачей в нашей кодовой базе является фильтрация различных коллекций, каждая из которых содержит экземпляры указанной выше модели. Один из способов сделать это - использовать тот факт, что любой key path литерал Swift может быть автоматически преобразован в функцию, что позволяет нам использовать следующий компактный синтаксис при фильтрации по любому логическому свойству, например в данном случае isRead
:
let articles: [Article] = ...
let readArticles = articles.filter(\.isRead)
Это действительно здорово, однако приведенный выше синтаксис можно использовать только тогда, когда мы хотим сравнить с true
- это означает, что если бы мы хотели создать аналогично отфильтрованный массив, содержащий все непрочитанные статьи, нам пришлось бы использовать вместо этого замыкание (или передавать функцию):
let unreadArticles = articles.filter { !$0.isRead }
Это, конечно, не очень большая проблема, но если описанные выше операции выполняются во многих разных местах нашей кодовой базы, тогда мы можем начать спрашивать себя: «А было бы здорово, если бы мы могли также использовать тот же красивый key path синтаксис для инвертированных логических значений? "
Здесь на помощь приходит концепция кастомизации синтаксиса. Реализуя следующую префиксную функцию, мы фактически можем создать небольшую настройку, которая позволит нам использовать key path независимо от того, сравниваем ли мы с true
или false
:
prefix func !<T>(keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
return { !$0[keyPath: keyPath] }
}
Вышеупомянутое, по сути, является перегрузкой встроенного префиксного оператора !
, который позволяет применить этот оператор к любому Bool
key path, чтобы превратить его в функцию, которая инвертирует (или переворачивает) его значение, что, в свою очередь, теперь позволяет нам обработать наш массив unreadArticles
следующим образом:
let unreadArticles = articles.filter(!\.isRead)
Это довольно круто и не делает наш код непонятнее, учитывая, что мы используем оператор !
таким образом, который соответствует тому, как он используется по умолчанию - для отрицания логического выражения.
Сравнение на основе key paths
Мы можем пойти еще дальше и также сделать возможным использование key paths для формирования фильтрующих запросов, которые сравнивают данное свойство с любым видом значения Equatable
. Это стало бы полезным, если бы мы, например, захотели иметь возможность отфильтровать массив Equatable
по каждой категории (category
) статьи. Тип этого свойства, Category
, в настоящее время определяется как enum, который выглядит следующим образом:
extension Article {
enum Category {
case fullLength
case quickReads
case basics
...
}
}
Точно так же, как мы ранее уже перегружали !
с key path специфичным вариантом, мы можем сделать то же самое с оператором ==
, и, как и раньше, мы вернем возвращающее Bool
замыкание, которое затем может быть напрямую передано в API по типу filter
:
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
return { $0[keyPath: lhs] == rhs }
}
С учетом вышеизложенного теперь мы можем легко фильтровать любую коллекцию, используя сравнение на основе key path, например:
let fullLengthArticles = articles.filter(\.category == .fullLength)
Заключение
В зависимости от того, кого вы спрашиваете, тот факт, что Swift позволяет нам легко создавать указанные выше функциональные возможности с помощью пары легких перегрузок, может оказаться либо действительно потрясающим, либо невероятно тревожным. Я склоняюсь к тому, что истина где-то посредине, и думаю, что это действительно здорово, что мы можем вносить незначительные доменно-ориентированные изменения в синтаксис Swift. Но в то же время я думаю, что эти изменения всегда следует вносить с целью сделать наши код проще, а не сложнее.
Как и все, о чем я пишу здесь, на Swift от Sundell, я лично использовал. Я использовал вышеупомянутую технику в нескольких проектах, в которых мне приходилось много фильтровать коллекции, и она проявила себя замечательно, но я бы не стал ее использовать, если бы только у меня не было сильной потребности в такой функциональности.
Чтобы получить более подробный и более продвинутый вариант описанной выше техники, ознакомьтесь с разделом «Предикаты в Swift» и не стесняйтесь присылать мне свои вопросы и комментарии через Twitter или по электронной почте.
Перевод статьи был подготовлен в преддверии старта курса "IOS Developer. Professional".
Насколько востребованы iOS-разработчики в период кризиса?
Какие требования к соискателям предъявляют компании-работодатели?
Какие вопросы задают на собеседовании, и как не допустить ошибку при ответе?
Какие знания и навыки необходимы, чтобы выделиться из толпы и обеспечить себе карьерный прогресс?
На все эти вопросы, в рамах бесплатного карьерного вебинара, ответит наш эксперт - Ексей Пантелеев. Также Ексей подробно расскажет о программе курса и процессе обучения. Записаться на вебинар.
AlexWoodblock
Как бы я ни любил трюки, которые позволяет делать Swift, но считаю, что в коде, который пишется в достаточно большой команде им либо не место, либо их создание должно быть централизованным решением всей команды с последующей миграцией на них всей кодовой базы. Желательно еще добавить что-то вроде Lint, проверяющий, что оператор действительно используется. Иначе получится, что часть команды знает о самописном операторе, часть нет, часть знает, но не использует, и в итоге каждое столкновение с ним в коде будет головной болью для всех, кто не использует его.