Programming Mac OS X with Cocoa for Beginners/Objective C, the language and its advantages
Previous Page: First Cocoa program - Hello World | Next Page: Some Cocoa essential principles
In the last chapter, we were introduced to Objective-C, and used it to write the "Hello World" code in an object-oriented fashion that worked with Cocoa. Now we will learn more about Objective-C as a language, and why it has certain advantages. This isn't meant as a complete guide to Objective-C, more of an overview. There is another wikibook that goes into far more detail. The original goal of Objective-C was to create a language that is efficient and productive, while avoiding being overburdened with too many features that are often only of theoretical benefit. As a strict superset of C, this does mean that some of C's drawbacks remain in Objective-C, but on the other hand it makes it much faster to learn. Becoming an expert in C++, with its myriad of features, can take years. It's probably fair to say that this learning curve for C++ means that much C++ code is not written as perfectly as it could be. Mastering Objective-C, by contrast, is quite easy, so code quality can be higher at an earlier stage. That said, Objective-C doesn't provide other features which can reduce bugs, such as const (read only) objects, and so forth. (In Cocoa, read-only objects are established by design, for example NSString is read only, whereas NSMutableString is read/write).
We have already seen much of the basic Objective-C syntax. Object methods are accessed using the square bracket notation, and parameters are passed using colons and parameter names. Methods are just like functions in C, only the syntax of how they are called differs. This different syntax is required because we need to specify WHAT object we are referring to, and WHICH method we'd like it to carry out. The square brackets link these two pieces of information together. Other than that they are just like functions — they can return an optional value, and take any number of parameters.
The body of a method is also just like a function. We start with the name of the method and its return type, then enclose the method's code in curly braces, just as in C. Within the code, we have seen that we can refer to the object itself using 'self'. This saves us from having to find out our own address, the compiler will handle all of that. Most objects we'll encounter are subclasses of other, already existing objects. In Cocoa, there is a generic base object class called NSObject, which contains a lot of useful functionality that most classes will wish to inherit. So in general we can be sure that every object has an ancestor object. We can refer to the most immediate ancestor object (called the superclass of the object) within any method body using the shorthand 'super'. This is like self, but refers to the immediate ancestor instead. Super is most often used when you need to override a method in your current class with a method in the superclass. Self is used much more often than super, but occasionally we'll need to use super.
We saw how IB helped us out last chapter by creating the skeletons for our custom view class for us. Every class consists of two parts — the interface and the implementation. Usually these are placed in separate files, the .h and the .m respectively. The idea of this two-part design is both to make managing your code easier, and to encourage you to hide implementation details. Remember, objects should be usable from external code without the internal details of how they work being exposed. If we endeavour to hide all of the internal parts from the outside world, we will encounter fewer bugs, because we have eliminated dependencies on those details. Thus the interface (.h) file is used to tell the world about your object's methods, and the implementation file contains the code that makes it work, but should be of no interest to the outside world. If you use external libraries, very often you'll simply not have access to the .m files — only the .h files are available. This is true of Cocoa itself; you will do well to follow the same model.
@interface GCHelloView : NSView
{
/* data members go here */
}
/* methods go here */
@end
Within the curly braces you can add data members (or just members) to the class. Data members are like variables that are owned by concrete instances of the object. Every separate instance has its own copy of the data members, and they can have independent values. Data members are used to store the actual values of the properties of the object.
Methods are just like function prototypes. They go after the data member section, but before the '@end' statement. As in C++, they are required, but unlike C++, only those that are unique to the class are required — overrides of superclass methods don't need to be written out again. This helps the user of the object focus on what is unique and interesting about this object, as opposed to its superclasses. In other words the implementation file is a strict delta or difference file from the superclass.
Let's add some data members and methods, as an example.
#import <Cocoa/Cocoa.h>
@interface GCHelloView : NSView
{
int _age;
int _size;
NSString* _text;
}
- (void) setAge:(int) age;
- (int) age;
- (void) setText:(NSString*) text withSize:(int) size;
- (NSString*) text;
@end
We have added three data members, an integer called _age
, an integer called _size
, and an NSString called _text
. The leading underscores used here are just part of the name, they have no special meaning, but they can be quite handy. Within code you write to implement an object's methods, you can refer to the object's data members simply by naming them, as if they were variables local to that function. By using the underscore, you can tell at a glance that they are data members, and not local variables. Any similar convention you might care to use is a good idea — getting confused between what is local, global or an object's data member is a recipe for bugs.
We also added four methods, setAge:
, age
, setText:withSize:
and text
. Remember, the colons are part of the method's name. Method names should follow this convention in Cocoa. Whenever you want to set a property, create a method called setProperty
, and whenever you want to ask for that property, create a method whose name is the property. Some programmers might be tempted to call such a method getProperty
— don't do this! In Cocoa, this naming scheme is more than just a convention, it is used later to allow us to automatically read and write properties in all sorts of simple and elegant ways that are awkward or impossible in other programming languages.
Since age
is just an integer, couldn't we just refer to it from elsewhere without having to go through all the overhead of calling a method? Yes we could, but we'd be breaking the rule about hiding the implementation. Suppose at a later stage you decided that this object should calculate the age by counting tree rings, instead of simply storing it. If external code was peeking at the integer, it would stop working when we changed the code within the age method so it counted tree rings. So never, never peek at data members from outside of an object. Even within the object, we should usually call our own methods to read our data member's values, since again, if later we change the way it works, we can minimise the number of places that need to be changed by making sure that there is only one main place that deals with that value.
Note how we declare a method's parameter types. We put the type in parentheses following the colon. We can also, as here, nominate a variable name for the parameter, though as in C this is optional. However, since the parameter name is built into the method, leaving it out doesn't leave us with an undocumented mystery, as it does in C. In other words:
- (void) setText:(NSString*) withSize:(int);
gives us all we need to know about what the method does, leaving no mystery. We have the names of the parameters (text and size) and their types (NSString*
and int
). For contrast, in C you might have had something like:
void setText(NSString*, int);
What is the second parameter? Unless the programmer used a more descriptive name or added a comment to tell us, we can't tell.
The return types of the method are also indicated using the same bracket notation. In addition, the leading '-' is important — this tells us that this is an instance method, one that pertains to a particular instance of an object. If we wanted to declare a class method, we would use a '+' symbol instead. This use of + and - allows us to tell at a glance which methods are which in the interface file. C++ by contrast uses the 'static' keyword for class methods.
Finally, notice that the interface imports the Cocoa/Cocoa.h
file at the top. #import
is just like #include
, except that it automatically includes the file only one time, so you don't need to use any #define tricks to ensure it. Here we import Cocoa so that we get the interface for NSView
, of which this is a subclass. At the very least, a class interface must somehow make details of its superclass known, though where you are subclassing a Cocoa object, using #import is best.
Implementation
editThe implementation of an object is placed in the .m file, and is declared like this:
#import "GCHelloView.h"
@implementation GCHelloView
- (id) initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]) != nil)
{
_text = @"Hello World!";
_size = 48;
_age = 16;
}
return self;
}
- (void) setAge:(int) age
{
_age = age;
}
- (int) age
{
return _age;
}
- (void) setText:(NSString*) text withSize:(int) size
{
[text retain];
[_text release];
_text = text;
_size = size;
[self setNeedsDisplay:YES];
}
- (NSString*) text
{
return _text;
}
- (void) drawRect:(NSRect)rect
{
NSPoint p;
NSMutableDictionary* attribs;
NSColor* c;
NSFont* fnt;
p = NSMakePoint( 10, 100 );
attribs = [[NSMutableDictionary alloc] init];
c = [NSColor redColor];
fnt = [NSFont fontWithName:@"Times Roman" size:_size];
[attribs setObject:c forKey:NSForegroundColorAttributeName];
[attribs setObject:fnt forKey:NSFontAttributeName];
[[self text] drawAtPoint:p withAttributes:attribs];
[[NSColor blueColor] set];
NSFrameRect([self bounds]);
}
@end
The implementation of an object is everything between @implementation
and @end
. The methods are coded just as a C function. Here we have our unique methods, setText:withSize:
and so on, and also a couple of methods that we inherited from our superclass, initWithFrame:
and drawRect:
These last two don't appear in our interface file, as they are not a unique feature of this class. To override them, we simply redefine the methods in the implementation.
The setAge:
and age
methods should be fairly obvious — the values are simply assigned to the data member _age
, or return its value. Many methods that simply read and write properties of an Objective-C object are like this — short and simple. It can get a bit tedious to write these all out if you have many properties, but don't be tempted to avoid this, because hiding the implementation is much more important in the long run.
The setText:withSize:
method is a little more complex, since we need to retain and release objects. We'll be covering this in detail in the next chapter, so don't worry about it for now. However, this is required most times that you are using objects as properties of other objects, which will happen a lot. Simple values such as the int _size
can just use simple assignments. The last line, [self setNeedsDisplay:YES]
is an example of calling one of your own methods. Here, we use the method setNeedsDisplay
which is inherited from NSView
. This signals to Cocoa that the view content has changed and should be redrawn. Cocoa makes a note of that and will cause our drawRect:
method to get called in due course, so anyone who makes use of the setText:withSize:
method of our view will see those new settings appear immediately.
The initWithFrame:
method also sets these various data members to some default values, so that the object will work even if nobody ever comes along and sets the text, its size, etc. Every object has an init
method whose purpose is to establish the object's default properties so that it will function properly right out of the box. Sometimes, objects will need parameters sent to the init
method, so a different version of an init
method is needed, as the way initWithFrame:
was coded. Note that the init
method's first job is to call the superclass's initWithFrame:
method. This happens all the way back up the chain of ancestor classes until everyone has initialized what it knows about. So there are two main rules to follow when writing your init
method:
- call your super's
init
method - initialize only what you know about (your own data members only)
The init
method must return 'self' as its result, provided that the super's initialization succeeded. If not, it will return nil and so should you. If your own initialization fails, but your super's succeeded, you should return nil also. This indicates to the caller that something went wrong while making your object. More about this later.
Some Objective-C advantages
editLike C++ and other object-oriented languages, Objective-C supports polymorphism. This means two different things. In the first place, it means that you can have different methods that differ only according to their parameter types, and the correct one will be called according to what you're passing. This is static polymorphism, so in C++:
void setAge(int);
void setAge(float);
are two distinct methods. The right one is called depending on whether you pass an int or a float. While handy, this can get confusing, especially when automatic type conversion is taking place, as it frequently does between simple scalar values. Objective-C doesn't support this type of potentially ambiguous polymorphism. Instead, because parameters are an inherent part of a method's name, you can make it much clearer, e.g:
- (void) setAgeWithInt:(int);
- (void) setAgeWithFloat:(float);
There is no ambiguity, since you have to name which one you mean explicitly. This might reduce the potential for bugs for some people, though C++ programmers might grumble.
The second kind of polymorphism is dynamic polymorphism. We have already seen this in action in our simple program. When Cocoa calls the view's drawRect:
method, it actually doesn't have a clue that the object in question is a GCHelloView
, and not a plain vanilla NSView. It calls our object as if it were an NSView. The method call resolves to our custom object however, and so our code gets called, allowing it to do its thing. All object-oriented languages need to support this kind of polymorphism, so the fact that Objective-C does too is no surprise.
What is different is that Objective-C resolves the method at runtime. This is in contrast to languages such as C++ that resolve everything at compile time, building a set of fixed tables that allow this apparent magic to occur. For the typical case, this runtime binding makes no difference. However, there are other cases where the runtime binding makes a huge difference, and this adds a degree of power to Objective-C that is absent from C++ and others. It safely allows us to use anonymous objects, and even null objects. It allows us to create a delayed method call called an invocation that can be used later, and it permits things like Undo to be implemented much more easily. It greatly helps us create a graphical user interface for our code, because commands, actions and so on of various window controls can be made extremely general. It also permits something called "Key-value observing" and "key paths" which are incredibly powerful — more on these later.
Let's suppose we have an object that must respond to a click on a button in a window. Really, the button shouldn't need to care which sort of object it is controlling, only that when it is clicked, it should do something. Let's suppose a method exists called action:
, and the button knows that it should call this on an object called 'target' when clicked. In a traditional language, the target object's type must be known to the button, so that it can determine if a method called action:
really exists. Once that type is indicated to the button, it can only ever deal with objects of that type, or their subclasses. However, in Objective-C, we could hook up totally unrelated objects to the button, and as long as they implemented action:
, they would respond to the button. This is a great improvement in flexibility. Many GUI elements such as buttons and menus use this type of target/action approach.
An 'anonymous' object has a type of 'id'. Anywhere you see 'id' used as an object type, an anonymous object reference is created. 'id' can stand in for any type of object whatsoever.
In Objective-C, the value nil has a type of id. This means that nil is an object! (NULL, meaning nothing, is still available) In other languages, especially C and C++, we must be careful not to use an object reference of nil, meaning we have to litter our code with lots of checks, like:
if (object != nil) {
object->action();
}
In Objective-C, we don't need to bother. Calling a nil
object's methods is harmless, because of the runtime binding of the method call. Of course the action won't do anything, but it won't crash.
Another example. Let's suppose we want to invoke a periodic action on one of our objects. Cocoa has a handy class for setting up periodic actions, NSTimer
. One day we might want our 'GCWidget' class to have its 'dwim' method called once a minute, another time we have an unrelated HPSprocket class that needs its 'fubar' method called every 5 seconds. In a traditional language, both of these classes would have to inherit from a common class known to NSTimer, and in addition the periodic method would have to have the exact same name. But because of the runtime binding and anonymous objects, NSTimer simply needs to be told the method name to call, and call it on an object type of 'id'. If the object implements the method, it does its thing. If not, nothing bad happens.
Objective-C implements the runtime binding using a mechanism called a selector. This helps make the process a lot more efficient — as you might imagine if a method lookup really had to go through and compare the actual names of the methods letter by letter it would be very slow. Instead, at compile time Objective-C converts every method name into a simple unique number which is much faster to lookup. There is still a cost compared to C++'s approach, but the benefits are worth it. In practice, a method call in Objective-C is probably only about 2 or 3 times slower than a C++ method call.
We can obtain the selector for a method using the '@selector( ... )' function. Sometimes we need to do this when we are setting up targets and actions, as in our timer example above.
Previous Page: First Cocoa program - Hello World | Next Page: Some Cocoa essential principles