Objective-C & KeyValueCoding: как избежать исключения с помощью valueForKeyPath :? - PullRequest
10 голосов
/ 19 декабря 2011

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

[myObject valueForKeyPath:myKeyPath];

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

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

Stefan

Ответы [ 6 ]

8 голосов
/ 19 декабря 2011

Вы можете попробовать это:

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

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

5 голосов
/ 20 августа 2012

Для 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 голосов
/ 19 декабря 2011

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

3 голосов
/ 22 декабря 2011

Должно быть возможно достаточно просто перенести это поведение на произвольные классы.Я с уверенностью, но без гарантии представляю следующий код, который вы сможете использовать для добавления реализации без исключения 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 s ивозвращает ноль, но вы можете изменить реализацию так, как вам хочется, изменив код в ExceptionlessVFUKIMP.Если вы удалите NSLog и просто вернете ноль, я подозреваю, что вы получите то, что вы хотите, основываясь на вашем вопросе.

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

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

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

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

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

1 голос
/ 28 декабря 2011

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

Одно можно сказать наверняка: использование @try-@catch - это очень плохо , так как очень вероятно, что утечка памятии т.д. Исключения в ObjC / iOS не предназначены для нормального выполнения программы.Они также очень дорогие (как создание, так и настройка @try-@catch IIRC).

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

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

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

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

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

0 голосов
/ 28 декабря 2011

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

...