|
Objective-C: Dynamite!by Andrew M. Duncan, author of Objective-C Pocket Reference04/28/2003 |
"Objective-C is the result of adding object facilities to C with the goal of making programmers more productive. The result differs greatly from C++, which adds objects to C without making computers less efficient: quite a different goal." [PC Week, November 3, 1997]
So what's the big deal about Objective-C? Well, you've heard about objects, right? Encapsulating data and operations, inheriting features from ancestors, maybe even the mysterious "polymorphism." These are all commendable organizing principles for software, but there are many roads to Rome. If you have some experience with C++, Java, Eiffel, or Object Pascal then you've seen these ideas in action. What could another OO language have to offer?
Ask this question of an experienced Smalltalk programmer and be prepared for a lengthy response. Smalltalk adds to procedural programming a simple, elegant object model. In addition, Smalltalk's object mechanisms are relatively "open." That is, you can look at the way objects and classes are represented (classes are objects), and how methods are called. Objective-C was conceived as a way to add Smalltalk's object model to the plain old C language. It's remarkably successful at doing this.
Like Smalltalk, Objective-C is a simple language. Beyond its C roots, there are only a few constructs and concepts to learn, but their consequences are quite rich. Objective-C provides a simple and uniform way to encapsulate data as objects and send messages between them. Languages like C++ and Perl, with their Byzantine complexity, let you in essence construct your own dialects and idioms. But one person's dialect is another's incomprehensible gibberish. Haven't you had to debug stuff like this?
|
Related Reading
|
Objective-C is also (like Smalltalk) a dynamic language. Briefly, this means that Objective-C defers, until runtime, decisions more static languages (such as C++) would perform at compile time. This lets you do a number of interesting things that would be awkward or simply impossible in a less dynamic language. These techniques include:
Of these, the most fundamental and most characteristic of Objective-C is the last: "typeless" declarations, also called dynamic typing. In this article, I'll discuss this topic at greater length, and explain why this distinctive feature of Objective-C is particularly advantageous.
To be clear about this, at runtime every Objective-C object has a type: it's an instance of a particular and unambiguous class. This does not differ from any of the other popular object-oriented languages. With dynamic typing you don't need to declare in your source code what the type of an object will be, for the edification of the compiler. Objective-C supports fully static typing, just as in C++ or Java, but you can choose not to use it.
How can you get away from specifying an object's type, and why would this be a good thing, even a Good Thing? In C++, Java, and many other OO languages, you have to declare the type of the receiver--the object whose method you are calling--in code such as this:
Circle c; // Declare type of variable "c".
c.draw(); // Compiler knows c has this method.
The compiler can guarantee (absent erroneous typecasting, of course) that the receiver's class will support the methods you call on it. There is a school of thought that holds this to be an important tool for checking the correctness of a program.
However, this sort of compiler checking is not only desirable in C++ but mandatory: not for reasons of program design, but due to the mechanism of method dispatch. When a C++ compiler encounters a method call, it emits code to index into a class-specific table whose layout it has already determined. Now, to cover its rear, the compiler must prevent the wrong table from being used at runtime. If it fails, subsequent execution is undefined; almost anything can happen, and it will always be wrong.
This is an unwelcome intrusion of implementation details into the conceptual design and practical use of a language. What's so bad about sending a message that is not understood? If a person on the street asks you for a flump, and you don't know how to respond, do you exit with a core dump? Do you feel that a Higher Power should prohibit the question from even being asked?
Objective-C takes a simpler approach to method calls: it maintains runtime information about the class hierarchy, and searches it at call time for the appropriate code to handle each method call. The speed of modern processors, hardware tricks like branch prediction and speculative execution, and Objective-C runtime tricks like caching of search results, make this method-call implementation fast enough for all but the most nested of loops. In addition, this procedure can tell when the receiver has no matching method, and so can handle this situation in a defined way. Objective-C provides default behavior for unhandled messages but lets you customize it at will.
Objective-C refers to this approach to calling methods as sending messages to objects, which describes well this outlook: inter-object communication is not like a function call, but more like mailing a letter, which gets delivered by a mechanism that is slower but more flexible than just branching to a new execution address. This makes possible all the techniques mentioned in the bullet points above. In addition, distributed programs are easier to design and implement, because message sending does not depend on one distributed component's knowing the inheritance hierarchy of another. For an example of how adherence to the C++ style of method dispatch complicates remote messaging, see the COM model.
So, Objective-C's dynamic model lets you be more loose with your data types. What good is this? Many of us have grown up with rather strictly typed languages, and have a vaguely Puritan unease with loosely typed languages. Doesn't this open up a whole can of worms? Why isn't dynamic typing "considered harmful"?
|
Adding types to languages is an attempt to provide some guarantee that data objects will not be interpreted incorrectly--a float treated as an integer, a Square as a Socket, and so on. However, making a strict guarantee of this sort is undecidable; that is, no program (or finite chain of reasoning) can detect such bugs reliably. It will either reject some correct programs or accept some buggy ones, or it will go into an infinite loop and never tell us.
How then do type-checking compilers work? They have to make a Faustian bargain: they are guaranteed to terminate with an answer, but that answer is sometimes wrong. And for safety's sake, they must always err on the side of excessive safety: they will reject some programs that are type-safe and would never crash due to a type error.
Here's an example of the sort of design made simpler by dynamic typing. I often write code in which I want to send a message from one object to another when some event occurs. This is an application of the Observer design pattern. A typical C++ solution looks like this:
// An abstract class declaring the interface.
struct Observer {
void stuffHappened() = 0;
};
// Implementing the interface.
class Whatever : public Observer { ... };
// The interface of the Sender.
class Sender {
Observer* observer;
void someMethod();
};
// Implementation of Sender.
void Sender::someMethod {
// ... stuff happens ...
observer->stuffHappened();
};
Now, this is a reasonable solution for the problem, but as you see it here, it's a fait accompli. You have already:
The interface is a form of coupling between the Sender and Observer. Those two classes may just be in separate packages, or the Sender may be in library code from another vendor, and you are supplying the Observer. In either case, both sides have to know about the interface declaration.
This looks good as a finished design, but it's hard to arrive at such a design when you have to work this way. Interface designs change, method names fluctuate, both Sender and Observer may decide to send or observe different things. In other words, with static typing you have to know your solution before you get started.
The Objective-C way of doing this is:
// The interface of the Sender.
@interface Sender {
id observer; // Some kind of object.
-(void) someMethod;
@end
// Implementation of Sender.
@implementation Sender
-(void) someMethod {
// ... stuff happens ...
// Obj-C syntax for same method call.
[observer stuffHappened];
}
@end
What's the difference? Here you have only specified the Sender. You don't waste any time specifying the interface the Observer has to support. You have declared it as type id, which means, "some kind of class (that is, not a built-in int), but we're not being any more specific." In particular, you won't know until runtime whether the object really does support the stuffHappened message. Objective-C lets you customize the handling of (otherwise) unhandled messages: this can range all the way from nothing at all to rerouting the message to a delegated substitute object to terminating the program with a runtime error.
So what? Doesn't this amount to so much backsliding from the hard-won lessons of data typing? Why is this not just an example of lazy programming?
There's the reasoned response, which goes like this: During development, a lot of design issues are in flux. It's a lot easier to work out how your framework is supposed to behave without having to start out in a most-factored state, with plenty of empty interface fluff. In fact, Objective-C lets you use static typing as well, so once you settle on a design, you can declare your types with no penalty. Best of both worlds.
Then there's the theoretical response: Objective-C objects are more flexible because of this tolerance for unrecognized messages. In effect, they are already implementers (in an empty sense) of an infinite number of as-yet-undesigned interfaces. You can plug them into all sorts of designs, and if they have anything to offer, any service to provide, they will provide it; otherwise they will just get out of the way.
Finally there's the radical response. Program safety is not decidable anyway. In any nontrivial program there are many bugs, among which type errors are a small subset; and an unhandled message may not even be a bug. And in any case, Objective-C provides a mechanism for intercepting and either discarding or redirecting unhandled messages. If it really is a bug, wouldn't you prefer logging an error to calling some unexpected (and definitely wrong) method, as you would do in C++ if you called a method on an object that doesn't have it? (You would have to use casting to do this, to get around the compiler's static type-checking, of course.)
This sort of design often crops up in GUI frameworks, where it's tedious (at best) for the designer to try to anticipate all the kinds of messages, and their Senders and Observers. Instead, buttons, sliders, and all the kinds of custom controls that make GUIs useful can just accept a generic Observer. For a good example of how Objective-C's dynamism can be used to create elegant program frameworks, look at Apple's Cocoa library, in particular the Application Kit and its Responder Chain.
In summary, relaxing the iron grip of static type-checking lets you write simpler, more elegant program architectures. Objective-C gives you the option of enforcing the law where you think corporate polluters will do the most harm, but looking the other way in coffeehouses where creative freedom is needed. To be sure, this is a double-edged sword; you can find more extensive discussion here.
Andrew M. Duncan started programming in FORTRAN on Control Data 6600 hardware in 1974, and a quarter century later progressed to Mac OS X.
O'Reilly & Associates recently released (December 2002) Objective-C Pocket Reference:
Sample Excerpt: Object Life Cycle, is available free online.
You can also look at the Table of Contents, the Index, and the Full Description of the book.
For more information, or to order the book, click here.
Return to the O'Reilly Network.
Copyright © 2007 O'Reilly Media, Inc.