9. Polymorphism, Dynamic Typing, and Dynamic Binding

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.

Polymorphism: Same Name, Different Class

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;
}


Program 9.1. Output


(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.

Dynamic Binding and the id Type

Chapter 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.

Program 9.2.


// 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;
}


Program 9.2. Output


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.

Compile Time Versus Runtime Checking

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.

The id Data Type and Static Typing

If 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.

Argument and Return Types with Dynamic Typing

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.

Asking Questions About Classes

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

image

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!).

Program 9.3.


#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.

Program 9.3. Output


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.

Message Forwarding

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.”

Exercises

  1. What will happen if you insert the message expression

    [compResult reduce];

    into Program 9.1 after the addition is performed (but before compResult is freed)? Try it and see.

  2. Can the 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?

  3. Add a 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.
  4. Based on the discussions about argument and return types in this chapter, modify both 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.

  5. Given the 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)];

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.163.207