Objective-C & KeyValueCoding: How to avoid an exception with valueForKeyPath:?
Asked Answered
L

6

13

I've got an object of type id and would like to know if it contains a value for a given keyPath:

[myObject valueForKeyPath:myKeyPath];

Now, I wrap it into a @try{ } @catch{} block to avoid exceptions when the given keypath isn't found. Is there a nicer way to do this? Check if the given keypath exists without handling exceptions?

Thanks a lot,

Stefan

Lotze answered 19/12, 2011 at 12:44 Comment(0)
P
4

It should be possible to graft this behavior onto arbitrary classes reasonably simply. I present with confidence, but without warranty, the following code which you should be able to use to add a non-exception-throwing implementation of valueForUndefinedKey: to any class, with one, centralized line of code per class at app startup time. If you wanted to save even more code, you could make all the classes you wanted to have this behavior inherit from a common subclass of NSManagedObject and then apply this to that common class and all your subclasses would inherit the behavior. More details after, but here's the code:

Header (NSObject+ValueForUndefinedKeyAdding.h):

@interface NSObject (ValueForUndefinedKeyAdding)

+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;

@end

Implementation (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

Then, in your AppDelegate class (or really anywhere, but it probably makes sense to put it somewhere central, so you know where to find it when you want to add or remove classes from the list) put this code which adds this functionality to classes of your choosing at startup time:

#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

What I'm doing here is adding a custom implementation for valueForUndefinedKey: to the classes MyOtherClass1, 2 & 3. The example implementation I've provided just NSLogs and returns nil, but you can change the implementation to do whatever you want, by changing the code in ExceptionlessVFUKIMP. If you remove the NSLog, and just return nil, I suspect you'll get what you want, based on your question.

This code NEVER swizzles methods, it only adds one if it's not there. I've put in checks to prevent this from being used on classes that already have their own custom implementations of valueForUndefinedKey: because if someone put that method in their class, there's going to be an expectation that it will continue to get called. Also note that there may be AppKit code that EXPECTS the exceptions from the NSObject/NSManagedObject implementations to be thrown. (I don't know that for sure, but it's a possibility to consider.)

A few notes:

NSManagedObject provides a custom implementation for valueForUndefinedKey: Stepping through its assembly in the debugger, all it appears to do is throw roughly the same exception with a slightly different message. Based on that 5 minute debugger investigation, I feel like it ought to be safe to use this with NSManagedObject subclasses, but I'm not 100% sure -- there could be some behavior in there that I didn't catch. Beware.

Also, as it stands, if you use this approach, you don't have a good way to know if valueForKey: is returning nil because the keyPath is valid and the state happened to be nil, or if it's returning nil because the keyPath is invalid and the grafted-on handler returned nil. To do that, you'd need to do something different, and implementation specific. (Perhaps return [NSNull null] or some other sentinel value, or set some flag in thread-local storage that you could check, but at this point is it really all that much easier than @try/@catch?) Just something to be aware of.

This appears to work pretty well for me; Hope it's useful to you.

Plagioclase answered 22/12, 2011 at 15:22 Comment(3)
I'd suggest that overriding valueForUndefinedKey: isn't problematic on an NSManagedObject. Apple document the methods that you shouldn't, and probably shouldn't, override here: developer.apple.com/library/mac/#documentation/Cocoa/Reference/…Fluorocarbon
Wow! beautiful answer. Regarding the question, in performance-tight code you'd want to avoid @try/@catch because of the huge penalty (the unrolling mechanism is very slow). Do you also teach?Masurium
I have taught, but I don't do it as a profession. re: @try/@catch - it's been a while, but IIRC, try is basically zero-cost, it's the catch that costs. If you expect a lot of exceptions to be thrown, you might look for another approach to the problem. In Cocoa, exceptions should be rare, and are typically fatal for your process, since exceptions are not an idiom typically used by the framework. In other words, the common advice to "Doc, it hurts when I do this." is "Then don't do that."Plagioclase
S
9

You could try this:

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

However, that may not correspond to the getter you have, especially if it is a boolean value. If this doesn't work for you, let me know and I'll write you up something using reflection.

Stouffer answered 19/12, 2011 at 12:48 Comment(2)
unfortunately, this doesn't work, as the path isn't traversed... e.g. if myObject has a property myChild which has a property mySubChild the keyPath myChild.mySubChild isn't recognized...Lotze
this works great for me in my UIView that I use for a variety of objects - some that don't share propertiesArmpit
F
5

If the object is a custom class of yours, you could override valueForUndefinedKey: on your object, to define what is returned when a keypath doesn't exist.

Fluorocarbon answered 19/12, 2011 at 12:52 Comment(2)
but isn't this pretty static? Above all - some of myObjects are CoreData-objects, I don't want to insert valueForUndefinedKey: always after generating them...Lotze
That's a fair point of course, but I wasn't sure what the objects were. You could put the code into a category maybe?Fluorocarbon
B
5

For NSManagedObjects, an easy solution is to look at the object's entity description and see if there's an attribute with that key name. If there is, you can also take it to the next step and see what type of an attribute the value is.

Here's a simple method that given any NSManagedObject and any NSString as a key, will always return an 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---";
    }
}

If the key is invalid or the value can't be made into a string, the method returns an NSString error message (change those blocks to do whatever you want for those cases).

All of the NSNumber attribute types are returned as their stringValue representations. To handle other attribute types (e.g.: dates), simply add additional "else if" blocks. (see NSAttributeDescription Class Reference for more information).

Burma answered 20/8, 2012 at 16:59 Comment(0)
P
4

It should be possible to graft this behavior onto arbitrary classes reasonably simply. I present with confidence, but without warranty, the following code which you should be able to use to add a non-exception-throwing implementation of valueForUndefinedKey: to any class, with one, centralized line of code per class at app startup time. If you wanted to save even more code, you could make all the classes you wanted to have this behavior inherit from a common subclass of NSManagedObject and then apply this to that common class and all your subclasses would inherit the behavior. More details after, but here's the code:

Header (NSObject+ValueForUndefinedKeyAdding.h):

@interface NSObject (ValueForUndefinedKeyAdding)

+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;

@end

Implementation (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

Then, in your AppDelegate class (or really anywhere, but it probably makes sense to put it somewhere central, so you know where to find it when you want to add or remove classes from the list) put this code which adds this functionality to classes of your choosing at startup time:

#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

What I'm doing here is adding a custom implementation for valueForUndefinedKey: to the classes MyOtherClass1, 2 & 3. The example implementation I've provided just NSLogs and returns nil, but you can change the implementation to do whatever you want, by changing the code in ExceptionlessVFUKIMP. If you remove the NSLog, and just return nil, I suspect you'll get what you want, based on your question.

This code NEVER swizzles methods, it only adds one if it's not there. I've put in checks to prevent this from being used on classes that already have their own custom implementations of valueForUndefinedKey: because if someone put that method in their class, there's going to be an expectation that it will continue to get called. Also note that there may be AppKit code that EXPECTS the exceptions from the NSObject/NSManagedObject implementations to be thrown. (I don't know that for sure, but it's a possibility to consider.)

A few notes:

NSManagedObject provides a custom implementation for valueForUndefinedKey: Stepping through its assembly in the debugger, all it appears to do is throw roughly the same exception with a slightly different message. Based on that 5 minute debugger investigation, I feel like it ought to be safe to use this with NSManagedObject subclasses, but I'm not 100% sure -- there could be some behavior in there that I didn't catch. Beware.

Also, as it stands, if you use this approach, you don't have a good way to know if valueForKey: is returning nil because the keyPath is valid and the state happened to be nil, or if it's returning nil because the keyPath is invalid and the grafted-on handler returned nil. To do that, you'd need to do something different, and implementation specific. (Perhaps return [NSNull null] or some other sentinel value, or set some flag in thread-local storage that you could check, but at this point is it really all that much easier than @try/@catch?) Just something to be aware of.

This appears to work pretty well for me; Hope it's useful to you.

Plagioclase answered 22/12, 2011 at 15:22 Comment(3)
I'd suggest that overriding valueForUndefinedKey: isn't problematic on an NSManagedObject. Apple document the methods that you shouldn't, and probably shouldn't, override here: developer.apple.com/library/mac/#documentation/Cocoa/Reference/…Fluorocarbon
Wow! beautiful answer. Regarding the question, in performance-tight code you'd want to avoid @try/@catch because of the huge penalty (the unrolling mechanism is very slow). Do you also teach?Masurium
I have taught, but I don't do it as a profession. re: @try/@catch - it's been a while, but IIRC, try is basically zero-cost, it's the catch that costs. If you expect a lot of exceptions to be thrown, you might look for another approach to the problem. In Cocoa, exceptions should be rare, and are typically fatal for your process, since exceptions are not an idiom typically used by the framework. In other words, the common advice to "Doc, it hurts when I do this." is "Then don't do that."Plagioclase
C
1

There's no easy way to solve this. Key Value Coding (KVC) isn't intended to be used that way.

One thing is for sure: using @try-@catch is really bad since you're very likely to leak memory etc. Exceptions in ObjC / iOS are not intended for normal program flow. They're also very expensive (both throwing and setting up the @try-@catch IIRC).

If you look at the Foundation/NSKeyValueCoding.h header, the comment / documentation for

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

clearly states which methods need to be implemented for -valueForKey: to work. This may even use direct ivar access. You would have to check each one in the order described there. You need to take the key path, split it up based on . and check each part on each subsequent object. To access ivars, you need to use the ObjC runtime. Look at objc/runtime.h.

All of this is vary hacky, though. What you probably want is for your objects to implement some formal protocol and then check -conformsToProtocol: before calling.

Are your key paths random strings or are those strings under your control? What are you trying to achieve? Are you solving the wrong problem?

Carpophagous answered 28/12, 2011 at 17:19 Comment(0)
O
0

I don't believe this is possible in a safe way (i.e. without mucking with -valueForUndefinedKey: or something similar on other peoples' classes). I say that because on the Mac side of things, Cocoa Bindings—which can be set to substitute a default value for invalid key paths—simply catches the exceptions that result from bad key paths. If even Apple's engineers don't have a way to test if a key path is valid without trying it and catching the exception, I have to assume that such a way doesn't exist.

Okapi answered 28/12, 2011 at 7:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.