10
\$\begingroup\$

I've attempted to implement a 1-to-1 dictionary in Objective-C. I'm probably missing some convenient methods that one might commonly want to use.

These are both declared in the same .h file and implemented in the same .m file. I think if much more code is added to either of these, splitting them into their own files would probably be preferred, but remaining in the same files allows me to hide some implementation details that I might not otherwise be able to hide.

The goal here is to make the object familiar and feel very similar to an NSDictionary (or NSMutableDictionary) but allow 1-to-1 mapping, so that for each key there is exactly one value, and for each value there is exactly one key.

BiDiDictionary.h

Immutable:

@interface BiDiDictionary : NSObject

- (instancetype)init;
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;

+ (instancetype)dictionary;
+ (instancetype)dictionaryWithDictionary:(NSDictionary *)dictionary;

- (id<NSCopying>)objectForKey:(id<NSCopying>)key;
- (id<NSCopying>)keyForObject:(id<NSCopying>)object;

@property (readonly) NSArray *allKeys;
@property (readonly) NSArray *allObjects;

@end

Mutable:

@interface BiDiMutableDictionary : BiDiDictionary

- (void)setObject:(id<NSCopying>)object forKey:(id<NSCopying>)key;
- (void)setKey:(id<NSCopying>)key forObject:(id<NSCopying>)object;
- (void)removeObjectForKey:(id<NSCopying>)key;
- (void)removeKeyForObject:(id<NSCopying>)object;

- (void)removeAllObjects;

@end

BiDiDictionary.m

Immutable:

@implementation BiDiDictionary {
    @protected NSMutableDictionary *_keyValueDict;
    @protected NSMutableDictionary *_valueKeyDict;
}

- (instancetype)init {
    return [self initWithDictionary:nil];
}

- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
    self = [super init];
    if (self) {
        _keyValueDict = [NSMutableDictionary dictionary];
        _valueKeyDict = [NSMutableDictionary dictionary];
        for (id key in [dictionary allKeys]) {
            if (!_keyValueDict[key]) {
                id value = dictionary[key];
                if (![value conformsToProtocol:@protocol(NSCopying)]) {
                    NSException *ex = [NSException exceptionWithName:@"InvalidArgumentException"
                                                              reason:@"Values must conform to NSCopying protocol"
                                                            userInfo:@{@"InvalidArgument":value}];
                    @throw ex;
                } else if (_valueKeyDict[value]) {
                    NSException *ex = [NSException exceptionWithName:@"DuplicateValueException"
                                                              reason:@"Duplicate value found in the initilization dictionary"
                                                            userInfo:@{@"Duplicate":value}];
                    @throw ex;
                } else {
                    _keyValueDict[key] = value;
                    _valueKeyDict[value] = key;
                }
            }
        }
    }
    return self;
}

+ (instancetype)dictionary {
    return [[self alloc] init];
}

+ (instancetype)dictionaryWithDictionary:(NSDictionary *)dictionary {
    return [[self alloc] initWithDictionary:dictionary];
}

- (id)objectForKey:(id)key {
    return _keyValueDict[key];
}

- (id)keyForObject:(id)object {
    return _valueKeyDict[object];
}

- (NSArray *)allKeys {
    return [_keyValueDict allKeys];
}

- (NSArray *)allObjects {
    return [_valueKeyDict allKeys];
}

@end

Mutable:

@implementation BiDiMutableDictionary

- (void)setObject:(id<NSCopying>)object forKey:(id<NSCopying>)key {
    if (![(id<NSObject>)key conformsToProtocol:@protocol(NSCopying)]) {
        NSException *ex = [NSException exceptionWithName:@"InvalidArgumentException"
                                                  reason:@"Keys must conform to NSCopying protocol"
                                                userInfo:@{@"InvalidArgument":key}];
        @throw ex;
    }
    if (![(id<NSObject>)object conformsToProtocol:@protocol(NSCopying)]) {
        NSException *ex = [NSException exceptionWithName:@"InvalidArgumentException"
                                                  reason:@"Objects must conform to NSCopying protocol"
                                                userInfo:@{@"InvalidArgument":object}];
        @throw ex;
    }
    [self removeObjectForKey:key];
    [self removeKeyForObject:object];
    _keyValueDict[key] = object;
    _valueKeyDict[object] = key;
}

- (void)setKey:(id<NSCopying>)key forObject:(id<NSCopying>)object {
    @try {
      [self setObject:object forKey:key];
    }
    @catch (NSException *ex) {
        // rethrowing to hide some implementation details
        @throw;
    }
}

- (void)removeObjectForKey:(id<NSCopying>)key {
    if (![(id<NSObject>)key conformsToProtocol:@protocol(NSCopying)]) {
        NSException *ex = [NSException exceptionWithName:@"InvalidArgumentException"
                                                  reason:@"Keys must conform to NSCopying protocol"
                                                userInfo:@{@"InvalidArgument":key}];
        @throw ex;
    }
    id object = _keyValueDict[key];
    if (object) {
        [_valueKeyDict removeObjectForKey:object];
    }
    [_keyValueDict removeObjectForKey:key];
}

- (void)removeKeyForObject:(id<NSCopying>)object {
    if (![(id<NSObject>)object conformsToProtocol:@protocol(NSCopying)]) {
        NSException *ex = [NSException exceptionWithName:@"InvalidArgumentException"
                                                  reason:@"Objects must conform to NSCopying protocol"
                                                userInfo:@{@"InvalidArgument":object}];
        @throw ex;
    }
    id key = _valueKeyDict[object];
    if (key) {
        [_keyValueDict removeObjectForKey:key];
    }
    [_valueKeyDict removeObjectForKey:object];
}

- (void)removeAllObjects {
    [_keyValueDict removeAllObjects];
    [_valueKeyDict removeAllObjects];
}

@end

Aha! The rare, but not entirely pointless NSException class!

\$\endgroup\$
3
+25
\$\begingroup\$

Overall, this seems like a very sensible and straightforward implementation. I don't have a lot to say, but I do have a few small question:

  1. In [-BiDiMutableDictionary setKey:forObject:] you catch the exception, do nothing with it, then re-throw it. This is exactly the same as not catching it. I'd just remove the @try/@catch block. - As @nhgrif points out, this sets the source of the exception to the spot where it was re-thrown, so it makes some sense. I guess I'd document that in the code since it's a non-obvious bit of information.

  2. In [-BiDiMutableDictionary removeObjectForKey:], what's the purpose of checking if the key passed in supports <NSCopying>? If it doesn't, then the key isn't contained in the dictionary because you've ensured that keys that don't implement the protocol can't be added via the -set methods. If the key is contained in the dictionary, then you already know it implements the protocol. So there doesn't appear to me to be much point in checking in the -remove method.

\$\endgroup\$
5
  • \$\begingroup\$ Rethrowing is not exactly the same as not-catching it. developer.apple.com/library/mac/documentation/Cocoa/Conceptual/… One of the differences is that to anyone who handles the rethrown exception, it looks as if the exception originated from the last place it was thrown. If setKey:forObject: doesn't rethrow, then an exception here would seem as if we were calling setObject:forKey: (that's what the exception would claim as its source without rethrowing). This can be confusing to the developer. There are other differences in that link \$\endgroup\$
    – nhgrif
    Jul 1 '15 at 12:02
  • \$\begingroup\$ As for the second point, when implementing, I think I was thinking of how an array would behave if you were to try removing from an index outside its bounds, but I think your recommendation is probably right for a dictionary. \$\endgroup\$
    – nhgrif
    Jul 1 '15 at 12:04
  • \$\begingroup\$ @nhgrif Ah, OK. I wasn't aware of that difference in Objective-C exceptions. Very good to know! If I can remember how to do strike-through, I'll edit that point. \$\endgroup\$ Jul 1 '15 at 15:31
  • \$\begingroup\$ This isn't completely special to Objective-C--other languages behave similarly (or have other reasons for rethrowing). This is the case with other languages. However, note that I did document it: // rethrowing to hide some implementation details. Do you think this comment should be more clear? \$\endgroup\$
    – nhgrif
    Jul 1 '15 at 15:37
  • \$\begingroup\$ Personally, for me, because I wasn't aware of that detail of exceptions, it would be nice if the comment were more straightforward. Along the lines of // rethrowing so the caller sees the exception originating here. It might be obvious to others, and thinking about it, it makes a lot of sense. It just never occurred to me that it would have that effect. \$\endgroup\$ Jul 2 '15 at 2:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.