Objective-C & KeyValueCoding: как избежать исключения с valueForKeyPath:?

У меня есть объект типа id и хотел бы знать, содержит ли он значение для данного keyPath:

[myObject valueForKeyPath:myKeyPath];

Теперь я помещаю его в блок @try{ } @catch{}, чтобы избежать исключений, когда данный ключ не найден. Есть ли лучший способ сделать это? Проверьте, существует ли данный путь ключа без обработки исключений?

Большое спасибо,

Стефан

Ответ 1

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

Заголовок (NSObject+ValueForUndefinedKeyAdding.h):

@interface NSObject (ValueForUndefinedKeyAdding)

+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;

@end

Реализация (NSObject+ValueForUndefinedKeyAdding.m):

#import "NSObject+ValueForUndefinedKeyAdding.h"
#import <objc/runtime.h> 
#import <objc/message.h>

@implementation NSObject (ValueForUndefinedKeyAdding)

+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler
{
    Class clazz = self;

    if (clazz == nil)
        return;

    if (clazz == [NSObject class] || clazz == [NSManagedObject class])
    {
        NSLog(@"Don't try to do this to %@; Really.", NSStringFromClass(clazz));
        return;
    }

    SEL vfuk = @selector(valueForUndefinedKey:);

    @synchronized([NSObject class])
    {    
        Method nsoMethod = class_getInstanceMethod([NSObject class], vfuk);
        Method nsmoMethod = class_getInstanceMethod([NSManagedObject class], vfuk);
        Method origMethod = class_getInstanceMethod(clazz, vfuk);

        if (origMethod != nsoMethod && origMethod != nsmoMethod)
        {
            NSLog(@"%@ already has a custom %@ implementation. Replacing that would likely break stuff.", 
                  NSStringFromClass(clazz), NSStringFromSelector(vfuk));
            return;
        }

        if(!class_addMethod(clazz, vfuk, handler, method_getTypeEncoding(nsoMethod)))
        {
            NSLog(@"Could not add valueForUndefinedKey: method to class: %@", NSStringFromClass(clazz));
        }
    }
}

@end

Затем, в вашем классе AppDelegate (или действительно в любом месте, но, вероятно, имеет смысл разместить его где-то в центре, поэтому вы знаете, где его найти, когда вы хотите добавить или удалить классы из списка) поместите этот код, который добавляет этот функциональность для классов по вашему выбору во время запуска:

#import "MyAppDelegate.h"

#import "NSObject+ValueForUndefinedKeyAdding.h"
#import "MyOtherClass1.h"
#import "MyOtherClass2.h"
#import "MyOtherClass3.h"

static id ExceptionlessVFUKIMP(id self, SEL cmd, NSString* inKey)
{
    NSLog(@"Not throwing an exception for undefined key: %@ on instance of %@", inKey, [self class]);
    return nil;
}

@implementation MyAppDelegate

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MyOtherClass1 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
        [MyOtherClass2 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
        [MyOtherClass3 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
    });
}

// ... rest of app delegate class ... 

@end

Что я делаю здесь, это добавить пользовательскую реализацию для valueForUndefinedKey: к классам MyOtherClass1, 2 и 3. Реализация примера, которую я предоставил только NSLog, и возвращает нуль, но вы можете изменить реализацию, чтобы сделать независимо от того, что вы хотите, изменив код в ExceptionlessVFUKIMP. Если вы удалите NSLog и просто верните нуль, я подозреваю, что вы получите то, что хотите, в зависимости от вашего вопроса.

Этот код НИКОГДА не проверяет методы, он добавляет только один, если он не существует. Я поставил проверки, чтобы это не использовалось на классах, у которых уже есть свои собственные реализации valueForUndefinedKey:, потому что если кто-то поместит этот метод в свой класс, ожидается ожидание того, что он будет продолжать вызываться. Также обратите внимание, что может быть код AppKit, который EXPECTS исключает из реализации NSObject/NSManagedObject. (Я точно этого не знаю, но это возможность рассмотреть.)

Несколько примечаний:

NSManagedObject предоставляет настраиваемую реализацию для valueForUndefinedKey:. Выполняя свою сборку в отладчике, все, что она делает, это бросить примерно одно и то же исключение с немного другим сообщением. Основываясь на этом 5-минутном исследовании отладчика, я чувствую, что это должно быть безопасно использовать это с подклассами NSManagedObject, но я не уверен на 100% - там может быть какое-то поведение, которое я не поймал. Осторожно.

Также, если вы используете этот подход, у вас нет хорошего способа узнать, возвращается ли valueForKey: nil, потому что keyPath действителен и состояние оказалось nil, или если он возвращает nil, потому что keyPath недействителен, а обработчик с привитой обработкой возвращает nil. Для этого вам нужно будет сделать что-то другое и конкретную реализацию. (Возможно, верните [NSNull null] или какое-либо другое значение дозорного знака или установите некоторый флаг в локальном хранилище потоков, который вы могли бы проверить, но на данный момент это действительно намного проще, чем @try/@catch?) Просто об этом нужно знать.

Кажется, это работает очень хорошо для меня; Надеюсь, это вам полезно.

Ответ 2

Вы можете попробовать следующее:

if ([myObject respondsToSelector:NSSelectorFromString(myKeyPath)])
{
}

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

Ответ 3

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

Здесь простой метод, который задает любой NSManagedObject и любой NSString как ключ, всегда будет возвращать NSString:

- (NSString *)valueOfItem:(NSManagedObject *)item asStringForKey:(NSString *)key {

    NSEntityDescription *entity = [item entity];
    NSDictionary *attributesByName = [entity attributesByName];
    NSAttributeDescription *attribute = attributesByName[key];

    if (!attribute) {
        return @"---No Such Attribute Key---";
    }
    else if ([attribute attributeType] == NSUndefinedAttributeType) {
        return @"---Undefined Attribute Type---";
    }
    else if ([attribute attributeType] == NSStringAttributeType) {
        // return NSStrings as they are
        return [item valueForKey:key];
    }
    else if ([attribute attributeType] < NSDateAttributeType) {
        // this will be all of the NSNumber types
        // return them as strings
        return [[item valueForKey:key] stringValue];
    }
        // add more "else if" cases as desired for other types

    else {
        return @"---Unacceptable Attribute Type---";
    }
}

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

Все типы атрибутов NSNumber возвращаются как их представления stringValue. Чтобы обрабатывать другие типы атрибутов (например: даты), просто добавьте дополнительные блоки "else if". (см. NSAttributeDescription Ссылка на класс для дополнительной информации).

Ответ 4

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

Ответ 5

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

Одно можно сказать наверняка: использование @[email protected] очень плохое, так как вы, скорее всего, будете утечки памяти и т.д. Исключения в ObjC/iOS не предназначены для нормального потока программы. Они также очень дороги (как бросать, так и настраивать @[email protected] IIRC).

Если вы посмотрите на заголовок Foundation/NSKeyValueCoding.h, комментарий/документация для

- (id)valueForKey:(NSString *)key;

четко указывает, какие методы должны быть реализованы для -valueForKey: для работы. Это может даже использовать прямой доступ к ivar. Вам нужно будет проверить каждый в порядке, описанном там. Вам нужно взять ключевой путь, разбить его на основе . и проверить каждую часть на каждом последующем объекте. Для доступа к ivars вам необходимо использовать время выполнения ObjC. Посмотрите objc/runtime.h.

Тем не менее, все это отличается хаккой. Вероятно, вы хотите, чтобы ваши объекты реализовали какой-то формальный протокол, а затем проверьте -conformsToProtocol: перед вызовом.

Являются ли ваши ключевые пути случайными строками или являются ли эти строки под вашим контролем? Чего вы пытаетесь достичь? Вы решаете неправильную проблему?

Ответ 6

Я не верю, что это возможно безопасным способом (т.е. без удаления с помощью -valueForUndefinedKey: или чего-то подобного на классах других людей). Я говорю это, потому что на стороне Mac, Cocoa Bindings - который может быть установлен для замены значения по умолчанию для недопустимых путей ключа - просто улавливает исключения, которые возникают из-за неправильных путей ключа. Если даже инженеры Apple не имеют возможности проверить правильность пути ключа, не пытаясь его устранить и поймать исключение, я должен предположить, что такого способа не существует.