Objective-C Programming/in depth
Now that you have familiarised yourself with the basics of writing a class (you should now be able to construct a simple class or two, and manipulate them), let's have a look at some of Objective-C's deeper features.
Dynamic typing and binding
[edit | edit source]This is one of Objective-C's strongest features, and allows for greater flexibility and simplicity.
Let's look at something we only glossed over before: the type id.
The id type
[edit | edit source]Objective-C provides a special type that can hold a pointer to any object you can construct with Objective-C—regardless of class. This type is called id, and can be used to make your Objective-C programming generic.
Let's look at an example of what is known as a container class, that is, a class that is used to hold an object, perhaps in a certain data structure.
Say our container class is a linked list, and we have a linked list of LinkedListNode objects. We don't know what data we might put into the linked list, so this suggests that we might use an id as a central data type. Our interface then might look like
@interface LinkedListNode : NSObject
{
id data;
LinkedListNode *next;
}
- (id) data;
- (LinkedListNode *) next;
@end
This means that we could store any class of object in any linked list node, and we would be able to recover them from the node as easily.
Consequences
[edit | edit source]Keeping with the previous example, say we have a linked list of Document objects. Say now, we want to calculate the word count of each document in the list, by means of a wordCount method.
But, since an id type can hold a pointer to any object, how do we know that the object has the wordCount method defined? How do we know that what we are getting out is indeed a Document?
We could merely declare different classes for lists containing Documents, or lists containing other types, but this duplicates a lot of functionality, which is not the best practice and we lose the generic nature of our LinkedListNode class.
Objective-C provides you with many different solutions to this problem.
Dynamic binding
[edit | edit source]Say that our linked list doesn't just contain Documents, but has also Spreadsheet and Chart objects as well. Let's assume all these objects have a title method defined, but implemented differently. Since at the core of LinkedListNode is the id class, how would we know which title method will be called when we pull out an object from our linked list? Will it be Document's? Or Chart's? Dynamic binding is a solution to this problem.
Dynamic binding means that when we call a certain object's method, and there are several implementations of that method, the right one is figured out at runtime. In Objective-C, this idea is used, but is also extended - you are permitted to send any message to any object. At first glance this may sound rather dangerous, but in fact this allows you a lot of flexibility.
Let's return to our example. We want to print out all the titles of the objects in the linked list, based on their title method. Dynamic binding means that the right method to be called (Documents's, Spreadsheet's, or Chart's title method) will be figured out at runtime, and we can simply have a loop in the form
LinkedListNode *p;
for(p = start; p != nil /* pointer to no object */; p = [p next])
{
printf("%s\n", [[p data] title]);
}
Dynamic binding is fine for certain circumstances, and can make some other code unexpectedly clearer. However, other circumstances may require more than just this. I want to say more but this is all basics for dynamic binding.
isKindOfClass
[edit | edit source]Say we are not assured that the objects we place into the LinkedListNodes are not all Documents. If we have, for example, a Chart object in one of the nodes, and it doesn't have wordCount defined (in Objective-C terminology, we say that Chart objects do not respond to wordCount), and we try and do something like:
LinkedListNode *p;
int wordcount_sum=0;
for(p = start; p != nil /* pointer to no object */; p = [p next])
{
wordcount_sum += [[p data] wordCount];
}
this would produce a runtime error when p points to the node containing the Chart object, because Chart doesn't respond to the wordCount message. We ask the Chart to give us a word count, but it doesn't know what to do! In Objective-C, in this example, Chart or any other object can do two things:
- fail, and report the error, or
- send the message on to another object to handle.
We'll look at the second situation in the next section. So, in this instance, the program fails and reports an error.
For this instance, the best Objective-C feature for now, is to use Object's isKindOfClass: method. We can add the following check, to get:
LinkedListNode *p;
int wordcount_sum=0;
for(p = start; p != nil /* pointer to no object */; p = [p next])
{
if([p isKindOfClass:[Document class]])
wordcount_sum += [[p data] wordCount];
else
continue;
}
This loop will skip over the Chart object in this instance. The class method, class, returns a class object. The type Class is a special type that can hold a pointer to any class object (similar to how id can hold a pointer to any object). The instance method class also returns the class object for the class of the object. This means that if we wrote the test as [p class] == [Document class], we could test whether the object's class is exactly Document. We could also achieve this result using [p isMemberOfClass:[Document class]]. This contrasts with [p isKindOfClass:[Document class]], which will check if the object is that class or any of its subclasses (since, due to inheritance, any instance of a subclass "is a" instance of the superclass too). In most cases, isKindOfClass: is the better way to test whether an object is an instance of a class.
This sort of behaviour is a way that polymorphism is implemented in Objective-C. Polymorphism is a feature of methods that can be applied to different types, which could have different behaviour when applied to those types.
This sort of behaviour can, of course, become unwieldy when used improperly. Objective-C gives you, however, ways of addressing this. We can either make sure that all objects that we may want to work on have a predefined set of methods implemented, by using what is known as a protocol, or we can "tack on" extra methods to the class, to eliminate this redundant checking, by using what is known as a category. Let's look at protocols first.
Protocols
[edit | edit source]Spies and secret agents must need to make themselves known to other agents, in order to collect information from others. However, the agents must know whether the other spy or agent is really on their side—the bad guys could send a fake spy in to give the other spies false information! So some form of authentication is usually used, such as a password or a passphrase, or a secret handshake, or they may meet at a certain pre-arranged location. All these are examples of protocols to establish whether a person is trustworthy enough.
Objective-C uses protocols in exactly the same manner. An id holds any object—we want to make sure that if we call some method upon the contents of an id variable, it is guaranteed to respond to it.
A protocol, in Objective-C, is a list of method declarations that any class that wishes to adopt the protocol must implement.
We write a protocol in a similar way to writing an @interface declaration. Here is one typical protocol.
@protocol Document
- (int) wordCount;
- (id) title: (char *) new_title;
- (char *) title;
@end
Note that we can call the protocol the same as a class—this is okay. It may be useful to place this Document protocol in the same header as the Document class, or we may want to keep it in a separate file.
If we want to signify that the Document class adheres (or is said to conform, in Objective-C) to the Document protocol, we must say this in the @interface declaration. We do this by writing
@interface Document <Document>
The <Document> means that the interface to the Document class adheres to the Document protocol.
Let's revisit our LinkedListNode class. Recall we wanted to get the sum of all the words in each document. But, in our LinkedListNode class, the core datatype is an id. There are basically no restrictions on what we can put into an id variable. Ideally, what we'd like to do is say something along the lines of "all objects that adhere to the X, Y or Z protocol". We use angle brackets again to specify this.
Let's create a new class, call it LinkedListDocument. This class only has in each node an object that conforms to the Document protocol. Instead of saying id data in the interface, we say instead:
@interface LinkedListDocument : Object
{
id <Document> data;
In order to use the protocol in the type specification, or in the interface specification, we must import the header file in which the protocol is defined.
"Inheritance" of protocols
[edit | edit source]A protocol can incorporate other protocols that have been defined, in a similar way to inheritance, but there is no hierachy imposed upon protocols like there are in classes. For example, in the Document protocol, we could write it this way instead:
@protocol Document <Countable, Titleable>
@end
@protocol Countable
- (int) wordCount;
@end
@protocol Titleable
- (id) title: (char *) new_title;
- (char *) title;
@end
This allows us to structure and compartmentalize our protocols at a greater level. However, note that a protocol's method declarations can be empty! This means that we can use "empty" protocols to mark or group certain objects based on a higher level than merely what methods the conforming objects respond to (for example, creating an "empty" GraphicsBased or TextBased protocol in order to set the printer mode, which Document, Spreadsheet, or Chart objects could adopt).
conformsTo
[edit | edit source]Say we now want to write an addDocument: method to our LinkedListDocument class. We could write something like
- (BOOL) addDocument: (id <Document>) newDocument
{
...
which means that any object we must pass in to the addDocument method must conform to the Document protocol. This may be a little restrictive. We may want to have a declaration like
- (BOOL) addDocument: (id) newDocument
and return NO when the new document doesn't conform to the Document protcol, for instance. How can we do this? We may want something that works a little like isKindOfClass:. Objective-C provides us with a method conformsTo: for just this purpose. We can then write addDocument: like
- (BOOL) addDocument: (id) newDocument
{
if([newDocument conformsToProtocol:@protocol(Document)])
{
//add the document...
return YES;
}
else return NO;
}
Note the @protocol(Document) symbol. This acts in just the same way as [Document class] does, except that @protocol(Document) returns a Protocol object instead of a class object.
If we however have a protocol, and we want to test whether it incorporates another protocol, we can use conformsTo: as well. For example, given the protocols above ("Inheritance" of protocols),
[@protocol(Document) conformsToProtocol:@protocol(Countable)]
would give YES as a result.
- In Java, the keyword "interface" means the same as "protocol" in Objective-C . Objective-C uses "interface" to define a declaration for a class , as in "C++" , so the declaration can be used in header files which can be included in client source files for use. The older C++ and Objective-C do not separate the concerns of defining storage layout and function address tables , so an Objective-C "interface" can only be used for one implementation file , whereas in Java, only non-storage declarations such as constants and function names and inner interfaces are defined in interface definitions, so these can be reused amongst various implementation classes. The C++ equivalent of a protocol is a abstract class , where in the declaration definition, all the functions end with " = 0;" . In C++, object implementations of abstract classes can't be received as copy constructed parameters via functions , but can be received as abstract class pointers. In Objective-C , every object made by calling alloc on a class and instantiated with a init function is returned as a pointer to an object , and there seems to be no mechanism for automatic allocation and deallocation on the stack in a function scope , as occurs with non-pointer variable declarations within the body of a function, as in C++. Java also follows the objective-C lead of only allowing object creation via the "new" keyword and a constructor . In Java, interfaces are regarded as a kind of superclass relationship, so "X.class.isAssignableFrom(o)" can be used to test for conformance of an object o to a interface X.
Categories
[edit | edit source]Let's return now to our previous example, where we have LinkedListNodes which do not contain only Documents. We want to get the sum of all the words in all the Documents in the linked list. Recall our previous solution that we added a check
if([p isKindOfClass:[Document class]])
wordcount_sum += [[p data] wordCount];
else
continue;
However, this sort of check isn't quite the best solution, on a couple of levels. If we've only got Document, Spreadsheet, and Chart objects, and neither Spreadsheet nor Chart objects have a way of providing a word count, then, in essence, their word count could be said to be 0. What we want to do, now, is somehow "stick on" a small implementation of wordCount that simply returns 0 for objects that have no word count, and then we can remove the check that we have before and merely let dynamic binding do it's work.
Categories provide this way of "sticking on" extra methods, as an alternative to subclassing. What categories do, is a way of building up a class based on separate pieces that are logically grouped together, for example, by category (hence the name).
So, how do we define a category? We write it in a similar form to the @interface declaration, but we do this separately.
Here's an example of a WordCount category, on the Spreadsheet class. We first create an interface for the category:
@interface Spreadsheet (WordCount)
- (int) wordCount;
@end
which goes into a header file, and then the implementation of the category
@implementation Spreadsheet (WordCount)
- (int) wordCount
{
return 0;
}
@end
which can go in a .m file. Importing the category's header file means that the category is being used. We can do a similar procedure for Chart or any other object we need to add a category to.
Now, Spreadsheet or any other object that has this category, now has a valid wordCount method defined upon it (albeit a simple stub method, but we can do anything we like otherwise), so writing the loop
for(p = start; p != nil /* pointer to no object */; p = [p next])
{
wordcount_sum += [[p data] wordCount];
}
will be successful since if [p data] returns a Spreadsheet, the wordCount method will be used.
Note: methods declared in categories override preexisting methods. This means that we can do some neat tricks (see the Advantages section below). Also beware that the behaviour in picking a method declared in two categories is undefined.
Advantages of categories
[edit | edit source]Categories allow us to split up a large class into several pieces. Each can be developed and created independently, and then brought together at the end to form the class.
As a consequence however, this allows us some nice advantages. We can, in essence, extend the functionality of any class that may fall into our hands, whether it is in a class you create yourself, or in a binary form, such as in the OPENSTEP/Cocoa/GNUstep frameworks, we can add a category to NSString to provide functionality that the original NSString did not provide. It is possible to even add categories to Object! Categories can also be used to override previous functionality, which allows us then to "patch" these preexisting classes also, which might be provided with a bug. However, this should be done carefully and only as necessary.
A word on categories
[edit | edit source]And to finish off this section, let's hear some words of wisdom about categories:
- What is truly relevant and exciting is how naturally logic is shifted to the appropriate class. Any time Objective-C code looks at the class of an object (using the -isKindOfClass: [isKindOf] method), it is likely that there exists a better, more efficient implementation using categories. This can be generalized with a design heuristic: "Let the runtime do the work of deciding the class of an object". By using the runtime effectively, a developer can often remove some if/then and most case statements. The benefit is obvious: simpler, cleaner code and better performance, since the runtime is optimized for this task.
example of polymorphism to eliminate if..then and switch..case type testing
[edit | edit source]Elimination of case statements using polymorphic child classes of a base type or category is mundanely shown in this example:
During runtime of a Snake touchscreen game, a user changes the state of the Snake by touching the screen . The snake object has 4 properties of East , West, North, and South MovementAndResponseState objects, all which have a reference to the owning Snake object and can access needed properties of the Snake object. The MovementAndResponseState class is composed of a MovementCategory and ResponseCategory, the movement category handles the movement of the snake in a particular direction by determining the next position on the snake's list of positions, and the response category handles the response to touch, by determining which of East, West, North, or South properties should be the Current direction property of the Snake object, in response to a user touch event . The snake object would have the following delegate pattern:-
-(void) move {
[self.state move];
}
-(void) handleTouches: (NSSet*) touches forEvent : (UIEvent) event {
for (UITouch touch in touches )
[ self.state handleTouch: touch forEvent: event ] ;
}
-(void) touchesBegan: (NSSet*) touches forEvent : (UIEvent) event {
[self handleTouches: touches forEvent: event];
}
-(void) touchesCancelled: (NSSet*) touches forEvent : (UIEvent) event {
[self handleTouches: touches forEvent: event];
}
-(void) touchesMoved: (NSSet*) touches forEvent : (UIEvent) event {
[self handleTouches: touches forEvent: event];
}
-(void) touchesEnded: (NSSet*) touches forEvent : (UIEvent) event {
[self handleTouches: touches forEvent: event];
}
The generalization is that in applications that respond to arbitrary user input and are event driven, alternatives exist to the main window loop with a massive case statement that type tests which event type occurred and dispatches to custom handling code.
- " http://en.wikipedia.org/wiki/Message_loop_in_Microsoft_Windows " describes the venerable message loop-and-case-statements, and mentions frameworks later hiding this stuff, e.g. having GUI designers that link the handler code to a designed graphical element ala delphi-style properties , cocoa-style arrow dragging between designer editor and program text editor, or per-gui-control-object observer pattern "addXXXListener" callback registries , as in sun java's awt and swing frameworks and eclipse swt framework.
The Objective-C programming language: Objective-C concepts - Objective-C syntax - Objective-C in depth - Objective-C advanced features