NSPredicate + Objective-C

NSPredicate + Objective-C

Коллекции ломтиков и кубиков без усилий

Небольшое примечание — все мои будущие сообщения будут публиковаться на моем выделенном веб-сайте, и эта публикация больше не обновляется. Спасибо за прочтение!

Когда появился Swift, мы были очарованы его простотой по сравнению с Objective-C. Затем это быстро стало ключом к развитию протокольно-ориентированного программирования. Также забудьте ссылочные типы и классы. Этот список можно продолжить.

И правда — это отличные инструменты, и у них есть отличные варианты использования. Но я чувствую, что они часто воспринимаются как серебряные пули без необходимого количества размышлений, которое, вероятно, следует уделять архитектурным решениям.

Итак, в 2018 году сообщения в блоге переполнены хакерством Swift (даже в моем блоге ??‍♂️), а на конференции обсуждаются восковые поэтические разговоры о его будущем, используя язык функционального программирования (да, я тоже это сделал ??‍♂️).

Кажется, всем нравится работать с коллекциями в Swift, но мы также можем делать аналогичные вещи в Objective-C, начиная с iOS 3. Итак, сегодня я болтаю о мощь NSPredicate и то, как вы можете просматривать коллекции с его помощью, используя ?.

Я думаю, уместно вернуться к нему, так как сейчас мы видим разработчиков, которые на этом этапе начали с Swift, а затем вернулись, чтобы поддержать Objective-C. Если это вы, возможно, вас разочаровало количество шаблонов или итераций, которые вам пришлось написать при использовании коллекций в Objective-C.

Сегодня у меня есть кое-что для тебя.

Вариант использования

За последние годы мы прошли долгий путь, когда дело дошло до коллекций Objective-C. Не более чем несколько лет назад нам пришлось сказать компилятору, что мы намного умнее, чем он был на самом деле:

NSString *aString = (NSString *)[anArray indexOfObject:0];

Спасибо, Небеса, Купертино и Друзья © в конце концов добавили универсальные шаблоны путем стирания шрифтов. Это ознаменовало значительное улучшение:

NSArray <NSString *> *anArray = @[@"Sup"];
NSString *aString = [anArray firstObject];

Но дженерики или нет, мы часто взаимодействуем с содержимым коллекций Objective-C, делая что-то вроде этого:

for (NSString *str in anArray)
{
    if ([str isEqualToString:@"The Key"]) 
    {
        // Do something
    }
}

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

Здесь предикаты могут уменьшить удар. Речь идет не о том, чтобы «хитрить» или «мило» с нашим кодом, а о прагматичности и лаконичности.

Вид на 10,000 футов

По сути, NSPredicate используется для ограничения или определения параметров для фильтрации памяти или при выполнении выборки. В сочетании с Core Data он действительно заработал. Это похоже на SQL, за исключением менее ужасного *.

* Я шучу, просто операции на основе наборов никогда не имели для меня смысла ?.

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

См. также:  Спасибо, что поделились этим, Карина. Здесь было несколько, о которых я не знал.

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

// In place
[mutableArray filterUsingPredicate:/*NSPredicate*/]
// New instance returned
[mutableArray filteredArrayUsingPredicate:/*NSPredicate*/]

Хотя предикаты могут быть созданы из NSExpression, NSCompoundPredicate или NSComparisonPredicate, они также могут быть созданы с использованием строкового синтаксиса. Это похоже на язык визуального формата, который можно использовать для определения ограничений макета.

Мы сосредоточимся на полезности использования метода строкового синтаксиса.

Установка

Чтобы проиллюстрировать это, давайте рассмотрим следующий код для оставшейся части сообщения:

// Pseudo code 
Person:NSObject
Identifier:NSString
Name:NSString
PayGrade:NSNumber
// An some property somewhere containing Person instances
NSArray <Person *> *employees

Время запроса ⚡️

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

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

{
    @"erersdg32453tr",
    @"dfs8rw093jrkls",
    // etc
}

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

// Assume "employees" is an existing array of Person objects
NSArray <NSString *> *morningEventAttendees = @[/*Identifiers of people listed above*/];
NSMutableArray <Person *> *peopleAttendingMorningEvent = [NSMutableArray new];
for (NSString *userID in morningEventAttendees)
{
    for (Person *person in employees)
    {
        if ([person.identifier isEqualToString:userID])
        {
           [peopleAttending addObject:person];
        }
    }
}
// Now peopleAttendingMorningEvent has what we want

Точно такой же результат достигается при использовании предиката как такового:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@", peopleAttendingMorningEvent];
NSArray <Person *> *peopleAttendingMorningEvent = [employees filteredArrayUsingPredicate:morningAttendees];

?.

Синтаксис предиката позволяет использовать SELF, который здесь очень эффективен. Он представляет собой объект, содержащийся в обрабатываемом массиве, поэтому для нас — объекты Person.

  • Еще одним преимуществом является то, что мы отказались от изменчивости определения массива.

По этой причине мы можем получить доступ к ключевым путям, связанным с объектом, который представляет SELF. Вы видите это выше, так как имеется ссылка на свойство identifier.

Если вы предпочитаете, любой ключевой путь также может быть выражен через переменную, используя вместо нее синтаксис «% K». Эта версия делает то же самое, что и выше:

[NSPredicate predicateWithFormat:@"SELF.%K IN %@", @"identifier", peopleAttendingMorningEvent];

Составные предикаты

Совмещать сравнения просто. Предположим, что наши требования теперь требуют поиска пользователей, посещающих мероприятия так же, как указано выше, но теперь их уровень заработной платы также должен составлять от 50 000 до 60 000.

Если традиционные подходы победят, тогда наш первый оператор if будет только расти:

// Same code as above same for this tweak
if ([person.identifier isEqualToString:userID] && (person.paygrade.integerValue >= 5 && person.paygrade.integerValue <= 10))
{
    [peopleAttending addObject:person];
}

Но использование рефакторинга предиката приводит нас к этому более идиоматическим способом:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@ && SELF.paygrade.integerValue BETWEEN {50000, 60000}", peopleAttendingMorningEvent];

Синтаксис позволяет использовать разные операторы, обозначающие одно и то же, что может помочь улучшить удобочитаемость в соответствии с вашими предпочтениями. Например:

  • «&&» или «И»
  • “||” or “OR”
  • «!» или не»

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

  • =,==
  • !=,<>
  • >=,=>
  • <
  • >
  • <=,=<
См. также:  Исследование InvalidProgramException из дампа памяти (часть 2 из 3)

Сравнение строк

Нам часто приходится подбирать значения на основе сравнения строк. Хорошо известно, что Objective-C проявляет свою безответную любовь к многословности не в большем свете, чем при работе с NSString:

NSString *name = @"Jordan"
name = [name stringByAppendingString:[NSString stringWithFormat:@"%@ %@", @"Wesley", @"Morgan"]]

… тогда как Swift просто ухмыляется и с гораздо меньшей суетой объединяет свои собственные строки. Таким образом, мы можем принять во внимание, что такая многословность неприменима к NSPredicate и сравнениям строк.

// Assume mutablePersonAr is a Person array with names of "Karl", "Jordan"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name BEGINSWITH 'K'"];
// Now only contains Karl
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

Практически любое сравнение может быть достигнуто с помощью синтаксиса предиката CONTAINS, BEGINSWITH, ENDSWITH и LIKE:

// Assume mutablePersonAr is a Person array with names of "Karl", "Kathryn"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name LIKE 'Kar*'"];
// Now only contains Karl
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

Возможно, вы заметили звездочку выше, которая, как и многие аналогичные DSL, представляет собой подстановочный знак.

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

NSString *predicateFormat = @"(SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat];
// Now only contains Karl
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

Кроме того, существует даже поддержка сочетания синтаксиса SQLish NSPredicate с регулярными выражениями посредством синтаксиса MATCHES:

[NSPredicate predicateWithFormat:@"SELF.phoneNumber MATCHES %@", phoneNumberRegex];

Однако сейчас подходящий момент, чтобы указать на то, что синтаксис формата предиката именно такой, каков он есть. Прямая струна. И если вы не Мавис Бикон, вы будете время от времени допустить опечатку.

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

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

NSString *predicateFormat = @"SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat];
// Now only contains Karl
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

Для борьбы с такими проблемами я часто объединяю предикаты с NSStringFromSelector (), чтобы обеспечить дополнительный уровень защиты от опечаток и будущего рефакторинга:

NSString *predicateFormat = @"(SELF.%@ LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"
NSString *kpName = NSStringFromSelector(@selector(identifier));
NSString *kpPaygrade = NSStringFromSelector(@selector(paygrade));
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat, kpName, kpPaygrade];
// Now only contains Karl
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

Немного тяжелее? Конечно. Безопаснее? Абсолютно.

Запросы коллекции KeyPath

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

// Assume a Person object now has this property on it:
// NSArray <NSNumber *> *previousPay
// Find everyone who's average previous pay was over 10
NSString *predicateFormat = @"[email protected] > 10";
NSPredicate *previousPayOverTen = [NSPredicate predicateWithFormat:predicateFormat];
// Everyone whose previous pay's average was greater than 10
[mutablePersonAr filterUsingPredicate:previousPayOverTen];

Вы можете переключить наш @avg на:

  • @sum
  • @Максимум
  • @min
  • @считать

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

Копаем глубже в массивы

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

  • массив [ПЕРВЫЙ]
  • массив [ПОСЛЕДНИЙ]
  • массив [РАЗМЕР]
  • массив [индекс]

Основываясь на примере кода выше, это позволяет выполнять такие запросы:

// Find everyone who's had three previous different salaries
NSString *predicateFormat = @"previousPay[SIZE] == 3";
NSPredicate *threePreviousSalaries = [NSPredicate predicateWithFormat:predicateFormat];
// These Person objects had three previous salaries
[mutablePersonAr filterUsingPredicate:threePreviousSalaries];

И, как мы упоминали выше, он идеально подходит для применения нескольких условий:

// Find everyone who's had three previous different salaries and whose first one was greater than 8
NSString *predicateFormat = @"(previousPay[SIZE] == 3) AND (previousPay[FIRST].intValue > 8)";
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[mutablePersonAr filterUsingPredicate:predicate];

Если пойти еще дальше, вы получите еще больше мощности, используя одно из следующих действий:

  • @distinctUnionOfArrays
  • @unionOfArrays
  • @ unionOfObjects
  • @distinctUnionOfObjects
См. также:  Xcode: 'ModuleNotFoundError: нет модуля с именем' psycopg2 '

Подождите, но предположим, что у нас есть массив массивов, содержащих объекты Person, и все, что нам нужно, это уникальные идентификаторы экземпляров Person среди них:

// Assume p1/2/3/4 are all hydrated Person objects
NSArray <NSArray <Person *> *> *previousEmployees = @[@[p1],@[p2,p1,p2],@[p1],@[p4,p2],@[p4],@[p4],@[p1]];
// Get every unique ID
NSArray *unqiuePreviousEmployeeIDs = [previousEmployees valueForKeyPath:@"@distinctUnionOfObjects.identifier"];
// The array would contain only unique IDs

Круто, нет?

На этом веселье не заканчивается, поскольку есть даже поддержка подзапросов:

// Assume Person objects have a new property for their team:
// NSArray <Person *> *team;
// Find everyone in an employee array who has people in their team with a pay over 1 and no previous pay history
NSString *predicateFormat = @"SUBQUERY(team, $teamMember, $teamMember.paygrade.intValue > 1 AND $teamMember.previousPay == nil)[email protected] > 0";
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[employeeAr filterUsingPredicate:predicate];

Подзапросы весьма полезны, если вам нужно выполнить поиск в массиве объектов, который также содержит свойство, которое само по себе является коллекцией. Итак, у нас есть массив объектов Person, и мы заглядываем в их массив teamMember.

Удобство — ключ к успеху (Путь)

Хотя NSPredicate создан для поиска, он не был бы Objective-C, если бы вы не могли всего изменить их точное назначение. Здесь не исключение.

Когда вы думаете о предикате, вы думаете о фильтрации коллекции — это означает, что возвращаемый результат (или мутация на месте) по-прежнему содержит тот же материал.

Но вы можете сделать так, чтобы это не было одним и тем же. И мы действительно сделали это в предыдущем примере кода. Приведенный выше массив массивов использовался для возврата массива идентификаторов — экземпляров NSString. Keypathin ’делает все это возможным.

Вот более прямой пример:

// We want an array of identifier strings whose length is greater than 10
NSString *predicateFormat = @”SELF.identifier.length > 10";
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
NSArray <NSString *> *longEmployeeIDs = [[employeeArray filteredArrayUsingPredicate:predicate] valueForKey:@”identifier”];
// Now longEmployeeIDs has not Person objects, but only strings

Заключение

Вы можете прожигать коллекции Objective-C с помощью слащавого синтаксиса. Вы можете перейти к определенному подмножеству элементов без вложенных циклов. С NSPredicate все намного проще для глаз.

В то время как Swift имеет первоклассную языковую поддержку для нарезки и нарезки коллекций, на самом деле не так уж и сложно использовать объект, созданный для выполнения многих из тех же вещей. Если вы обнаружите, что используете зрелую кодовую базу или только что созданную базу с The Dino (Objective-C), позвольте предикатам течь свободно.

До следующего раза ».

 

Джордан Морган (@ JordanMorgan10) | Twitter
Последние твиты от Джордана Моргана (@ JordanMorgan10). iOS в @buffer, автор @pluralsight и я пишем это… www.twitter.com

 

If you enjoyed this week’s post, please feel free to go ahead and NSRecommend(this, where: below);

Понравилась статья? Поделиться с друзьями:
IT Шеф
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: