In this chapter you'll learn about the features of the Objective-C language that make it such a powerful programming language and that distinguish it from some other object-oriented programming languages such as C++. Three key concepts are described in this chapter: polymorphism, dynamic typing, and dynamic binding. Polymorphism enables programs to be developed so that objects from different classes can define methods that share the same name. Dynamic typing defers the determination of the class an object belongs to until the program is executing, and dynamic binding defers the determination of the actual method to invoke on an object until program execution time.
Program 9.1 shows the interface file for a class called Complex
, which is used to represent complex numbers in a program.
Program 9.1. Interface File Complex.h
// Interface file for Complex class
#import <objc/Object.h>
@interface Complex: Object
{
double real;
double imaginary;
}
-(void) print;
-(void) setReal: (double) a;
-(void) setImaginary: (double) b;
-(void) setReal: (double) a andImaginary: (double) b;
-(double) real;
-(double) imaginary;
-(Complex *) add: (Complex *) f;
@end
We're not going to show the implementation file here; presumably you've already done that in exercise 7 from Chapter 4, “Data Types and Expressions.” If you didn't complete that exercise, perhaps now is the time to do so. We added an additional setReal:andImaginary:
method from that exercise to enable you to set both the real and imaginary parts of your number with a single message.
Program 9.1. Test Program main.m
// Shared Method Names: Polymorphism
#import "Fraction.h"
#import "Complex.h"
int main (int argc, char *argv[])
{
Fraction *f1 = [[Fraction alloc] init];
Fraction *f2 = [[Fraction alloc] init];
Fraction *fracResult;
Complex *c1 = [[Complex alloc] init];
Complex *c2 = [[Complex alloc] init];
Complex *compResult;
[f1 setTo: 2 over: 5];
[f2 setTo: 1 over: 4];
[c1 setReal: 10.0 andImaginary: 2.5];
[c2 setReal: -5.0 andImaginary: 3.2];
// add and print 2 complex numbers
[c1 print]; printf (" + "); [c2 print];
printf (" = ");
compResult = [c1 add: c2];
[compResult print];
printf ("
");
[c1 free];
[c2 free];
[compResult free];
// add and print 2 fractions
[f1 print]; printf (" + "); [f2 print];
printf (" = ");
fracResult = [f1 add: f2];
[fracResult print];
printf ("
");
[f1 free];
[f2 free];
[fracResult free];
return 0;
}
(10 + 2.5i) + (-5 + 3.2i) = (5 + 5.7i)
2/5 + 1/4 = 13/20
Note that both the Fraction
and Complex
classes contain add:
and print
methods. So, when executing the message expressions
compResult = [c1 add: c2];
[compResult print];
how does the system know which methods to execute? It's simple: The Objective-C runtime knows that c1
, the receiver of the first message, is a Complex
object. Therefore, it selects the add:
method defined for the Complex
class.
The Objective-C runtime system also determines that compResult
is a Complex
object.1 So, it selects the print
method defined in the Complex
class to display the result of the addition. The same discussion applies to the following message expressions:
fracResult = [f1 add: f2];
[fracResult print];
The corresponding methods from the Fraction
class are chosen to evaluate the message expression based on the class of f1
and fracResult
.
As mentioned, the ability to share the same method name across different classes is known as polymorphism. It enables you to develop a set of classes that each can respond to the same method name. Each class definition encapsulates the code needed to respond to that particular method, and this makes it independent of the other class definitions. It also enables you to add new classes at a later date that can respond to methods with the same name.
id
TypeChapter 4 briefly touched on the id
data type and noted that it is a generic object type. That is, it can be used for storing objects that belong to any class. The real power of this data type is exploited when it's used this way to store different types of objects in a variable during the execution of a program. Study Program 9.2 and its associated output.
// Illustrate Dynamic Typing and Binding
#import "Fraction.h"
#import "Complex.h"
int main (int argc, char *argv[])
{
id dataValue;
Fraction *f1 = [[Fraction alloc] init];
Complex *c1 = [[Complex alloc] init];
[f1 setTo: 2 over: 5];
[c1 setReal: 10.0 andImaginary: 2.5];
// first dataValue gets a fraction
dataValue = f1;
[dataValue print];
printf ("
");
// now dataValue gets a complex number
dataValue = c1;
[dataValue print];
printf ("
");
[c1 free];
[f1 free];
return 0;
}
2/5
10 + 2.5i
The variable dataValue
is declared as an id
object type. Therefore, dataValue
can be used to hold any type of object in the program. Make sure to note that there is no asterisk used in the declaration line:
id dataValue;
The Fraction f1
is set to 2/5, and the Complex
number c2
is set to (10 + 2.5i). The assignment
dataValue = f1;
stores the Fraction f1
into dataValue
. Now, what can you do with dataValue
? Well, you can invoke any of the methods that you can use on a Fraction
object with dataValue
, even though the type of dataValue
is an id
and not a Fraction
. But, if dataValue
can store any type of object, how does the system know which method to invoke? That is, how does it know when it encounters the message expression
[dataValue print];
which print
method to invoke? You know you have print
methods defined for both the Fraction
and Complex
classes.
As noted previously, the answer lies in the fact that the Objective-C system always keeps track of the class to which an object belongs. It also lies in the concepts of dynamic typing and dynamic binding—that is, making the decision about the class of the object and therefore which method to invoke dynamically at runtime rather than at compile time.
So, during execution of the program, when the system goes to send the print
message to dataValue
, it first checks the class of the object stored inside dataValue
. In the first case of Program 9.2, this variable contains a Fraction
, so it is the print
method defined in the Fraction
class that is used. This is verified by the output from the program.
In the second case, the same thing happens. First, the Complex
number c1
is assigned to dataValue
. Next, the message expression
[dataValue print];
is executed. This time, because dataValue
contains an object belonging to the Complex
class, the corresponding print
method from that class is selected for execution.
This is a simple example, but I think you can extrapolate this concept to more sophisticated applications. When combined with polymorphism, dynamic binding and dynamic typing enable you to easily write code that can send the same message to objects from different classes.
For example, consider a draw
method that could be used to paint graphical objects on the screen. You might have different draw
methods defined for each of your graphical objects, such as text, circles, rectangles, windows, and so on. If the particular object to be drawn is stored inside an id
variable called currentObject
, for example, you could paint it on the screen simply by sending it the draw
message, like so:
[currentObject draw];
You could even test it first to ensure that the object stored in currentObject
does in fact respond to a draw
method. You'll see how to do that later in this chapter.
Because the type of object stored inside an id
variable can be indeterminate at compile time, some tests are deferred until runtime—that is, while the program is executing.
Consider the following sequence of code:
Fraction *f1 = [[Fraction alloc] init];
[f1 setReal: 10.0 andImaginary: 2.5];
Recalling that the setReal:andImaginary:
method applies to complex numbers and not fractions, the following message will be issued when you compile the program containing these lines:
prog3.m: In function 'main':
prog3.m:13: warning: 'Fraction' does not respond to 'setReal:andImaginary:'
The Objective-C compiler knows that f1
is a Fraction
object because it has been declared that way. It also knows that, when it sees the message expression
[f1 setReal: 10.0 andImaginary: 2.5];
the Fraction
class does not have a setReal:andImaginary:
method (and did not inherit one either), so it issues the warning message shown previously.
Now consider the following code sequence:
id dataValue = [[Fraction alloc] init];
...
[dataValue setReal: 10.0 andImaginary: 2.5];
These lines do not produce a warning message from the compiler because the compiler doesn't know what type of object is stored inside dataValue
when processing your source file.
It's not until you run the program containing these lines that an error message is reported. The error will look something like this:
objc: Fraction: does not recognize selector -setReal:andImaginary:
dynamic3: received signal: Abort trap
When attempting to execute the expression
[dataValue setReal: 10.0 andImaginary: 2.5];
The runtime system first checks the type of object stored inside dataValue
. Because dataValue
has a Fraction
stored in it, the runtime system checks to ensure that the method setReal:andImaginary:
is one of the methods defined for the class Fraction
. Because it's not, the error message shown previously is issued and the program is terminated.
id
Data Type and Static TypingIf an id
data type can be used to store any object, why don't you just declare all your objects as type id
? There are several reasons why you don't want to get into the habit of overusing this generic class data type.
First, when you define a variable to be an object from a particular class, you are using what's known as static typing. The word static refers to the fact the variable is always used to store objects from the particular class. So, the class of the object stored in that type is predeterminate, or static. When you use static typing, the compiler ensures, to the best of its ability, that the variable is used consistently throughout the program. The compiler can check to ensure that a method applied to an object is defined or inherited by that class, and it issues a warning message otherwise.2 Thus, when you declare a Rectangle
variable called myRect
in your program, the compiler checks that any methods you invoke on myRect
are defined in the Rectangle
class or are inherited from its superclass.
However, if the check is performed for you at runtime anyway, why do you care about static typing? You care because it's better to get your errors out during the compilation phase of your program rather than during the execution phase. If you leave it until runtime, you might not even be the one running the program when the error occurs. If your program is put into production, it might be some poor unsuspecting user who discovers that a method is not recognized by a particular object when she is running the program.
Another reason for using static typing is that it makes your programs more readable. Consider the following declaration:
id f1;
versus
Fraction *f1;
Which do you think is more understandable—that is, which makes it clearer to the reader what the intended use of the variable f1
is? The combination of static typing and choosing meaningful variable names (which we intentionally did not choose in the previous example) can go a long way toward making your program more self-documenting.
If you use dynamic typing to invoke a method, you need to make note of the following rule: If a method with the same name is implemented in more than one of your classes, each method must agree in the type of each argument and the type of value it returns. That's so that the compiler can generate the correct code for your message expressions.
The compiler performs a consistency check among each class declaration it has seen. If one or more methods conflict either in argument or return types, the compiler issues a warning message. For example, both the Fraction
and Complex
classes contain add:
methods. However, the Fraction
class takes as its argument and returns a Fraction
object, whereas the Complex
class takes and returns a Complex
object. If frac1
and myFract
are Fraction
objects, and comp1
and myComplex
are Complex
objects, statements such as
result = [myFract add: frac1];
and
result = [myComplex add: comp1];
do not cause any warnings to be issued by the compiler (as you have seen) because in both cases the receiver of the message is statically typed and the compiler can check for consistent use of the method as it is defined in the receiver's class.
However, if dataValue1
and dataValue2
are id
variables, the statement
result = [dataValue1 add: dataValue2];
causes the following warning messages from the compiler:
ex3.m: In function 'main':
ex3.m:12: warning: multiple declarations for method 'add:'
Fraction.h:18: warning: using '-(Fraction *)add:(Fraction *)f'
Complex.h:18: warning: also found '-(Complex *)add:(Complex *)f'
The compiler doesn't know which type of object will be stored in dataValue1
, but it does know that both the Fraction
and Complex
classes define an add:
method. So, it issues a warning that these two methods have been found and that they are not consistently declared. It then arbitrarily assumes you want the add:
method from the Fraction
class. This assumption affects how the compiler generates code to pass arguments to the method and to handle the value returned from the method.
At runtime, the Objective-C runtime system still checks the actual class of the object stored inside dataValue1
and selects the appropriate method to execute. However, the compiler might have generated the incorrect code to pass arguments to the method or handle its return value. This would likely happen in cases in which one method took an object as its argument and the other took a floating-point value. If the inconsistency between two methods is just a different type of object (for example, the Fraction
's add:
method takes a Fraction
object as its argument and returns one, whereas the Complex
's add:
method takes and returns a Complex
object), the correct code is still generated by the compiler because memory addresses are passed for objects anyway. To avoid the warning messages from the compiler, you can declare your arguments and return types to be of type id
, like so:
-(id) add: (id) value;
If you use this when declaring and defining both methods, the compiler no longer complains when the add:
method is invoked on an id
object because the inconsistency no longer exists. You should resort to using these generic definitions only if you will be using dynamic typing on your objects because it makes the methods harder to understand. From the previous declaration, it is not at all clear which type of value the add:
method takes or returns.
As you start working with variables that can contain objects from different classes, you might need to ask questions such as the following:
• Is this object a rectangle?
• Does this object support a print method?
• Is this object a member of the Graphics
class or one of its descendants?
The answers to these questions might be used to execute different sequences of code, avoid an error, or check the integrity of your program while it's executing.
Table 9.1 summarizes some of the basic methods supported by the Object
class for asking these types of questions. In this table, class-object
is a class object (typically generated with the class
method), and selector
is a value of type SEL
(typically created with the @selector
directive).
Table 9.1. Methods for Working with Dynamic Types
Other methods are available that are not described here and that allow you to ask questions about conformity to a protocol (covered in Chapter 11, “Categories, Posing, and Protocols”) and for determining membership in a class given a string representation of the class name.
To generate a class object from a class name or another object, you send it the class
message. So, to get a class object from a class named Square
, you write the following:
[Square class]
If mySquare
is an instance of Square
object, you get its class by writing this:
[mySquare class]
To see whether the objects stored in the variables obj1
and obj2
are instances from the same class, you could write
if ([obj1 class] == [obj2 class])
...
To see whether myFract
is an instance of the Fraction
class, you would test the result from the expression, like so:
[myFract isMemberOf: [Fraction class]]
To generate one of the so-called selectors listed in Table 9.1, you apply the @selector
directive to a method name. For example,
@selector (alloc)
produces a value of type SEL
for the method named alloc
, which you know is a method inherited from the Object
class. The expression
@selector (setTo:over:)
produces a selector for the setTo:over:
method that you implemented in your Fraction
class (remember those colon characters in the method names).
To see whether an instance of the Fraction
class responds to the setTo:over:
method, you can test the return value from the expression, like so:
[Fraction instancesRespondTo: @selector (setTo:over:)]
Remember, the test covers inherited methods, not just one that is directly defined in the class definition.
The perform:
method and its variants (not shown in Table 9.1) allow you to send a message to an object, where the message can be a selector stored inside a variable. For example, consider this code sequence:
SEL action;
id graphicObject;
...
action = @selector (draw);
...
[graphicObject perform: action];
In this example the method indicated by the SEL
variable action
is sent to whatever graphical object is stored in graphicObject
. Presumably, the action might vary during program execution—perhaps based on the user's input—even though we've shown the action as draw
. To first ensure that the object can respond to the action, you might want to use something like this3:
if ([graphicObject respondsTo: action] == YES)
[graphicObject perform: action]
else
// error handling code here
Program 9.3 asks some questions about the Square
and Rectangle
classes defined in Chapter 8, “Inheritance.” Try to predict the results from this program before looking at the actual output (no peeking now!).
#import "Square.h"
#import <stdio.h>
int main (int argc, char *argv[])
{
Square *mySquare = [[Square alloc] init];
// isMemberOf:
if ( [mySquare isMemberOf: [Square class]] == YES )
printf ("mySquare is a member of Square class
");
if ( [mySquare isMemberOf: [Rectangle class]] == YES )
printf ("mySquare is a member of Rectangle class
");
if ( [mySquare isMemberOf: [Object class]] == YES )
printf ("mySquare is a member of Object class
");
// isKindOf:
if ( [mySquare isKindOf: [Square class]] == YES )
printf ("mySquare is a kind of Square
");
if ( [mySquare isKindOf: [Rectangle class]] == YES )
printf ("mySquare is a kind of Rectangle
");
if ( [mySquare isKindOf: [Object class]] == YES )
printf ("mySquare is a kind of Object
");
// respondsTo:
if ( [mySquare respondsTo: @selector (setSide:)] == YES )
printf ("mySquare responds to setSide: method
");
if ( [mySquare respondsTo: @selector (setWidth:andHeight:)] == YES )
printf ("mySquare responds to setWidth:andHeight: method
");
if ( [Square respondsTo: @selector (alloc)] == YES )
printf ("Square class responds to alloc method
");
// instancesRespondTo:
if ([Rectangle instancesRespondTo: @selector (setSide:)] == YES)
printf ("Instances of Rectangle respond to setSide: method
");
if ([Square instancesRespondTo: @selector (setSide:)] == YES)
printf ("Instances of Square respond to setSide: method
");
[mySquare free];
return 0;
}
Make sure you build this program with the implementation files for the Square
, Rectangle
, and Point
classes.
mySquare is a member of Square class
mySquare is a kind of Square
mySquare is a kind of Rectangle
mySquare is a kind of Object
mySquare responds to setSide: method
mySquare responds to setWidth:andHeight: method
Square class responds to alloc method
Instances of Square respond to setSide: method
The output from Program 9.3 should be clear. Remember that isMemberOf:
tests for direct membership in a class, whereas isKindOf
: checks for membership in the inheritance hierarchy. So, mySquare
is a member of the Square
class; but it's also a “kind of” Square
, Rectangle
, and Object
because it exists in that class hierarchy (obviously all objects should return YES
for the isKindOf:
test on the Object
class, unless you've defined a new root object).
The test
if ( [Square respondsTo: @selector (alloc)] == YES )
tests whether the class Square
responds to the class method alloc
, which it does because it's inherited from the root object Object
. Realize that you can always use the class name directly as the receiver in a message expression, and you don't have to write
[Square class]
in the previous expression (although you could do that if you wanted). That's the only place you can get away with that. In other places you'll need to apply the class
method to obtain the class object.
Sometimes you might want to send a message that was sent to your class to another class to handle. This is called message forwarding, and it is somewhat analogous to forwarding your phone so that it rings somewhere else: The call is placed to your number and then forwarded to a different one that you specify. The object that is the recipient of the forwarded message is known as the delegate. Obviously, if you know which method it is and precisely where you want to send it, you can just write a method of the specified name and invoke the delegate with the corresponding arguments.
However, in the more general case, you might not know precisely which message you're getting and whether the object you want to delegate it to can handle it.
To forward messages, you need to override the forward::
method from the Object
class. If you just want to ignore any messages your class doesn't recognize, put the following definition in your class:
-(id) forward: (SEL) selector: (marg_list) arglist
{
return nil;
}
Of course, you can also display your own warning messages here or take some other action. Forwarding the message to another class to handle is a bit more complicated and has to be treated differently when working with Foundation's NSObject
root class (described in Part II). For those reasons, we won't go into any details here on how to do that. For further information, consult the references listed in Appendix E, “Resources.”
[compResult reduce];
into Program 9.1 after the addition is performed (but before compResult
is freed)? Try it and see.
id
variable dataValue
, as defined in Program 9.2, be assigned a Rectangle
object as you defined it in Chapter 8, “Inheritance”? That is, is the statement
dataValue = [[Rectangle alloc] init];
valid? Why or why not?
print
method to your Point
class defined in Chapter 8. Have it display the point in the format (x, y). Then modify Program 9.2 to incorporate a Point
object. Have the modified program create a Point
object, set its value, assign it to the id
variable dataValue
, and then display its value.add:
methods in the Fraction
and Complex
classes to take and return id
objects. Then write a program that incorporates the following code sequence:
result = [dataValue1 add: dataValue2];
[result print];
where result
, dataValue1
, and dataValue2
are id
objects. Make sure you set dataValue1
and dataValue2
appropriately in your program and free all objects before your program terminates.
Fraction
and Complex
class definitions you have been using in this text and the following definitions
Fraction *fraction = [[Fraction alloc] init];
Complex *complex = [[Complex alloc] init];
id number = [[Complex alloc] init];
determine the return value from the following message expressions. Then type them into a program to verify the results.
[fraction isMemberOf: [Complex class]];
[complex isMemberOf: [Object class]];
[complex isKindOf: [Object class]];
[fraction isKindOf: [Fraction class]];
[fraction respondsTo: @selector (print)];
[complex respondsTo: @selector (print)];
[Fraction instancesRespondTo: @selector (print)];
[number respondsTo: @selector (print)];
[number isKindOf: [Complex class]];
[number respondsTo: @selector (free)];
[[number class] respondsTo: @selector (alloc)];
3.149.27.234