Objective C Generics

xcode   programming   objective-c   generics  

After writing my last post at midnight while jumping up and down with excitement over the discovery of Generics for Objective C, I decided to write a longer post with more details. So today I wrote up some test code to make sure they worked as I thought. This post is is the result and then wrote this post about it.

So why do we want Generics?

Often we want to write a class that works against an arbitrary type without actually knowing what that type is. We might find out when we go to use that class elsewhere in our code, or we might want to use it with a range of types and therefore cannot make it type specific. A good example of such a class is Apple's NSArray. It stores objects of type id because it could be used to store any type of object. However when we want to use an NSArray, we often only want to store one type in it. An array of numbers, or accounts, or users, etc.

The problem with the collections classes this is that there is no way for them to know what type of objects we want to store. Therefore they cannot validate the objects we pass and their methods have to use the id type for arguments. Which means:

  • There is nothing to stop us accidentally adding an object of the wrong type. For example, putting a NSString in an array of MyUser objects.

  • XCode cannot indicate problems. Our first indicator occurs at runtime when the app acts strangely or simply crashes.

  • Finally code completion is unable to help us because it see's all the methods as taking id types.

Generics solve this problem. They provide a way for us to give the classes a 'hint' as to what type of objects it will be working with. This can then flow on to the compiler so it fails if we accidentally try to put the wrong thing in it. XCode's code completion can also make use of the information to give us better hints as to what the class expects.

Generally speaking this sounds like a trivial thing and perhaps thats why Apple hasn't highlighted it. But it's actually more important than that, and once you've gone Generic, you won't go back. Trust me on that.

Apple's Generics

So lets start with Apple's Generics as mentioned in the XCode 7 release notes. In those Apple says it has enabled support for Generics on it's collection classes. So lets use them as our introduction.

First, here's a snippet from a unit test that shows how we can put anything into a NSSet.

-(void) testSet {
    NSMutableSet *cats = [[NSMutableSet alloc] init];
    [cats addObject:[[Cat alloc] init]];
    [cats addObject:[[Dog alloc] init]];
    [cats addObject:[[Droid alloc] init]];
}

As you can see the NSSet as we know it is quite happy to store things that are not cats and not even animals. You can also see in this screen dump how code completion doesn't help much either. Seeing everything as id and giving no hints we might have things wrong.

Now lets take a look at this same set using a generic declaration to tell the set what we are going to store in it.

-(void) testSet {
    NSMutableSet<Cat *> *cats = [[NSMutableSet alloc] init];
    [cats addObject:[[Cat alloc] init]];
    [cats addObject:[[Dog alloc] init]];
    [cats addObject:[[Droid alloc] init]];
}

By specifying <Cat *> after the class we are telling the set that the objects that will be passed to it will be objects of type Cat. Looking at the code in XCode we now see two errors (I have turned on displaying warnings as errors in my build settings) on the lines where we attempt to put a Dog and Droid into the set. Because the set now knows what to expect it can trigger the errors.

Notice how the errors show the passed and expected types. Now if we look at the XCode's code completion popup we can also see that the set's methods are now expecting Cat * objects instead of just id objects.

Finally, what about NSDictionary objects. Like NSSet and NSArray they can take Generic declarations. The difference is that you need to specify both the key and values types like this:

NSDictionary<NSString *, MyAccount *> accountsByName = ...  

So adding Generics declarations to collections can help to stop mistakes before they happen and give you increased visibility as to what you are storing in them.

But is that all?

The Generics Apple didn't mention

After fooling around with using Generics on the collections, I decided to see if I could apply the same principles to my own classes. I figured that all I had to do was copy what Apple had done with the collections. And it worked. So lets now take a look at how to 'generify' any class.

First lets take a look at a class I've whipped up. It's called GenericPetHotal and is instantiated any time you need a cattery, kennel or 'droidary' for you pet. Hey - Robots can be pets too!

Headers

Anyway, here's the header file for GenericPetHotel:

@interface GenericPetHotel<__covariant ObjectType> : NSObject
-(void) checkinPet:(ObjectType) animal withName:(NSString *) name;
-(ObjectType) checkoutPetWithName:(NSString *) name;
@end

There are two parts to this. First, after the name of the class we have a list of names that will be used as placeholders. These stand in for the types you will specify when using the class. Here I've followed Apple and used ObjectType as the placeholder name. it's not a keyword. just a name that is used to represent the Generic argument you pass into so that the compiler knows where to reference them. In other words

GenericPetHotel<Cat *> ...  

Will caused all occurrences of ObjectType in the header to be replaced with Cat *. Thats why things like code completion can then work with it.
ObjectType can actually be any name you like. In Java, you would typically see a single capital letter such as X or Y. Apple has decided to go with more descriptive names.

At the moment I've not been able to find out what the __convariant keywords means exactly, but it appears to be optional. Apple use it, so I do as well.

The second part of creating a 'Generified' header is simply replacing class references where ever you need to with the appropriate placeholder names.

implementation

Pretty simple and so far, just works. Now lets take a look at the implementation:

@implementation GenericPetHotel {
    NSMutableDictionary<NSString *, id> *_pets;
}

-(instancetype) init {
    self = [super init];
    if (self) {
        _pets = [[NSMutableDictionary alloc] init
    }
    return self;
}

-(void) checkinPet:(id) animal withName:(NSString *) name {
    _pets[name] = animal;
}

-(id) checkoutPetWithName:(NSString *) name {
    return _pets[name];
}
@end

There's a couple of this going on here. Firstly you will notice that we are not using ObjectType anywhere. It appears that the name of a Generic reference is only usable in the header of a class. I've tried all sorts of syntaxes to see if there was a way to declare and use it in the implementation, but I've been unsuccessful. So I've concluded that Objective-C Generics simply don't make the placeholders available to implementations. Unfortunately I cannot confirm against Apples code.

What I have concluded is that when a Generic is resolved by the compiler, it resolves to the top most class that can replace it. Normally this is id, however bounded Generics can make this more specific. See Restricting Generics below.

So the second thing to notice is where ever a Generic name has been used in the header, it's now replaced with an appropriate type.

Finally you will notice that I've used a NSMutableDictionary which I've specified as being keyed by a NSString * and containing id values. They're EveryWhere!

That's it. Thats all I needed to do to create my own 'Generified' classes.

Restricting Generics

If you take a look at the headers of NSSet, NSArray, etc you will see them look something like this:

@interface NSSet<__covariant ObjectType> : NSObject ...
    ...
    @property (readonly, copy) NSArray<ObjectType> *allObjects;
    - (nullable ObjectType)anyObject;
    - (BOOL)containsObject:(ObjectType)anObject;
    ...
@end

Now when the compiler sees a simple placeholder name (ObjectType), it replaces it with id when the class is used. But sometimes you don't want that. Sometimes you want to restrict what sort of classes can be specified. For example, this makes sense:

GenericPetHotel<Cat *> cattery = ...  

But this doesn't:

GenericPetHotel<NSString *> cattery = ...  

So we need a way to be able to specify a limitation on what can be set as the Generic type of a class when a variable is declared. To do this we can add a parent class qualifier to the Generic declaration of the class when we create it. Here's how we declare GenericPetHotel to only allow declarations where the Generic class is an Animal or a class which extends Animal.

@interface GenericPetHotel<__covariant ObjectType:Animal *>  : NSObject

With this declaration, code such as GenericPetHotel<NSString *> cattery = ... will fail to compile:

Another side effect of this (although it's up to you) is that your implementation code can now be more specific. So instead of:

-(void) checkinPet:(id) animal withName:(NSString *) name { ...
-(id) checkoutPetWithName:(NSString *) name { ...

We can use the following quite safely:

-(void) checkinPet:(Animal *) animal withName:(NSString *) name { ...
-(Animal *) checkoutPetWithName:(NSString *) name { ...

I've also found that you can use protocols as well.

@interface GenericPetHotel<__covariant ObjectType:id<Swim>>  : NSObject

In other words whatever class is specified when using this class, it must implement the Swim protocol. Of course, you can also use protocols when declaring an instance of a Generic class:

GenericPetHotel<Animal<Swim> *> *theBath = [...  

To ensure that only animals that can swim are checked in.

The compiler refers to declaring types like this as Bounded types.

Summary

Hopefully this has given you a taste of what can be done with Generics in Objective C. I know I'll be using them as much as possible from now on. For catching un-intentional collection errors if nothing else, but I'll probably be writing Generic classes as well. I hope you find them useful.


Comments powered by Disqus