Reputation: 4629
In my custom framework, I have a method like the one shown below which fetches value from dictionary and converts it into BOOL and returns the boolean value.
- (BOOL)getBoolValueForKey:(NSString *)key;
What if the caller of this method passes a key that does not exist. Should I throw a custom NSException saying key does not exist(but throwing exception is not recommended in objective c) or add NSError parameter to this method as shown below?
- (BOOL)getBoolValueForKey:(NSString *)key error:(NSError **)error;
If I use NSError, I will have to return 'NO' which will be misleading since 'NO' can be a valid value of any valid key.
Upvotes: 10
Views: 1563
Reputation: 19602
You pinpoint the major weakness in Apples error handling approach.
We are dealing with those situations by guaranteeing that the NSError
is nil
in success cases, so you actually check the error:
if (error) {
// ... problem
// handle error and/ or return
}
As this contradicts Apples error handle, where an Error
is never guaranteed to be nil
, but is guaranteed to be not nil
in failure cases, affected methods have to be well documented to the clients know about this special behaviour.
This is not a nice solution, but the best I know.
(This is one of the nasty things we do not have to deal with any more in swift)
Upvotes: 1
Reputation: 257
In this case, I would prefer returning NSInteger with returning 0
, 1
and NSNotFound
if caller passes key that doesn't exist.
From the nature of this method, It should be caller judgement to handle NSNorFound
. As I can see, returning error is not very encouraging to user from the method's name.
Upvotes: 0
Reputation: 4702
If you wish to communicate passing a non-existent key as a programmer error, i.e. something that should actually never occur during runtime because for instance something upstream should have taken care of that possibility, then an assertion failure or NSException is the way to do it. Quoting Apple's documentation from the Exception Programming Guide:
You should reserve the use of exceptions for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server. You usually take care of these sorts of errors with exceptions when an application is being created rather than at runtime.
If you wish to communicate a runtime error from which the program can recover / can continue executing, then adding an error pointer is the way to do it.
In principle it is fine to use BOOL
as the return type there even if there is a non-critical error case. There are however corner cases with this in case you intend to interface with this code from Swift:
NO
always implies that an error is thrown, even if in your Objective-C method implementation you do did not populate the error pointer, i.e. you would need a do / catch and handle specifically of a nil error. If you do intend to use this API from Swift, I would perhaps box the BOOL to a nullable NSNumber (at which case the error case would be nil, and the successful NO case would be an NSNumber with NO wrapped in it).
I should note, for the specific case of a potentially failable setter, there are strong conventions that you should follow, as noted in one of the other answers.
Upvotes: 3
Reputation: 650
We use this
- (id) safeObjectForKey:(NSString*)key {
id retVal = nil;
if ([self objectForKey:key] != nil) {
retVal = [self objectForKey:key];
} else {
ALog(@"*** Missing key exception prevented by safeObjectForKey");
}
return retVal;
}
Header file NSDictionary+OurExtensions.h
#import <Foundation/Foundation.h>
@interface NSDictionary (OurExtensions)
- (id) safeObjectForKey:(NSString*)key;
@end
Upvotes: 0
Reputation: 299345
The API for this is long-established by NSUserDefaults
, and should be your starting point for designing your API:
- (BOOL)boolForKey:(NSString *)defaultName;
If a boolean value is associated with defaultName in the user defaults, that value is returned. Otherwise, NO is returned.
You should avoid creating a different API for fetching bools from a keystore unless you have a strong reason. In most ObjC interfaces, fetching a non-exixtant key returns nil
and nil
is interpreted as NO
in a boolean context.
Traditionally, if one wants to distinguish between NO
and nil
, then call objectForKey
to retrieve the NSNumber
and check for nil
. Again, this is behavior for many Cocoa key stores and shouldn't be changed lightly.
However, it is possible that there is a strong reason to violate this expected pattern (in which case you should definitely note it carefully in the docs, because it is surprising). In that case, there are several well established patterns.
First, you can consider fetching an unknown key to be a programming error and you should throw an exception with the expectation that the program will soon crash because of this. It is very unusual (and unexpected) to create new kinds of exceptions for this. You should raise NSInvalidArgumentException
which exists exactly for this problem.
Second, you can distinguish between nil
and NO
by correctly using a get
method. Your method begins with get
, but it shouldn't. get
means "returns by reference" in Cocoa, and you can use it that way. Something like this:
- (BOOL)getBool:(BOOL *)value forKey:(NSString *)key {
id result = self.values[key];
if (result) {
if (value) {
// NOTE: This throws an exception if result exists, but does not respond to
// boolValue. That's intentional, but you could also check for that and return
// NO in that case instead.
*value = [result boolValue];
}
return YES;
}
return NO;
}
This takes a pointer to a bool and fills it in if the value is available, and returns YES
. If the value is not available, then it returns NO
.
There is no reason to involve NSError
. That adds complexity without providing any value here. Even if you are considering Swift bridging, I wouldn't use NSError
here to get throws
. Instead, you should write a simple Swift wrapper around this method that returns Bool?
. That's a much more powerful approach and simpler to use on the Swift side.
Upvotes: 4
Reputation: 24248
If You want all these
I suggest to make a block based implementation. You'll have a successBlock
and errorBlock
to clearly separate.
Caller will call the method like this
[self getBoolValueForKey:@"key" withSuccessBlock:^(BOOL value) {
[self workWithKeyValue:value];
} andFailureBlock:^(NSError *error) {
NSLog(@"error: %@", error.localizedFailureReason);
}];
and the implementation:
- (void)getBoolValueForKey:(NSString *)key withSuccessBlock:(void (^)(BOOL value))success andFailureBlock:(void (^)(NSError *error))failure {
BOOL errorOccurred = ...
if (errorOccurred) {
// userInfo will change
// if there are multiple failure conditions to distinguish between
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Operation was unsuccessful.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The operation timed out.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Have you tried turning it off and on again?", nil)
};
NSError *error = [NSError errorWithDomain:@"domain" code:999 userInfo:userInfo];
failure(error);
return;
}
BOOL boolValue = ...
success(boolValue);
}
Upvotes: 0