{"id":13892,"url":"\/distributions\/13892\/click?bit=1&hash=3a950d17d2eeeef532c3af6ac82585b34073fc4ca99c1401cfb200abe3d248df","title":"\u0421\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0442\u043b\u0438\u0447\u0438\u0442\u044c \u0446\u0432\u0435\u0442 \u0432\u043e\u0440\u043e\u043d\u043e\u0432\u0430 \u043a\u0440\u044b\u043b\u0430 \u043e\u0442 \u0443\u0433\u043e\u043b\u044c\u043d\u043e-\u0447\u0451\u0440\u043d\u043e\u0433\u043e?","buttonText":"\u041f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c","imageUuid":"4f9a8e20-c784-5902-ac58-b1e8d30c17d6","isPaidAndBannersEnabled":false}

NSPredicate: старый API с новыми сюрпризами

Недавно я работал с NSPredicate — API, который существует с момента выхода Mac OS X Tiger в 2005 году — и то, что выглядело довольно простым, не работало так, как я ожидал.

Я внедрял поддержку Apple Shortcuts в своем приложении для чтения, чтобы пользователи могли автоматизировать процессы взаимодействия. Я заметил, что при использовании EntityPropertyQuery, некоторые, основанные на свойствах, запросы статей не возвращали ожидаемое их количество. У меня было четырнадцать статей, сохраненных на симуляторе iPad. Четыре из них были написаны мной. Однако, когда я искал статьи, где автором был не «Дуглас Хилл», то вместо ожидаемых десяти, в результате получал лишь две.

Было ясно, что статьи не были включены в поиск, если не был указан автор статьи. Другими словами, когда свойство author было равно nil. (Я буду комбинировать термины nil и null, т.к. они представляют одну и ту же концепцию с разными именами в разных программных стеках.)

Отслеживание проблемы

Во-первых, давайте рассмотрим самый простой тест:

let maybeString: String? = nil let condition = maybeString != “test"

Предполагается, что condition в этом случае будет истинным. Если бы мы использовали ==, то результат явно был бы ложным. Однако здесь используется != поэтому мы ожидаем обратного. Хорошие новости: именно так это и работает!

Во-вторых, я запустил быстрый тест в playground, используя NSPredicate в простой ситуации:

class MyObject: NSObject {     @objc var author: String?     init(author: String?) {         self.author = author     } } let array = [     MyObject(author: "Douglas Hill"),     MyObject(author: "Someone else"),     MyObject(author: nil), ] (array as NSArray).filtered(using: NSPredicate(format: "author != %@", "Douglas Hill")) // [{NSObject, author "Someone else"}, {NSObject, nil}]

Этот тест показал, что при фильтрации объектов, автором которых не был «Дуглас Хилл», включались объекты, где автор был nil. Именно такое поведение я и ожидал.

В этом месте у меня было весьма серьезное подозрение, что это связано с хранилищем SQLite, которое использует мой стек Core Data. Я уверен, что ветераны SQL уже знают ответ.

В-третьих, я провел определенную отладку своего хранилища Core Data без использования Shortcuts и увидел то же самое, что и с Shortcuts: фильтрация по атрибуту(*свойству), не равному какому-либо значению, не будет включать объекты, для которых этот атрибут равен nil.

Я включил -com.apple.CoreData.SQLDebug 3, и это показало непосредственность(*незамысловатость, простоту) генерируемых команд SQL. Данный предикат: author != "Douglas Hill" добавит к команде SQL SELECT это:

WHERE t0.ZAUTHOR<> ?

Где значением ? является:

SQLite bind[0] = "Douglas Hill”

Я никогда не работал с SQL напрямую, только как с элементом реализации (и производительности) Core Data. На данный момент моя гипотеза заключалась в том, что подобная обработка null - именно то, как работает SQL.

К сожалению, SQL не похож на свободный(*независимый) и открытый стандарт, в котором вы легко сможете прочитать указание/спецификацию, чтобы проверить данную деталь. Я провел небольшое исследование в Интернете, и вторичные источники подтвердили мою гипотезу. NULL не считается равным или неравным чему-либо в SQL, или, другими словами, сравнения с null не являются ни истинными, ни ложными.

Этот комментарий jsumrall к вопросу о переполнении стека хорошо подводит итог:

Следует также отметить, что, поскольку != сравнивает только значения, выполнение чего-то вроде WHERE MyColumn != 'somevalue' не вернет NULL.

Чего ожидает пользователь?

С точки зрения программиста, я бы не сказал, что любой из способов обработки null однозначно лучше. Однако от NSPredicate я бы ожидал последовательности (*стабильности). Меня удивило то, что Core Data не сглаживает подобное поведение SQL, чтобы соответствовать тому, как сравнения обычно работают в программных стеках Apple.

С точки зрения пользователя, я думаю, ситуация иная. Пользователи не будут так остро осознавать концепцию null. Есть большая вероятность, что они считают null и пустую строку одним и тем же. Поскольку пользователям мои запросы будут доступны через Shortcuts, я думаю, более ожидаемо, что фильтрация элементов со свойством, не равным какому-либо значению, должна включать элементы, для которых это свойство равно null.

Реализация лучшего поведения

Эту кривизну легко сгладить самостоятельно. Во время настройки предиката для хранилища Core Data SQLite, с условием не быть равным какому-либо значению. Не устанавливайте предикат вот так:

NSPredicate(format: "%K != %@", stringKey, nonNilValue)

Вместо этого мы также проверяем равенство с nil/null, задавая предикат следующим образом:

NSPredicate(format: "%K != %@ OR %K == NIL", stringKey, nonNilValue, stringKey)

На практике это выглядит как удобное расширение для NotEqualToComparator из фреймворка Apple App Intents (Shortcuts API):

private extension NotEqualToComparator<EntityProperty<String?>, String?, NSPredicate> {     /// Creates a comparator for case- and diacritic-insensitive matching of an optional string property using an NSPredicate for Core Data objects. (My objects are articles.)     convenience init(keyPath: KeyPath<ArticleEntity, EntityProperty<String?>>) {         // Maps from Swift key paths to string keys.         let stringKey = Article.stringKey(from: keyPath)         self.init() { value in             if let value {                 return NSPredicate(format: "%K !=[cd] %@ OR %K == NIL", stringKey, value, stringKey)             } else {                 // Ignore this branch for now since Shortcuts doesn’t have any UI that lets a nil value be passed here. My actual code is slightly different due to an interesting reason, but that’s not the topic of this article.             }         }     } }

Резюме

  • В Swift результат nil != nonNilValue является истинным.
  • Как правило, NSPredicate, созданный как NSPredicate (формат: "%K != %@", stringKey, nonNilValue), будет соответствовать объектам, у которых свойство, соответствующее stringKey, равно nil.
  • При выборке из хранилища Core Data SQLite предикат, созданный, как указано выше, не будет соответствовать объектам, в которых свойство, соответствующее stringKey, равно nil. Это происходит потому, что Core Data напрямую сопоставляет команду с SQL, а SQL указывает, что нет значения, которое равно или неравно null.
  • Это можно обойти, создав предикат как NSPredicate(format: "%K != %@ OR %K == NIL", stringKey, nonNilValue, stringKey).
  • Урок: Проверяйте все.

Подписывайся на наши соцсети: Telegram / VKontakte

Вступай в открытый чат для iOS-разработчиков: t.me/swiftbook_chat

0
Комментарии
Читать все 0 комментариев
null