REALbasic: The Definitive Guide, 2nd EditionBy Matt Neuburg
2nd Edition September 2001
0-596-00177-0, Order Number: 1770
752 pages, $39.95
Chapter 4In this chapter:
Class Relationships and Class Features
New Classes and Inheritance
Events and New Events
The Class Hierarchy
Advanced Class Features
Chapter 3 outlined the REALbasic object model, explaining what classes and instances are, and how you work with them in REALbasic. We saw that in the IDE you edit classes; then the running program generates instances and sends messages between them. We talked about the message-sending mechanism, how instances are generated, and how your code can refer to the instance it wants to send a message to. Now we're going to talk about the notion of classes in more depth, and in particular about relationships between classes, other class features, and how to do things with classes. Also discussed are modules, which provide methods, properties, and constants that are available from anywhere. The chapter ends by describing a few useful example classes that you can make in the comfort and safety of your own home.
Most object-oriented programming languages, along with a mechanism for making classes, also provide a means of expressing relationships among classes. There are three such relationships in REALbasic:
- A class may be a type of some other class. When two classes are related in this way, the first is said to be a subclass of the second, and the second is said to be the superclass of the first. For example, if you have a Triangle class, you might also have an Isosceles class, where Isosceles is a type of Triangle. Isosceles is then a subclass of Triangle, and Triangle is the superclass of Isosceles. A class can have many subclasses (for example, Triangle might also have a Scalene subclass), but every subclass has exactly one immediate superclass. The subclass-superclass relationship is sometimes called "Is-A," because every instance of the subclass also is an instance of the superclass; for example, every Isosceles is a Triangle. This sort of relationship also brings with it the notion of inheritance, meaning that a subclass is everything the superclass is, and then some. For example, a Triangle has three sides; so does an Isosceles, but it adds a rule that two of the sides are equal.
- A class may be declared to qualify as also being some other class, without inheritance, and without the other class having any real existence; the other class is just a name. The first class is said to implement the second, and the second class is not a real class at all, but a class interface. For example, we might have reason to want a Triangle, a Face, and a StopSign to be DrawableThings, as a matter of nomenclature, but to have nothing else in common. A class can implement multiple class interfaces, and can implement class interfaces even if it is a subclass, so this is also a way of getting around the rule that says a subclass can't have more than one immediate superclass. We may think of this as an "Also Is-A" relationship.
- A class may have a property whose datatype is some other class. For example, there might be a Point class and a Line class; you could define the Line class as having two Point properties (because two points determine a line). This relationship, especially when the bond between the class and the properties is felt to be particularly strong, is sometimes called "Has-A." So here, a Line has two Points and just wouldn't be a Line without them, and perhaps our program doesn't use Points except as features of a Line; that's a good solid Has-A relationship. Another kind of "Has-A" relationship is where a class has a property that is of a different class, and also provides all the methods for working with that property; this is called a wrapper. An example appears later in this chapter.
What classes you create for your project, and in particular how you set up the relationships among them, is a matter of design--object-oriented design. Your project ends up with an internal architecture of classes. There will be a class hierarchy: A and AA are subclasses of B, B and BB are subclasses of C, and so forth. Cutting across this hierarchy you might have some class interfaces, giving AA and BB some special extra commonality. D might operate only as a property of A, encapsulating functionality or building a data structure. The principles of object-oriented design are a mixture of science, art, philosophy, and expediency; it's a big subject, too big for this book. But the first step is to understand REALbasic's object model, how classes can relate and what you can do with these relationships; that's what this chapter is about.
Relationships among classes in REALbasic, especially the class hierarchy, are not merely a convenience of design; they are crucial. REALbasic's application framework provides a hierarchy of built-in classes before you write any code at all; and adding to that hierarchy is how you take advantage of the built-in functionality of those classes. For example, you might want a class that acts just like a built-in PushButton but does a few things in addition. To get it, you'd make a subclass of PushButton. That's what the next section is about.
New Classes and Inheritance
To create a new class, choose File New Class. A listing for the new class will appear in the Project Window. If you immediately hit the Tab key, you'll be transported to the Properties Window, ready to give the new class a meaningful name.
If the new class you want to create is a window class, you choose File New Window instead. Window classes can't be subclassed, so they are not the sort of thing this chapter is primarily about.
When you create a new class, you may declare it to be a subclass of some other class, by setting, in the Properties Window, the new class's Super. To do so, you choose from a popup menu which lists all subclassable built-in classes and all classes you've added to this project. This specifies your new class's superclass, and thus makes your new class a subclass of that other class. It is also possible to specify that a class is to be a subclass of no other class; to do this, choose "<none>" in the Super popup (this is the default when you create a new class).
As we've already said, a subclass relates to its superclass through the medium of inheritance. Simply put, this means that an instance of the subclass also is an instance of the superclass ("Is-A"). More formally, the subclass has all the same methods and properties as the superclass, and an instance of the subclass can be sent all the same messages as an instance of the superclass. Furthermore, in the case of one of REALbasic's built-in classes that receives events as part of the application framework, a subclass receives those events as well. Subclassing is thus a quick and easy way to take advantage of an existing class's functionality. And most of REALbasic's built-in classes are part of its application framework, meaning that their functionality includes powerful stuff like displaying interface items on the screen or communicating over the Internet; so the ability to subclass such classes means that you can make some very powerful subclasses.
WARNING: Unfortunately, aside from the Super listing in the Properties Window, the nature of the relationship between subclass and superclass is not in any way reflected in the IDE. Looking at a subclass's Code Editor, you are not shown what methods and properties it inherits from the superclass; to find out, you have to look at the superclass (or its documentation). Nor is there any way to learn what are the subclasses of a given class. In short, the REALbasic IDE doesn't make inheritance easy to use. This is one of the worst aspects of the IDE's interface.
If a subclass is like its superclass, why make a new class at all? What makes a subclass a different class from its superclass? Basically, it's that you can add to a subclass members that its superclass lacks. A subclass is its superclass and then some. In the case of REALbasic's built-in classes, the ability to add to the subclass is crucial, because you can't modify the built-in classes; the way you take advantage of the functionality of one of REALbasic's built-in classes while customizing its structure and functionality is to make a subclass of it and customize that.
For example, recall the arcade game described in Chapter 3. There, we imagined a ScoreBox class, which would have a Score property, and would know how to display its value in a window. Also, the ScoreBox class would have an Increase method, which would increment the value of the Score property. Let's actually implement the ScoreBox class.
To do so, we'll take advantage of a built-in Control class called a StaticText, which already has the following useful functionality: it displays in its containing window the value of its own Text property. We can't modify the StaticText class, so we make a subclass of it: we create a new class, name it ScoreBox, and designate StaticText as its Super. Then, in the ScoreBox class's Code Editor, we give it a Score property which is an integer, and an Increase method, which goes like this:
self.score = self.score + 1 // increment the score
self.showScore // display the score
We have postponed the question of how the ScoreBox will actually display its score in its containing window, by giving that job to an unwritten subroutine. Let's write it. It will be a method handler, as we know. A StaticText always displays its own Text property, so it suffices to set the Text property to the value of the Score property. The Text property is a string, while the Score property is an integer, so we must also convert. Here is ScoreBox's ShowScore method handler:
self.text = str(self.score)
Finally, let's think about what happens when a window containing a ScoreBox instance first opens. That instance's score is autoinitialized to
0, which seems acceptable. But we also want to make sure it is displayed. Among the Events listed in ScoreBox's Code Editor is the Open event. An Open event handler, if we choose to write one, is automatically called by REALbasic when the control is instantiated. That's an appropriate moment to start displaying the score. Here is ScoreBox's Open event handler:
That's all there is to it! Let's try it out. Drag the ScoreBox listing from the Project Window into a Window Editor. A new control appears in the Window Editor; select it. Looking at the Properties Window, you can see that although this control is named StaticText1 by default, its Super listing says that it is indeed a ScoreBox instance. So StaticText1 should know how to accept the Increase message. Let's see if it does. Drag a PushButton from the Tools Window into the Window Editor; double-click it in the Window Editor to access its Action event handler in the window's Code Editor, and give it this code:
Run the project in the IDE. There's a number in the window! It's zero! And every time you press the button, the number increases!
It's simple, almost trivial; yet, for the reasons explained in Chapter 3, it's tremendously powerful. The score is now maintained, appropriately, by the object that primarily operates on it; other objects can call a ScoreBox's Increase method without worrying about what this does or what the score is; the ScoreBox, for its part, doesn't care who is calling it; and a project can contain multiple ScoreBox instances, each maintaining a separate score, yet all behaving identically.
We've seen that the code for the ScoreBox class's behavior doesn't live in the window's Code Editor; it lives in the ScoreBox class's own Code Editor, which you access by double-clicking the ScoreBox class's listing in the Project Window. All ScoreBox instances will look to this code for their behavior, and if you change this code, all ScoreBox instances will henceforward display the new behavior. Let's prove to ourselves that this is true. (If the project is running in the IDE, you'll have to kill it first, so that you can modify the project.) Start by dragging the ScoreBox listing from the Project Window into the Window Editor again. Now the window has two ScoreBox instances, StaticText1 and StaticText2. Change the PushButton's Action event handler to read:
Run the project and push the button repeatedly; both ScoreBox instances behave identically (and they increment together, since they both started at zero and both receive the Increase message when we push the button; I remind you, however, that they maintain independent scores). Now, in the ScoreBox class's Increase method, change this line:
self.score = self.score + 1 // increment the score
self.score = self.score + 2 // increment the score
Immediately run the project again, and push the button. Sure enough, now the number increases by two every time, in both ScoreBox instances. Notice that you didn't have to do anything horrible and clumsy like delete the ScoreBox control instances from the window and replace them with new ones; an instance takes on the altered class behavior immediately.
Behind the simplicity of this example lurks the power of inheritance. The ScoreBox instance can receive the Score message because we gave the ScoreBox class a Score property; but it can receive the Text message because the ScoreBox class's superclass, StaticText, has a Text property. The ScoreBox instance can receive the Increase message because we gave the ScoreBox class an Increase method handler; but it has an Open event handler, which is called automatically by REALbasic, because that's how REALbasic has defined its superclass, StaticText. Most impressive of all, we have created a working interface element without knowing anything about how to drive the Macintosh Toolbox, simply by subclassing a built-in class that does know how.
The existence of subclasses and inheritance complicates the question of what class a given instance is an instance of. Is a ScoreBox instance a ScoreBox, or is it a StaticText? This depends on what the meaning of "is" is. At bottom, a ScoreBox instance is clearly a ScoreBox; ScoreBox is its final class. Still, since a ScoreBox is a kind of StaticText (for that is what subclassing means), a ScoreBox instance is a StaticText instance as well; an instance of a subclass is also an instance of the superclass. And you can carry this on up the hierarchy of classes. StaticText is a subclass of RectControl, and RectControl is a subclass of Control. So a ScoreBox instance is also a RectControl instance and a Control instance.
This fact must often be taken into account when manipulating instances and references to instances. That's what this section is about.
We begin with this important fact: A subclass instance is acceptable in a context where an instance of the superclass is expected. We've already seen that this is true; a ScoreBox can talk about
self.textbecause Self, a ScoreBox instance, is acceptable as a recipient of the Text message, where a StaticText instance is expected. Similarly, if we have a ScoreBox called TheScoreBox in our window, REALbasic will not complain if we say this:
dim s as staticText
s = theScoreBox
The reason this works is that, by the terms of its declaration, assignment to
sis legal if the value being assigned is a StaticText; and TheScoreBox is a StaticText, by virtue of the fact that it is a ScoreBox (because ScoreBox is a subclass of StaticText).
But the reverse is not true; and for the selfsame reason. If our window contains a control called TheStaticText whose final class is StaticText, you cannot say this:
dim s as scoreBox
s = theStaticText // error: Type mismatch
Similarly, you cannot send the Increase message to an instance whose final class is StaticText; a StaticText knows nothing of the Increase message. The reason is simple: a StaticText is not a ScoreBox.
The criterion that REALbasic uses to determine what you can do with a reference is the declared type of the reference--not the class of the instance to which the reference really points. Consider the following:
dim s as staticText
s = theScoreBox // fine
theScoreBox.increase // fine
s.increase // error: Unknown identifier
sis pointing to a ScoreBox instance, TheScoreBox, and even though it is fine to send the Increase message to TheScoreBox under its own name (
theScoreBox.increase), yet the reference
shas been declared as a StaticText, so it cannot accept messages that a StaticText doesn't know about. In general terms, we're allowed to treat a subclass instance as a superclass instance, but a reference declared as the superclass cannot be treated as if it refers to an instance of the subclass, even if it does refer to an instance of the subclass. (In technical terms, REALbasic does its type-checking at compile time.) This situation is illustrated in Figure 4-1.
Figure 4-1. Legal messages to an instance depend upon the reference's type
In that case, what's the good of being allowed to treat a subclass instance as a superclass instance? Why would I ever say:
dim s as staticText
s = theScoreBox
if I cannot then treat
sas a ScoreBox? Part of the answer is that it can be useful and powerful to have a reference accept a value of more than one datatype indifferently. For instance, we might have an array that we intend to populate with StaticTexts and ScoreBoxes. We cannot declare the array as of type ScoreBox, because then no element can accept a StaticText value. But if we declare the array as of type StaticText, then an element can be a StaticText or a ScoreBox (or any other subclass of StaticText).
This raises the question of how we would know, once our array is populated, what class any given element actually is. The datatype of the reference we know already, since we declared it; but is there any way to tear this mask away and see the class of the instance? Unfortunately, REALbasic provides no way to ask an instance pointblank what its final class is. But it does provide a way to ask an instance whether it is (in any sense) an instance of a particular class. This is done by using the
IsAoperator, which takes a reference and a class name, and returns a boolean reporting whether the instance pointed to by the reference is in fact an instance of the proposed class. Some examples will show both the syntax and the result:
dim theTruth as boolean
dim s as staticText
s = theScoreBox
theTruth = (theScoreBox isA scoreBox) // true
theTruth = (theScoreBox isA staticText) // true too
theTruth = (s isA staticText) // true
theTruth = (s isA scoreBox) // true too
theTruth = (s isA theScoreBox) // legal and true!
To sum up: The
IsAoperator ignores the declared type of the reference; it looks only at the actual instance pointed to, and returns
trueif the proposed class is that instance's final class or one of that class's superclasses. In the case of controls only,
trueif the proposed class is the control's global name, even though that name is not what one normally thinks of as a class name (this is essentially the same syntax used with
Newto clone a control, as described in Chapter 3).
So, if a reference declared as the superclass is pointing to an instance of the subclass, we can learn that it is the subclass. What's more, we can treat it as the subclass--provided we explicitly inform REALbasic that that's how we want it treated. This is done through the use of casting, which is essentially a means of disguising one class as another so that REALbasic will see a message as appropriate. A reference to an instance is cast as a different class by using the class name as a function operating on the reference:
dim s as staticText
s = theScoreBox
s.increase // error, remember?
scoreBox(s).increase // fine!
As we know, the third line doesn't work, because REALbasic refuses to look behind the mask of the reference's declared datatype;
shas been declared as a StaticText, and REALbasic's attitude is, "That's all I know, and all I need to know." But we are more adventurous than REALbasic is, so we do look behind the mask! We use casting to tell REALbasic what we saw. By casting, we compel REALbasic to disbelieve the reference's declared datatype, and to treat the value of
sas a ScoreBox, willy-nilly; since a ScoreBox can receive the Increase message, REALbasic doesn't complain. What we're doing here is nullifying REALbasic's strict compile-time type-checking; we're saying, "I know I declared
sas a StaticText, but trust me, when the code actually runs,
swill be pointing to a ScoreBox." In particular, we are casting down: we reveal to REALbasic that a reference declared as a superclass is actually pointing further down the hierarchy, at a subclass.
But be warned: REALbasic does trust you when you cast; it obviously has no way to check in advance whether the instance really will be what you claim it will, so it simply throws all caution to the winds. So don't betray REALbasic's trust! You can get away with saying this:
dim s as staticText
dim c as string
s = theScoreBox
c = pushButton(s).caption // liar! error: IllegalCastException
but your application will terminate prematurely when the last line actually executes and it turns out that
sis not a PushButton after all. A common way to prevent this sort of mistake is to wrap casts in an
dim s as staticText
dim c as string
s = theScoreBox
if s isA pushButton then // nothing can go wrong now
c = pushButton(s).caption
Notice that casting doesn't perform any coercion; in other words, it does not transform an instance of one datatype into an instance of another datatype. In fact, it has no effect whatever upon any instances. Casting doesn't turn a sow's ear into a silk purse; it just makes a sow's ear look like a silk purse, and even then this works only because the sow's ear really is a silk purse to start with.
In the example from "New Classes and Inheritance," earlier in this chapter, we defined ScoreBox, a subclass of StaticText. Every message that ScoreBox can receive is defined either in itself or in its superclass (or in the superclass of that, and so on). The superclass defines some members; the subclass inherits these and adds some more. But in real life we may wish a subclass to replace the functionality of a member defined in its superclass. This is called overriding. To do this, one simply defines in the subclass a member with the same name as a message that the superclass can receive. Now the message is defined in both the subclass and the superclass.
In making an example, there's no point using ScoreBox to override any of StaticText's members. This is basically the same issue discussed in See Resolution of Names: where a built-in class is concerned, to override a property would hide the built-in functionality associated with that property, which is undesirable, and to override a method is forbidden. So we need a superclass and a subclass both of which we ourselves have defined. Let's subclass ScoreBox to make a DoubleScoreBox class. This will be a class that differs from ScoreBox only in that when it receives the Increase message, it increments its score by
Create the DoubleScoreBox class and make it a subclass of ScoreBox. Clear the Window Editor of controls. Drag in a ScoreBox, select it, and give it a more meaningful name--TheScoreBox. Drag in a DoubleScoreBox and name it TheDoubleScoreBox. Drag in a PushButton and have its Action event handler go like this:
Now it's time to define DoubleScoreBox's Increase method, to override ScoreBox's Increase method. Open DoubleScoreBox's Code Editor and define a method handler Increase, with this code:
self.score = self.score + 2 // increment the score
self.showScore // display the score
Run the project and press the button repeatedly. It works. Let's talk about what's happening.
It's true that our instance TheDoubleScoreBox is a ScoreBox, which defines an Increase method, to increment by
1; but its final class is DoubleScoreBox, which also defines an Increase method, to increment by
2, and that's the one that gets called when we send the Increase message to TheDoubleScoreBox. On the other hand, when TheDoubleScoreBox's Increase method calls
self.showScore, DoubleScoreBox has no ShowScore method; but the message is acceptable, because a DoubleScoreBox is also a ScoreBox, and a ScoreBox does define a ShowScore method, which is what gets called. We may summarize by saying that message names are resolved upward through the instance's class and its superclasses: first we look in the final class of the instance to see if it accepts the message; only if not do we look at its superclass, and so on. This is illustrated in Figure 4-2.
Figure 4-2. Overriding
Our use of the phrase "the instance" may seem surprising, because in the previous section the important thing was the declared datatype of the reference. To understand what's happening, separate the message-sending process into two distinct stages. First, REALbasic decides whether the reference can be sent the specified message at all; this is done by resolving the message name upward from the reference's declared datatype. If it can, then REALbasic decides which class should actually receive the message; this is done by resolving the message name upward from the instance's actual final class. For example:
dim s as scorebox
s = theScoreBox
s = theDoubleScoreBox
This increases TheScoreBox's score by
1and TheDoubleScoreBox's score by
2, even though the reference both times is
s, which is declared as a ScoreBox. Do you see why? The fact that the reference
sis declared as a ScoreBox means that it can accept the Increase message, so this code is legal--and that's all it means. Now we come to the question of what this code will actually do; and that depends upon the instance that
spoints to. When
spoints to an instance of the ScoreBox class, it is ScoreBox's Increase method that is called. When
spoints to an instance of the DoubleScoreBox class, it is DoubleScoreBox's Increase method that is called.
The principle here is that all programmer-defined methods are virtual methods. That's just technical talk for the very thing we've just been saying: the class of a reference may make it a legal recipient of a message, because that class can handle that message; but that fact is no guide as to what class will actually handle the message, because the instance may be of a class that overrides it.
Let's look at the matter from a different perspective. Instead of a linear architecture, imagine a situation where one class has two subclasses. We'll take advantage of overriding a virtual method so that it becomes, in effect, a decision-making mechanism. Suppose we're writing a Tic-Tac-Toe game. Every game piece is either an X or an O. But all game pieces have some common behavior. For example, a game piece should know how to draw itself in a given square on the board. Clearly we're going to have a GamePiece class and a Square class. We envision a routine that takes a piece and a square and tells that piece to draw itself into that square, like this:
Sub drawPieceIntoSquare(p as gamePiece, s as square)
The exact behavior of
pis going to depend on whether it is an X or an O. What will it mean, to be an X or an O? Let's make it mean that there is an X class and an O class! Obviously, these are both subclasses of GamePiece. Presume that
pis either an X instance or an O instance. If the X class has a DrawYourselfInto method and the O class has a DrawYourselfInto method, then when we send
pthe DrawYourselfInto message, the right thing will happen automatically. If
pis an X instance, X's DrawYourselfInto will execute; if
pis an O instance, O's DrawYourselfInto will execute. What handler will execute depends upon what class this particular instance is during this particular call to our routine. So the class structure becomes a decision-making mechanism! The fact that we can send a message to one higher class as a way of choosing among several lower classes is called polymorphism.
However, we've left out a small piece of the puzzle. We define GamePiece. We define X as a subclass of GamePiece, and we give it a DrawYourselfInto method. We define Y as a subclass of GamePiece, and we give it a DrawYourselfInto method. We try to execute the previous code, and we get an error. What's happened?
The answer is that we forgot the first part of the message-sending process. We know that the DrawYourselfInto message will get routed to the right class, the class of the actual instance that
ppoints to. But we also have to make it legal to send the DrawYourselfInto message to
pin the first place! This means that GamePiece must also have a DrawYourselfInto method handler. In other words, in order to take advantage of a virtual method, there has to be a virtual method. You can't override something that isn't there in the first place.
Very well; but what should GamePiece's DrawYourselfInto handler do? It's perfectly possible that it will do nothing at all; it might be completely empty! It could be that the whole of X's drawing functionality is contained in its DrawYourselfInto handler, and the whole of O's drawing functionality is contained in its DrawYourselfInto handler. And every GamePiece is going to be an X or an O. Our routine is never going to receive a value for
pwhose final class is GamePiece; it will always be an X or an O. So GamePiece's DrawYourselfInto handler does nothing, because it will never be called. Is this silly? Not at all. In fact, it's such a common technique that it has a name: GamePiece's DrawYourselfInto handler is said to be abstract. It exists only so that X and O can override it, so that a reference declared as GamePiece can be sent the DrawYourselfInto message, taking advantage of polymorphism.
In fact, we can go further. It may be that no instance whose final class is GamePiece will ever be generated throughout the entirety of our program. Not just GamePiece's DrawYourselfInto handler, but the whole GamePiece class, might exist only so that X and O can subclass it and take advantage of polymorphism. Again, this is quite common, and GamePiece is then said to be an abstract class. (See the for a Tic-Tac-Toe game that employs this sort of architecture.)
A subclass and its superclass represent one way in which two classes can relate. Both are classes, and the subclass inherits the fullness of the superclass's power, which may be considerable. Compared to this, a class interface seems evanescent, almost intangible. A class interface is abstract, and its methods, if any, are abstract. A class interface has no code; it has no properties; you can't make an instance whose final class is a class interface! A class interface is barely a class at all: it's a mere appearance of a class, a skeleton of a class. Yet it adds great power and flexibility to REALbasic's model of object orientation, because it breaks out of the structure of the subclass-superclass hierarchy.
To make a class interface, you choose File New Class Interface. Just as with a new class, a listing appears in the Project Window, and you can tab into the Properties Window to give your new class interface a meaningful name. But the resemblance ends there. The Name listing is the only listing in the Properties Window; a class interface has no Super, because it has no superclass. The Code Editor has no place for Events, Menu Handlers, or Properties--just Methods -- and while you can declare a method, you can't give it any code.
A class cannot have a class interface as its Super; a class does not inherit from a class interface. Instead, to form a relationship between a class and a class interface, you go into the class's Properties Window and edit its Interfaces listing. Here, you simply type the name of the class interface. If you like, the Interfaces listing can include the name of more than one class interface; separate the names with a comma. A class is said to implement the class interfaces you list among the Interfaces in its Properties Window.
What does it mean for a class to implement a class interface? We already know part of the answer. A class can have only one Super; but a class can implement more than one class interface, and this is entirely independent of what Super it may have. So implementing a class interface is a separate relationship from inheritance, and it is a relationship that permits a kind of multiple parentage.
But what kind of parentage? The answer lies in what the parent (the class interface) bequeaths to the child (any class that implements it). It bequeaths two things:
- The name
- As far as names are concerned, a class interface is just like a superclass. You know that a subclass instance is acceptable where an instance of the superclass is expected. In the same way, an instance of a class that implements a class interface passes the
IsAtest for the class interface, and is acceptable where an instance of the class interface is expected (which is good, because you can't otherwise make an instance of the class interface).
- The method declarations
- A class interface bequeaths to a class that implements it the requirement that that class should declare the same methods that it itself declares. A method declaration in a class interface is thus effectively a rule that a class must obey if it wants the privilege of inheriting the name; it's an abstract virtual method with the power to force an implementing class to override it. Actually declaring the method in the implementing class's Code Editor is up to you (though no law says you have to give it any code).
What are class interfaces for? It was pointed out in "Casting," earlier in this chapter, that the datatype of a reference is like a mask; regardless of the true class of the instance pointed to, the reference can accept only messages appropriate to its datatype. Class interfaces allow a reference to wear an arbitrary mask. You will typically use the power of this arbitrariness to pass instances around in ways that would normally be impossible, because you're cutting across the hierarchy of subclasses and superclasses.
For example, in earlier sections of this chapter we implemented ScoreBox as a subclass of StaticText. But there are other built-in classes that can display text in a window, and these would be equally good candidates as a way of implementing a scorebox type of functionality. Consider an EditField; it automatically displays its Text property, just as a StaticText does. Let's implement a class just like ScoreBox, but based on EditField.
Make a new class, call it EFScoreBox, make it an EditField subclass, give it a Score property which is an integer, and give it code exactly like the ScoreBox's code: the same Open event handler, the same Increase method handler, the same ShowScore method handler. Clear the Window Editor and drag one ScoreBox and one EFScoreBox into it; these will be called StaticText1 and EditField1 by default. Give the window a PushButton and give its Action event handler this code:
Run the project and push the button several times. It works; both the StaticText and the EditField are incrementing and displaying their score.
Now let's think a moment. From the point of view of the code in the PushButton, there's something silly about this situation. A ScoreBox can accept the Increase message; an EFScoreBox can accept the Increase message. They should, in fact, be the same kind of entity: "things I can send the Increase message to." This sameness should be expressed as a class. We could call it Increaser, and we imagine being able to say something like this:
dim i as increaser
// ... set
ito point to either a ScoreBox or an EFScoreBox ...
Under the rules of the class hierarchy, that's impossible. For where might Increaser fit into the hierarchy? Both StaticText and EditField have RectControl as their superclass, and there's nothing we can do about that; those are all built-in classes and we can't access them. We could make Increaser a subclass of StaticText and make ScoreBox a subclass of Increaser; but then EFScoreBox can't be a subclass of Increaser. This impasse is schematized in Figure 4-3.
Figure 4-3. Limitations of simple inheritance
The solution is to implement Increaser as a class interface, because class interfaces can cut across the hierarchy of subclasses and superclasses. Let's do it. Make a new class interface and call it Increaser. Go into Increaser's Code Editor and declare a method called Increase. Now select ScoreBox in the Project Window and type Increaser into its Interfaces listing in the Properties Window; do the same for EFScoreBox. Now make the PushButton's Action event handler go like this, and run the project:
dim i as increaser
i = statictext1
i = editfield1
It works: we've subsumed both our ScoreBox and our EFScoreBox under a single class umbrella. Of course, we're not doing anything very powerful with this capability; but we could. For example, let's turn our PushButton into a broadcaster. A broadcaster is an object that sends out a message without caring who receives it: "Whoever has ears to hear, let him hear." As a simplified implementation, we'll make it the case that our PushButton broadcasts to any Increasers in the neighborhood (that is, in the same window as itself). Here is the PushButton's Action event handler now:
dim i as increaser
dim c as integer
for c = 0 to self.controlCount-1
if self.control(c) isA increaser then
That's quite a remarkable piece of code. We've done the impossible: we've united two utterly distinct classes under a single class heading, and we've used the power of polymorphism to treat them indifferently under that heading, sending instances of each of them the Increase message without knowing or caring what they really are.
I should pause to describe the more usual implementation of a broadcaster, which is roughly as follows. Start with two class interfaces: let's call them Broadcaster and Recipient. Each defines a communication method; let's say that Broadcaster defines TellRecipient, and Recipient defines HearBroadcaster. The idea is that when the Broadcaster wants to broadcast, its TellRecipient method sends the HearBroadcaster message to a Recipient. There is also some implication that a class implementing Broadcaster has a property whose datatype is Recipient, or perhaps more than one such property, or perhaps a property that is an array of Recipients (I call this an implication because a class interface has no way to require or express it, but the presence of the TellRecipient method helps remind us); this is how it knows who its available recipients are. It then remains to point these Recipient properties at actual Recipient instances, thus hooking together the particular Broadcaster and its particular Recipients; this must be done in code, and when and how to do it is up to the programmer. The power of this architecture is well illustrated by an example I've constructed, too elaborate to describe here, but which you can download from my web site, where either a Scrollbar or a Slider (indifferently) can broadcast to either a StaticText or a ProgressBar (indifferently).
Let's now turn to an example that views class interfaces from a different perspective. Imagine a class that implements a data structure, and provides some methods for manipulating this data structure. There might be several such classes, unrelated and implementing completely different data structures. It ought to be possible to pass an instance of any of these classes to a subroutine, which can then use the methods to manipulate the data structure, without knowing anything about what the nature of the data structure actually is. Class interfaces make this possible.
A powerful example is a Sort routine (a routine that arranges data in order from smallest to largest). If a routine knows how to sort, it knows how to sort anything; it doesn't matter what the type of data is, or how the data are stored. The routine needs just three things:
- It must be possible to refer to the data by index number, and the routine must be told explicitly the lower and upper bounds on this index number.
- It must be possible to learn, given two index numbers, how the corresponding data compare: is one item smaller than, larger than, or identical to the other?
- It must be possible to command that the data for two index numbers be swapped.
The first requirement can be met by the way our routine is called. The other two requirements can be met through a class interface. Suppose we have a class interface, Sortable, which declares a method Compare and a method Swap. Now our routine can be handed as its parameters a lower bound index, an upper bound index, and an instance of the Sortable class. Our routine is secure in the knowledge that it can call this instance's Compare and Swap handlers; and that's all it needs to know! In reality, the Sortable can be an array wrapper, a delimited string wrapper, a ListBox subclass, and so forth; the data to be sorted can be integers, strings, dates, colors, anything at all--if you can decide on a way to compare them, our routine can sort them.
Events and New Events
Events are the basis of a REALbasic application's ability to interact with the user and to behave in the live, responsive manner that befits a GUI-based application. An event is a message sent only by REALbasic, not by the programmer's code. It is triggered because some predefined occurrence has come to pass--typically some user action, or some action on the part of your code that resembles a user action. It is sent to an instance of a built-in class (or a subclass of such a class) that is predefined to be sent this type of event. When an instance receives an event, its event handler for that event is called. If you, the programmer, want that instance to respond to a predefined occurrence, you write code in the corresponding event handler.
For example, when the user clicks the mouse on an instance of a PushButton (or a PushButton subclass), the instance is sent an Action event. To say what this instance should do when the user clicks the mouse on it, you write code in its Action event handler. If you don't, the button won't respond to being clicked.
Although the programmer writes the event handler code, the event handlers themselves, and the events that call them, belong entirely to REALbasic. The programmer cannot add or remove an event handler. Nor can the programmer create an event; for example, you might wish that REALbasic should send an event to a certain instance every time the user changes the computer's speaker volume, or blows his nose, but that isn't going to happen. You simply have to hope and trust that REALbasic comes equipped with a set of events sufficient to allow your program to implement the kind of responsive functionality you desire for it. For the most part, it probably does; and where it doesn't, you'll have to understand and accept what it cannot do, and implement a workaround or modify your desires accordingly.
Knowing what the predefined occurrences are, and in what order the corresponding events will be sent to what instances, is a crucial part of learning to program effectively with REALbasic, so naturally this book provides lots of details. An overview appears in the section called , and then the discussion of each built-in class in and (along with Chapter 6 on menus) provides a complete list of the class's events and the occurrences that trigger them.
Where is the event handler where the programmer is to write code? The situation was simple before we knew about subclasses: drag a PushButton from the Tools Window into a Window Editor, and double-click it to see the PushButton's event handlers listed in the Controls section of the window's Code Editor. Now things have become confusing. We make a subclass of StaticText, called ScoreBox; open ScoreBox's Code Editor and its event handlers are there. Drag a ScoreBox from the Project Window into the Window Editor and double-click it; we're back among the event handlers in the window's Code Editor. Does the event handler code for a ScoreBox go in the class or in the window? Now bring ScoreBox's subclass, DoubleScoreBox, into the story. DoubleScoreBox's Code Editor has event handlers too! Should code for a DoubleScoreBox event handler go into ScoreBox, DoubleScoreBox, or the window? How do events relate to the class hierarchy?
The answer is simple, once you know how an event travels as it is triggered and sent. For purposes of events, pretend that an instance in the window is part of the class hierarchy--in particular, that it's at the bottom of the hierarchy. So, for example, when there's a DoubleScoreBox instance called TheDoubleScoreBox in a window, the hierarchy from top to bottom runs Control, RectControl, StaticText, ScoreBox, DoubleScoreBox, TheDoubleScoreBox. An event is directed ultimately at an instance. But since it is the instance's built-in superclass that is responsible for the fact that the event is being sent to this instance at all, it is the superclass that receives the event first; the event then travels down the class hierarchy toward the instance at which it is actually directed, looking for a class that handles the event. At that point, the process comes to an end. By "handles the event," I mean, "contains an event handler bearing the event's name, which has code in it." In a Code Editor, REALbasic signals that an event handler handles an event by making its browser listing bold.
The implication is that event handlers cannot be overridden. If ScoreBox contains code in its Open event handler, the Open event handler in DoubleScoreBox will never be executed, nor will the Open event handler belonging to a DoubleScoreBox instance in a window, because the Open event travels down the class hierarchy, finds that ScoreBox handles it, and stops.
In fact, the IDE enforces this rule by physically preventing you from overriding an event handler. If you put code into any class's event handler, REALbasic actually deletes the corresponding event handler from the Code Editors of its subclasses and superclasses. So, because you wrote code in the Open event handler in the ScoreBox class, you'll find there is no Open event handler in the DoubleScoreBox class or in any ScoreBox or DoubleScoreBox instance in a window! Just the other way, if you write code in the Close event handler of any DoubleScoreBox instance in a window, the Close event handlers in DoubleScoreBox and ScoreBox will vanish.
This means you have to be a little careful about the order in which you put code into event handlers! In the example just given, if we changed our minds and decided we wanted ScoreBox to handle the Close event, we would first have to remove the code from the DoubleScoreBox instance's Close event handler, in order to get ScoreBox even to have a Close event handler. This can get very painful when you've got dozens of instances in several different windows, and have to hunt through them all to find the one with code that's preventing you from writing an event handler in a class.
The fact that event handlers can't be overridden seems unfair, because manifestly you might need instances of different classes, or individual controls derived from the same class, to be able to respond differently to an event. For example, both a ScoreBox and a DoubleScoreBox presently permit their Score to be autoinitialized to
0. But what if we wanted a ScoreBox's Score initialized to
1and a DoubleScoreBox's Score initialized to
2? Clearly the place to make this happen is in the Open event handler, before the score is first displayed. But only ScoreBox even has an Open event handler. We could make ScoreBox's Open event handler go like this:
self.score = 1
But what will we do about DoubleScoreBox?
The best approach, I think, wherever it can be used, is to take advantage of virtual methods. Instead of initializing the Score property directly in an event handler, have the event handler call a method that initializes it. Methods are virtual, so the problem is solved. ScoreBox's Open event handler would then run as follows:
It is now just a matter of writing InitScore methods in ScoreBox and DoubleScoreBox; obviously, ScoreBox's InitScore method says:
self.score = 1
and DoubleScoreBox's InitScore method says:
self.score = 2
But there are situations where this approach isn't so viable. What if we want one particular instance of DoubleScoreBox in one particular window to initialize its Score to
0? We can't put an InitScore method in the instance in the window, because instances in windows don't have method handlers. We could create a special class just for this instance, but that seems extreme. To help us out, REALbasic provides a second way of dealing with the matter--the New Event mechanism.
The New Event mechanism enables code in an event handler to continue handing an event down the hierarchy toward the instance. You define a New Event in a class; its subclasses then inherit the right to handle that event. The only trick is that the class must explicitly trigger the New Event, so that it is sent on down the hierarchy.
So, let's say that in ScoreBox we anticipate the possibility that some ScoreBox subclass or instance may wish to override our initialization of the Score property. To make this possible, we'll propagate a New Event down the hierarchy. In ScoreBox, choose Edit New Event. The usual subroutine declaration dialog appears, and we name it InitScoreOverride. Drag a DoubleScoreBox instance into the Window Editor, and double-click it. Lo and behold, an InitScoreOverride event handler has appeared among this instance's event handlers in the window! Here, you can put this code:
me.score = 0
(You must use Me, because that's the DoubleScoreBox; Self is the window, remember?) However, this code has no effect, because the InitScoreOverride event is never triggered. We must take care of that. Go back to ScoreBox's Open event handler, and make it go like this:
The strategy is that we initialize the Score in the normal way, but then we send the InitScoreOverride event down the hierarchy just in case anybody wants to handle it. If nobody does, that's fine. In this particular case, one instance of DoubleScoreBox does handle it, changing the Score to suit its own taste before it is displayed for the first time. The only hard part, which wasn't particularly hard in this case, was deciding just when to trigger the InitScoreOverride event. You have to consider the order in which things are going to happen. Obviously it would have been wrong to do things in this order:
This would allow subclasses and instances to set the Score, but then we return it to its default setting, making InitScoreOverride useless.
The New Event mechanism was of great importance in Version 1 of REALbasic, when virtual methods didn't exist; but now virtual methods do exist, and New Events have been relegated almost to the status of an unused appendix. Nevertheless they do have their uses. In particular, I still like to use New Events as my chief way of giving a control class a method whose implementation is to be unique to each particular instance in a window. That's because the alternative--creating a subclass for each instance, so that virtual methods can be used--is usually too painful to contemplate.
Some final points about New Events. You will observe that in the ScoreBox Code Editor, an InitScoreOverride entry appears in the New Events category, but its code is blank and cannot be edited; its purpose is merely to register the fact that this is the class where this New Event is defined. A New Event can be triggered only from an event handler or method handler in the class in which it is defined (where it appears under New Events); no other class knows of its existence. Nor is it up to you where a New Event call is directed; it can only go on down the hierarchy toward the instance that was the target of the original event that started this chain. And so a New Event's name cannot be attached, with dot notation, to a reference (thus in our example we said
self.initScoreOverride). Finally, a New Event can have parameters, and can even return a value, like any subroutine. (But don't try passing a
ByRefparameter to a New Event; there's a bug where a change in a
ByRefparameter value in a New Event won't percolate back to the caller.)
The Class Hierarchy
Some of REALbasic's built-in classes are subclasses of other built-in classes; indeed, with one exception, they all are, since a class that is a subclass of no class is considered (theoretically at least) a subclass of the Object class. They thus constitute a class hierarchy; to outline this hierarchy is the purpose of this section.
Here are some notes about the outline. Some of the built-in classes cannot be subclassed by the programmer. Some of the built-in classes are abstract, meaning they cannot be instantiated but their subclasses can be. Some of the built-in classes are both: they cannot be instantiated and they cannot be subclassed by the programmer; instead, REALbasic has already provided subclasses of them, which you can instantiate (and possibly subclass). Window classes are a special case: programmer-defined subclasses of Window cannot be subclassed.
Built-in instances, such as the Keyboard and System objects, are not listed.
Application (abstract; maximum one subclass per project)
Screen (cannot be subclassed)
Sound (cannot be subclassed)
MouseCursor (cannot be subclassed)
DragItem (cannot be subclassed)
Picture (cannot be subclassed)
Movie (cannot be subclassed)
Graphics (cannot be subclassed)
StyledTextPrinter (cannot be subclassed)
TextInputStream (cannot be subclassed)
TextOutputStream (cannot be subclassed)
BinaryStream (cannot be subclassed)
ResourceFork (cannot be subclassed)
MemoryBlock (cannot be subclassed)
AppleEvent (cannot be subclassed)
AppleEventTemplate (cannot be subclassed)
AppleEventTarget (cannot be subclassed)
AppleEventObjectSpecifier (cannot be subclassed)
Database (cannot be subclassed)
DatabaseCursor (cannot be subclassed)
DatabaseCursorField (cannot be subclassed)
DatabaseQuery (cannot be subclassed)
TextConverter (cannot be subclassed)
TextEncoding (cannot be subclassed)
QuitMenuItem (cannot be subclassed)
Control (abstract; cannot be subclassed)
RectControl (abstract; cannot be subclassed)
The fact that the FolderItem class can be subclassed is probably a bug. The problem is that you can never assign an instance of the subclass any FolderItem functionality, such as the ability to point at a file on disk. The reason is that all FolderItem instances that have such functionality are generated by built-in functions, and these functions yield FolderItems, and a class is not its subclass. I regard the fact that the QuitMenuItem class cannot be subclassed as a bug. In REALbasic Version 1, it could be. ScrollableCursor, SoundConverter, and SoundFormat are listed in the Super popup, but these classes don't exist, so this is a bug.
A thing is global if it is visible at all times to all code. Many of REALbasic's things are not global. For example, suppose a button's Action event handler generates a new Date instance:
dim d as date
d = new date
This instance is not global; in fact, it's just the opposite--it's local. That's because the only reference to it is
dis a variable that exists only inside the button's Action event handler. The instance can't be seen from anywhere else.
Under REALbasic's object model, where everything depends upon references to instances that come and go, it makes a certain sense to speak of degrees of globalness. A property is typically more global than a variable, insofar as the object to which it belongs is more persistent and more readily referred to; a window property is very global, because a window persists until explicitly killed, and because it is generally possible to obtain a reference to a window without maintaining a name for it. Still, windows can be killed, so even a window property is not truly global. Much of the discussion in See Referring to Instances was devoted to this matter, and it will be raised again under .
On the other hand, REALbasic does come with quite a number of things that are truly global. It has many built-in methods belonging to no class; as pointed out in See Messages and Dot Notation, such methods can be regarded as belonging to a global object. And REALbasic has some built-in instances, such as the System object and the Keyboard object, that are global.
This section is about ways in which the programmer can create global methods and properties (and, by implication, global instances, since a property can be a reference to an instance). This isn't a terribly common thing to do, since if you make something global, you're not attaching it to any object, which means you've violated REALbasic's philosophy of object orientation. As a rule of thumb, if you think something needs to be global, think again; there is probably some object to which it properly belongs. But rules of thumb are made to be broken, and it is easy to imagine situations where something should indeed be global.
For example, in Chapter 2 we created a function, Average, which returns the integer average of two integers. Let's imagine that this function is extremely useful, and that we find ourselves needing to call it quite a bit. The question is where to store it in our project. It pertains to no particular object or class; it is a mathematical function, intended in effect to supplement REALbasic's own built-in methods--we are doing in code something we think REALbasic should do intrinsically at the global level. Thus, Average could appropriately be global.
Another example arises when information about an object needs to be maintained in the absence of that object. For example, let's say the user closes a window, and we want it to be the case that the next time a window of that class is instantiated, it should have the same position and dimensions as the window just closed. Where should the window's current position and dimensions be remembered? Not in the window; it's being killed, so the information will be lost. Nor in any object that might die. The information must be maintained no matter what, even if there are no windows or other instances present. Therefore it should be global.
REALbasic provides you with two locations for global storage: modules and the Application subclass.
A module is a global container of methods and properties; these methods and properties belong to no class.
To create a module, choose File New Module; the module's listing appears in the Project Window. You can then tab into the Properties Window to give the module a meaningful name. But this name, and how you distribute things among modules, is purely a matter of organizational convenience; your code knows nothing about it. (Modules are rather like folders in this respect.) To create methods and properties in a module, double-click the module's listing in the Project Window to open its Code Editor and proceed in the usual way.
To access a module member from code, you just use the member's name, with no reference to the module. For example, a method named MyGlobalMethod, which is a procedure taking no parameters, stored in a module named MyModule, would be called by saying:
myGlobalMethod // no mention of MyModule
Since module member names are global, a question arises of how they are resolved with respect to the namespace as a whole. The main part of the answer was given in See Resolution of Names: the module namespace is consulted last, after REALbasic's built-in functions and after implicitly trying to supply Self. Care should be exercised not to overshadow a module member name with a local name accidentally. For example, if a module contains a property named Prop and you use the name
propin a subroutine where
prophas been declared with a
Dimstatement as a local variable, it is the local variable that will be referred to. If a module contains a property named Prop and you use the name
propin a class containing a Prop property (or a subclass of such a class), it is
self.propthat will be referred to. In such situations, there is no way whatever to refer to the Prop in the module.
A module method can refer to other members of the same module by dot notation through the Self function; and if Self is omitted, a module method's references to properties and methods with no dot notation will be resolved by looking in the same module before looking in other modules. This gives modules, if not object-orientation, at least some, er, modularity. Nonetheless, giving two different members in two different modules the same name is probably unwise, since in referring to such members from elsewhere you cannot specify which is meant, and you cannot be certain how the reference will be resolved.
Modules can also contain global constants. A constant is like a property that is initialized in the IDE; its value can be only a literal string, number, or boolean. Aside from the convenience of not having to initialize its value, a global constant's great advantage is that it can be localized for the different languages and platforms. This is done by pressing the Add button in the Edit Constant dialog. In Figure 4-4, for example, the constant
yeshas been localized so as to have the value
"Yes"on a U.S. Macintosh but the value
"Oui"on a French Macintosh. These settings are consulted at the time you build the application: you specify the desired platform and language in the Build Application dialog.
Figure 4-4. Localizing a constant
A constant's value can be accessed in code, by its name; for example:
Constant values can also be assigned to properties initialized in the IDE, such as the caption of a button. To do this, the value supplied in the Properties Window must begin with
#as a sign that it's the name of a constant and not a literal string. For example, if I initialize the Caption property of a PushButton in the IDE as
#yes, the built application will display the button's caption in accordance with the value of the
yesconstant localized for the platform and language of the build. (To make a string property value start with
#, type # twice.)
A constant can't have a calculated value. Since unprintable characters can't be expressed in REALbasic except by calculation (see ), this might seem to mean that a constant string can't contain an unprintable character. But pasting an unprintable character into the Edit Constant dialog does work. So a workaround is to use some other application (such as BBEdit) to create the string containing the unprintable character, then copy and paste it into the Edit Constant dialog.
A constant value can be used to declare the size of a local array variable, but not an array property (I regard this as a bug). For example, given an integer constant HowMany, you can't declare an array property like this:
myArray(howMany) as string
The workaround is to declare the array with an arbitrary size (such as
-1) and Redim it in code:
The Application Subclass
Modules are the only place where global localizable constants can live. Apart from this, the primary advantage of modules is that they can be exported and reused in other projects--like classes except that they don't need to be instantiated. If you don't need either feature, but simply want globally available methods and properties, consider instead subclassing the Application class.
The Application subclass is automatically instantiated as your application starts up, and a reference to this instance is always available through the App function. Thus, if MyGlobalMethod lived in the Application subclass, it would be called by saying:
The Application subclass offers several major advantages over a module as a repository of global members:
- The Application subclass is sent an Open event, which provides an opportunity for its properties to be initialized.
- There is no possibility that access to members of the Application subclass will be cut off by accidental use of identical local names, because all messages to the Application subclass are explicitly directed to
- Modules defeat the purpose of object-orientation, whereas the Application subclass represents a definite "thing" and has a natural right to contain members.
Advanced Class Features
In this section we look at some miscellaneous further topics having to do with classes. Beginners, who may find the details presented here somewhat distracting, may wish to skim this section rather quickly at first, returning to it subsequently as examples arise later in the book or in the course of actual programming.
What's My Window?
Nothing in REALbasic is so important as obtaining a reference to a desired instance, and no instance is so vexing to get a reference to as the window containing a given control. There are three different ways for a control to get a reference to its containing window, depending on the context.
Let's start with code inside a control instance's event handlers. I'm talking here about any code that appears in a window's Code Editor, within its Controls category. Such code can obtain a reference to its containing window by way of the Self function (Chapter 3). This is the only code where Self and Me yield different results; Me returns a reference to the instance itself. So a PushButton instance in a window can refer to the window's title as
Next, consider code inside the Code Editor of a programmer-defined subclass of the built-in Control class. (ScoreBox, from earlier in this chapter, is an example, because it is a subclass of StaticText, which is a subclass of RectControl, which is a subclass of Control.) Here, Self doesn't refer to the instance's containing window, but to the instance itself. However, the Control class has a Window property that returns a reference to the instance's containing window; the subclass inherits this. So, the ScoreBox class can refer to the window's title as
Finally, there is code inside the Code Editor of a programmer-defined class that is not a Control subclass. We presume, obviously, that the instance is being used as a control--that is, it is contained by a window. There is only one reliable way for such code to get a reference to the instance's containing window, and that is to use a New Event.
The technique requires some preparation. You have dragged the class's listing from the Project Window into a Window Editor to make a control. You double-click the class's listing in the Project Window to access its Code Editor. Create a New Event: let's call it Owner, and let's have it be a function that returns a Window. Finally, in the Window Editor, double-click the instance to access its event handlers within the window's Code Editor. There is now an Owner event handler; give it this code:
Code within the class can now refer to the instance's containing window as
owner(). This device lacks elegance, since we must remember to code an Owner event handler individually for every instance in a window; but this is unavoidable.
Both the Control class's Window property and our Owner New Event have a big drawback: they return an instance of the Window class. That's fine if you want to access a feature of the window common to every window, such as its title; but it's not so good if you want to access a feature particular to the window class containing the instance. For example, if a window contains a button called PushButton1, an instance in the window can refer to
self.pushButton1, because Self is always the correct window class, and REALbasic knows that this kind of window has a PushButton1. But you can't say
owner.pushButton1, because the Window class contains no controls. You have to cast to the correct window class. For example, if the type of window containing PushButton1 is a Window1, then you'd say
window1(self.window).pushButton1. If you don't know what type of window contains the control instance, you will first have to test with
IsAto find out.
Constructors and Destructors
If an instance is neither a window nor contained by a window, then if the programmer has added to its class a method handler whose name is the name of the class, that method will be called automatically when the
Newoperator generates the instance. Such a method is called a constructor, and is commonly used to initialize the instance's properties. A constructor can have parameters, and if it does, the syntax of
Newwhen instantiating that class must be modified to match: values for the parameters must follow the class name, in parentheses.
For example, suppose DayInAugust is to be a Date subclass representing a day in August, 1954, and that it has a method DayInAugust, taking one parameter:
Sub dayInAugust(day as integer)
self.day = day
self.month = 8
self.year = 1954
That method is DayInAugust's constructor. Now when we generate a DayInAugust instance, we supply the value to initialize its Day property:
dim d as dayInAugust
d = new dayInAugust(10) // call constructor with parameter
msgbox d.longdate // it worked
A particularly elegant use of a constructor is as a copy constructor. This means that you hand
Newan instance of the class and it returns another instance whose properties are initialized with the same values. This is the same effect achieved by the Clone method suggested on Sub clone(o as myClass), but implemented in reverse, and packaged in a neater syntax. So, for example, if our class is called MyClass:
Sub myClass(o as myClass)
self.itsProperty = o.itsProperty
self.itsOtherProperty = o.itsOtherProperty
Now, given an instance of MyClass, we can generate a new instance that copies it:
dim oo as myClass // and o already exists somewhere
oo = new myClass(o)
It is also possible to make a destructor, which runs when the instance is destroyed. To make a destructor, give the class a method without parameters whose name is the name of the class preceded by
~(tilde). For example, a destructor for DayInAugust would be named
Destructors are automatically called sequentially through the class hierarchy, but constructors are not. Suppose B is a subclass of A and that both B and A have both a constructor and a destructor. Then when B is instantiated, B's constructor is called, but A's is not. When B is destroyed, B's destructor is called, and then A's destructor is called. Suppose now that B has neither constructor nor destructor, but A has both. When B is instantiated and destroyed, A's constructor and destructor are called.
Thus, the rule is that destructors are called all the way up the class hierarchy; but in the case of constructors, REALbasic looks up the hierarchy until it finds a constructor, calls it, and stops.
This does not mean that a superclass constructor can't be called when the subclass constructor is called; it just means that this won't happen automatically. The subclass constructor inherits the superclass constructor as a method and is perfectly free to call to the superclass constructor explicitly, by name. For example, if B wants A's constructor to be called before its own, the first line of B's constructor can say:
Those accustomed to other languages where superclass constructors are automatically called, such as C++, may be surprised at this behavior. It makes a certain sense, though, because it has the advantage of simplicity. Even in C++, after all, a superclass constructor that requires parameters must be called explicitly, so C++ involves a double treatment: sometimes superclass constructors are called automatically, sometimes they aren't. That's just the kind of complexity REALbasic's object model sensibly avoids.
Let's go back to the proviso with which this section started: a constructor is called for an instance that is neither a window nor contained in a window. This implies that a constructor is not called for an instance that is a window or contained in a window. This is not as problematic as it sounds. A window or a Control subclass receives an Open event automatically when it is instantiated, so its Open event handler serves the same purpose as a constructor, and that's what you're supposed to use. Similarly, for a window or a Control subclass, use its Close event handler as a destructor. This ensures that you will get instantiation and destruction notification in an orderly manner.
Nevertheless, there are two curious asymmetries. First, a destructor works for any instance, including a window, or an instance contained by a window; a constructor does not. Second, there is a category of class for which constructors don't work and that receives no Open event--namely, an instance in a window whose class is not a Control subclass; this is a major hole in REALbasic's functionality.
The presence of a constructor in a class can make it impossible for code within that class to cast to that class (because the cast is interpreted as a direct call to the constructor). For example, if a class MyClass has a method MyClass, then it can't cast by saying
myclass(mySuperclassInstance). One workaround, admittedly unpleasant, is to ask an instance of a different class to perform the cast.
The presence of a constructor that expects parameters disables the "default" constructor with no parameters. For example, if a class MyClass has a method MyClass that expects a parameter, then if you say simply
new myclass, you get an error ("New operator has incorrect number of parameters for constructor"). The solution is to add another constructor that takes no parameters, even if this does nothing. This is permissible because of overloading, which is the next topic.
A class can have more than one method with the same name, but taking different types or numbers of parameters. Such methods are said to be overloaded. To be honest, the only thing that's overloaded here is the name; the methods are distinct in REALbasic's mind, because they are readily distinguishable. When you call the method, REALbasic looks at the parameters you supply, and decides on that basis which method you mean.
For example, suppose you have two methods in MyClass, as follows:
Sub greetMe(s as string)
msgbox "Matt: " + s
Sub greetMe(i as integer)
dim c as integer
for c = 1 to i
REALbasic accepts these as distinct methods because their parameter lists are different: one takes a string, the other takes an integer. The following code calls the two methods in succession:
dim c as myclass
c = new myclass
Notice the use of parentheses even though these are procedures taking one parameter. It appears that this is necessary when method overloading is involved. If you get a "Type mismatch" error, lack of parentheses may be the cause.
For two parameter lists to be different, either they must contain a different number of parameters, or at least one corresponding pair of parameters must be of distinct types. A class and its subclass are not distinct types! For example, suppose we have these two methods:
Sub myMethod(c as scoreBox)
Sub myMethod(c as doubleScoreBox)
// ... (DoubleScoreBox is a subclass of ScoreBox)
If you try to call MyMethod with a DoubleScoreBox parameter, REALbasic will complain:
myMethod(theDoubleScoreBox) // error: ambiguous reference
The problem is that since a DoubleScoreBox is also a ScoreBox, either version of MyMethod could accept this parameter, so the call is ambiguous. The situation is not untenable, however; this works:
The explicit cast disambiguates the type of the parameter.
Constructors can be overloaded, but only if the number of parameters differs; the type of parameters fails to distinguish the constructors. This is a major bug. If class A has a constructor with a single string parameter and another constructor with a single integer parameter, then REALbasic will pretend that one of them doesn't exist, and trying to generate a new A instance with it will fail:
dim a as A
a = new A(7) // error: Type mismatch
The workaround, and it's a painful one, is to differentiate constructors by the number of parameters even if this means that some parameters go unused.
Method overloading does not work across subclass/superclass boundaries. For example, make MySubClass a subclass of MyClass, and distribute the two GreetMe implementations across them: give MyClass the GreetMe with the integer parameter, and give MySubClass the GreetMe with the string parameter. Now try this:
dim c as mysubclass
c = new mysubclass
c.greetme(3) // error: Type mismatch
The rules of inheritance seem to be broken. MySubClass inherits from MyClass, and MyClass has a GreetMe that takes an integer; but MySubClass seems not to know about this. The reason is that method names are resolved upward, and resolution stops as soon as it succeeds. So if a class contains any method with a given name, even if it takes the wrong parameters, REALbasic won't even look to see if its superclass also contains a method with that name, and possibly with the correct parameters.
However, there's an easy solution: your subclass has only to override the other superclass method. In our example, we'd give MySubClass a GreetMe method that does take an integer parameter. Now the subclass contains a method whose parameters match those of the call, and everything is fine. Well, not everything. There's still a problem: the functionality you wanted is in the superclass! Luckily, there's a way for code in the subclass to call code in the superclass; that's the next topic.
Code in a class method can direct a method call at its superclass. This is called a class-directed message. The syntax is simple: in a dot notation expression, before the dot, instead of using an instance name, you use a class name. This must be a superclass of the class in which the call appears; in other words, class-directed messages can be directed only up the hierarchy from the class containing the code that's sending the message. This restriction makes sense; a subclass should have the ability to decide how it wants to take advantage of the storehouse of methods it inherits from its superclasses, but it shouldn't be possible to subvert the hierarchy in other ways.
For example, let's go back to our ScoreBox and DoubleScoreBox implementations from earlier in this chapter. It may have occurred to you that there was something inelegant about our implementation of Increase. Here, you recall, is ScoreBox's Increase handler:
self.score = self.score + 1
Here is DoubleScoreBox's Increase handler:
self.score = self.score + 2
The routines are near duplicates of each other, and this duplication obscures the essential relationship between DoubleScoreBox and ScoreBox: a DoubleScoreBox is everything that a ScoreBox is, and then some. In particular, a DoubleScoreBox is a ScoreBox that increases its score one more than a ScoreBox does. To capture this fact, we can have DoubleScoreBox increase its score by
1(it increases its score one more than a ScoreBox does), and then call ScoreBox's Increase handler (it increases its score one more than a ScoreBox does):
self.score = self.score + 1
In the same way, you can see how class-directed methods solve the problem posed in the previous section. MyClass has one GreetMe method; MySubClass overrides that one and adds another. MySubClass's implementation of the overriding GreetMe can be as simple as calling the overridden GreetMe.
Class-directed messages don't have to have the same name as the method that was originally called, as in these examples. And the method that is called doesn't have to live in the class to which you send the message; it can live in a superclass of that class. Perhaps the best way to envision class-directed messages is to think of them as directed at Me, the instance, but with a proviso as to where resolution of the name should begin. In such a scenario, this line:
actually means, "
me.greetMe(i), but in resolving the name
greetMe, skip upward past all classes until you reach MyClass; start resolving upward from there."
Object-oriented programming is, in a sense, a way for the programmer to participate in the design of the programming language itself. When we create a class and populate it with members, we are telling other classes how they should speak when they want to communicate with this class.
However, other classes may have the power to speak too freely. We may intend that every instance of the ScoreBox class, for example, should be the "owner and protector" of its own score; when any other class wants to increase a score, it should send the instance an Increase message. But other classes can subvert our intentions, by manipulating the instance's Score property directly:
theScoreBox.score = theScoreBox.score + 1
It isn't enough to say, since we are writing the code ourselves, that therefore in actual fact no other class will speak this way, because we have a "rule" that this is forbidden. Where is this "rule"? If it is just in our own head, it might fall out of our head (we could forget); also, what's in our head is not in the head of some other programmer who might some day come to maintain the project, or to whom we might send an exported class. Self-restraint is no restraint.
REALbasic alleviates such problems by permitting you to declare a member as private. This is done by ticking the Private checkbox in the method or property's declaration dialog. The question is then who is allowed to access a private member. Once a class member is private, it is accessible only from the same class or its subclasses; one instance of a class (or its subclasses) can access a private member of the same instance or another instance of the same class (or its subclasses).
This solves the difficulty with the Score property. If Score is private, then Increase is the only way for outsiders to do anything to the score; Increase is a setter for Score. Similarly, if we want outsiders to be able to learn the score, they can't read Score directly, so we must provide a getter:
Function score() as integer
Experienced OOP programmers will observe that REALbasic's "private" corresponds to C++'s "protected"; total privacy, where not even a subclass can access a private member, is not available in REALbasic, and neither is "friendship," where a class may declare a limited list of other classes that can access its private members.
There is no visual indication in the Code Editor that a method or property is private; you have to double-click its browser listing and inspect its declaration dialog.
Class Properties and Class Methods
A class property (sometimes called a static property) is a property whose value is shared among all instances of the class. A class method (sometimes called a static method) is a method that can be called without directing a message at any particular instance of the class (though it does still live in that class). In Chapter 3 it was pointed out that REALbasic lacks class properties and class methods.
This section describes an architecture that helps to compensate for this lack. The idea is to take advantage of the fact that REALbasic permits global instances. We will store a special instance of the class as a property of the Application subclass. This instance will act as a "static" representative of the class; it will function as a master repository of information, and as an instance that can respond to messages in the absence of all other instances.
As an example, let's posit a Widget class whose "static" representative is a Widget instance named WidgetMaster, a property of the Application subclass. We instantiate this property in the Application subclass's Open event, so that the WidgetMaster instance is absolutely global:
self.widgetMaster = new widget
Let's say that we wish to be able to ask any Widget how many Widget instances there are. That's like a class property: every Widget instance should give the same answer. The job of WidgetMaster will be to know this answer and to make it available to any Widget instance. Let Widget have a property MasterCount that is an integer. Only WidgetMaster's value for this property will be correct, so to prevent accidents we make MasterCount private. Widget's constructor increments the MasterCount:
app.widgetMaster.masterCount = app.widgetMaster.masterCount + 1
exceptionline isn't explained until Chapter 8, but the point is that the very first time this constructor is called, the WidgetMaster instance won't exist yet--because it's the instance that's being created! We don't mind this, because we don't want WidgetMaster included in the count anyway; but we have to prevent the application from terminating prematurely, and that's what the
exceptionline does. Widget's destructor obviously just decrements the count:
app.widgetMaster.masterCount = app.widgetMaster.masterCount - 1
Widget's Count method consults WidgetCount's MasterCount. Thus, Count acts as a mediator to MasterCount; nonwidgets can't get access to MasterCount except indirectly through Count, which acts as a getter:
Function count() As integer
This works beautifully, as we can confirm through the following test:
dim w1, w2 as widget
w1 = new widget
w2 = new widget
msgbox str(w1.count) // 2, as expected
Furthermore, we can learn the count globally, without a reference to a particular widget instance, by asking WidgetMaster:
The example could be much extended to demonstrate such powerful techniques as actual instance management being performed by the master instance (a factory architecture), but that would go beyond the scope of this book.
In this section we sketch a few ideas for programmer-defined classes that illustrate useful basic features of classes.
This is the Stack class promised in Chapter 3. A stack is a dynamic storage mechanism, where items are added or removed one at a time, and only the most recently added item is accessible (like a stack of plates). You understand that it's completely unnecessary to implement a Stack class from scratch in REALbasic, because REALbasic provides dynamic arrays! So, we could easily implement a stack by using an array, adding items with Append and removing them with Remove. However, the example provides a flexible model that can easily be modified to form other storage mechanisms such as queues, and illustrates the power of object references as pointers to form linked data structures.
Our implementation, you recall, is that each item of the stack will contain a value and a pointer back to the previously added item of the stack; the earliest added item will point to
nil. In Chapter 3 these were called ItsValue and ItsPointer, so let's keep those names. We'll make it a stack of strings; so ItsValue is declared as string, and ItsPointer is declared as Stack. The chief actions on a stack are to add (push) or remove (pop) an item at the front; it's also useful to know whether the stack is empty. The basic implementation, shown in Example 4-1, is a simple matter of manipulating pointers.
Example 4-1: Stack class
Sub push(s as string)
dim newItem as stack
newItem = new stack
newItem.itsPointer = self.itsPointer
newItem.itsValue = s
self.itsPointer = newItem
Function pop() As string
dim value as string
value = self.itsPointer.itsValue
self.itsPointer = self.itsPointer.itsPointer
Function isEmpty() As boolean
return (self.itsPointer = nil)
We may test the Stack class as follows:
dim st as stack
dim s as string
st = new stack
s = st.pop() // throw away, just testing
while not st.isEmpty // pop and show whole stack
msgBox st.pop() // expect "no", "nonny", "hey", "hey"
The onus is on the caller not to pop an empty stack; in real life, Pop would probably raise an exception if the stack is empty, as described in Chapter 8. The example is remarkably economical. You might think we'd need two classes, one for the items of the stack and one to represent the stack as whole; instead, the reference that points at the stack is itself just like the items of the stack, but it doesn't store anything in ItsValue.
We would like to extend the power of arrays. We cannot subclass an array, because an array is not a class. But we can make a class that has an array as a member and provides an extended interface to it. Such a class is called a wrapper. This section presents a class CArray that is a wrapper for a one-dimensional array of strings.
There are several advantages to this class over a normal array. We will be able to define new array operations that ordinary arrays lack: for instance, a Swap function will cause the values of two items to be interchanged, a Reverse function will allow us to sort either ascending or descending, a CopyTo function will copy all items from one array into another, and a Clone method will create a new instance and CopyTo that. Also, it will be easy to subclass CArray to provide for even more specialized types of arrays; imagine, for example, an array that is constantly kept sorted by means of a binary search. Most important, an array wrapper class is a real class, as opposed to an ordinary array in REALbasic, which is neither a class nor a datatype, neither object nor scalar; an array wrapper class is a full citizen and can do things that an ordinary array cannot. For example, you cannot declare an array of array, but you can declare an array of a class that wraps an array; and a class that wraps a multidimensional array can be passed as a parameter to a subroutine (unlike an ordinary multidimensional array).
Example 4-2 gives a sample implementation. We provide wrapper functions for all the standard array operations, using the standard names (except that we cannot name a method Redim because that's a reserved word, so MyRedim is used), plus Set and Get (because we can't overload the assignment operator), plus our various extensions. The private array property is called TheArray; it is declared as being of size
-1initially, but a constructor is provided that lets us create a new CArray instance and Redim it to the desired size in a single command.
Example 4-2: cArray class
// constructor with no parameters
// do nothing, accept declaration to -1
Sub cArray(size as integer)
// constructor with one parameter
Sub myRedim(size as integer)
Sub set(index as integer, s as string)
self.theArray(index) = s
Function get(index as integer) As string
Sub insert(index as integer, s as string)
self.theArray.insert index, s
Sub append(s as string)
Sub remove(index as integer)
Function ubound() As integer
Sub swap(index1 as integer, index2 as integer)
dim s as string
s = self.get(index1)
dim i, u, u2 as integer
u = self.ubound()
u2 = u\2
for i = 0 to u2
Sub copyTo(c as cArray)
dim i, u as integer
u = min(self.ubound(), c.ubound())
for i = 0 to u
Function clone() As cArray
dim c as cArray
c = new cArray(self.ubound())
// for debugging
dim i, u as integer
u = self.ubound()
for i = 0 to u
Here is a brief test routine:
dim a, b, c as cArray
a = new cArray(4)
b = new cArray(2)
msgbox "Here is a:"
a.display // expect "hey", "ho", "hey", "nonny", "no"
msgbox "Here is b:"
b.display // expect "hey", "ho", "hey"
c = a.clone()
msgbox "Here is c:"
c.display // expect "yo", "no", "ho"
A hash is a storage structure that's like a pigeonhole desk for storing index cards: a glance at a card tells us instantly which pigeonhole it goes into, but inside each pigeonhole the cards are just a jumble. The means used to identify the correct pigeonhole is called the hash function. For example, if the cards contain text, the first letter of the text on a card to be stored could act as a hash function, resulting in 26 pigeonholes. This wouldn't be a very good hash function if all the texts started with the same few letters, or if there were many index cards, because some pigeonholes would end up with a jumble of many cards. But when the hash function and the nature and size of the data are such that we are likely to end up with an even distribution where just a very few items end up in each pigeonhole, hashing is a very fast and efficient mode of storage.
Hashing is typically a two-stage process: first use the hash function to choose the correct pigeonhole; then manipulate the jumble which that pigeonhole represents. So apart from the hash function, we must also decide how to implement the jumble. We'll use a CArray, the string array wrapper class developed in the previous example.
Let's say, then, that the problem is to store every individual word of a text, without storing any duplicates. We propose to implement a hash of strings as an array of CArray--except that to prevent duplicates we will need to be able to ask each CArray whether it already contains a word, so we will use a subclass of CArray called MyCArray which adds a Contains method handler:
Function contains(s as string) As boolean
dim i, u as integer
u = self.ubound
for i = 0 to u
if self.get(i) = s then
For the sake of generality and flexibility, we will factor out the hash function from the class that does the hashing; that is, we will tell our hashing class instance in code what hash function to use, rather than hardcoding this information into the class itself. A different hash function can thus be substituted without modifying the class that does the hashing. To accomplish this goal, the hash function will be expressed by a class that implements a class interface. Call this class interface HashFuncPtr, and give it a method called HashFunc which accepts a string and returns an integer. This means that any class that has a HashFunc method can act as a HashFuncPtr, and can be used by our hashing class to derive the index for any string. (This technique is REALbasic's equivalent of pointer-to-function.)
For this example, we will use a very fast but extraordinarily primitive and unrealistic hash function: we will sum the numeric equivalents of the first and last letters of the string (these numeric equivalents are discussed in Chapter 5). Create a class PrimitiveHashFunc which implements HashFuncPtr; its HashFunc method goes like this:
Function hashFunc(s as string) As integer
return ascb (leftb(s, 1)) + ascb (rightb(s, 1))
We come at last to the class that does the hashing, which we call Hasher. It has two properties, ItsArray (the array of MyCArray, initially declared to size
-1) and ItsHashFunction (the HashFuncPtr containing the hash function). The code, displayed in Example 4-3, is minimal: we can store strings, but we cannot retrieve them except for debugging purposes. The instance is initialized through its constructor, which needs to be told how big the array should be and what hash function to use. Storage is then performed through the InsertIfAbsent method. The GetHashIndex method just abstracts the code for calling the hash function.
Example 4-3: Hasher class
Sub hasher(size as integer, f as hashFuncPtr)
dim i as integer
for i = 0 to size
self.itsarray(i) = new myCArray(-1)
itsHashFunction = f
Function getHashIndex(s as string) As integer
Sub insertIfAbsent(s as string)
dim i as integer
i = self.getHashIndex(s)
if self.itsArray(i).contains(s) then // nothing to do
Function displayDump() As string
// for debugging
dim result as string
dim i, j, u, uu as integer
uu = ubound(itsArray)
for i = 0 to uu
u = self.itsArray(i).ubound
for j = 0 to u
result = result + "," + self.itsArray(i).get(j)
Here is some code to test Hasher. Since the maximum numeric equivalent of a letter is 255, the largest hash value possible is 510.
dim h as hasher
h = new hasher(510, new primitiveHashFunc)
// simple test just to show that it really is hashing
msgbox h.displayDump // expect to see each distinct word once
A map is a collection of name-value pairs, a way of storing a value keyed by an arbitrary name (as opposed to an array, which stores a value keyed by an index number). Given a name, you can add it and a corresponding value to the collection, or, if the name is already defined in the collection, you can retrieve or modify its value. REALbasic provides a class which behaves this way (the Collection class, discussed in Chapter 5), but it's inefficient, and besides, it's good exercise to create one for ourselves.
Let's pose a problem like that of the previous section: we wish to store every individual word of a text, without storing any duplicates; but this time we will attach to each word a count of how many times it occurs in the text. We are thus pairing a name (the word) with an integer (the count). So we create a class, Pair, to express this pairing, with two properties, Key and Value. Key, obviously, is a string. You might think Value would be an integer, but for reasons that will be clear later on, we choose to make it an IntPtr, which is a class consisting of a single integer property named V (for "value"). Pair will have a constructor:
Sub pair(s as string)
self.key = s
self.value = new intPtr
This initializes the Key to the string value passed in, and the Value to an IntPtr whose V will be autoinitialized to zero without any help from us.
The pairs will be maintained as an array. Since arrays aren't classes, we will use a CArray, the array wrapper class developed earlier. Unfortunately, CArray is a string array wrapper; now we need it to be a Pair array wrapper. This means we'll have to rewrite CArray, running through every method and property declaration and all the code, making the appropriate changes. (We will also have to disable the Sort method, because we presently have no way to sort an array of objects.) The full implementation is left as an exercise for the reader, but as an example, here's CArray's revised Set method:
Sub set(index as integer, s as pair)
self.theArray(index) = s
Are we really going to have to go through this maddening exercise with CArray every time we change the datatype of the array? No, thank heavens; in Chapter 5 we'll meet the variant datatype, and with its help we will rewrite CArray one last time, so that it wraps an array of anything without further rewriting. The sorting problem is easily solved with the help of Thomas Tempelmann's Sort class, mentioned earlier in this chapter.
We next face the problem of how the CArray will be maintained for efficient access. Given a key, we need to know quickly whether the CArray contains a Pair with that key. Clearly the worst solution is to keep the pairs in any old order; they should be kept sorted. This sounds like a job for binary search, the algorithm for rapid access to a sorted array described under .
For the sake of clarity and flexibility, we'll package up the binary search algorithm as a class, which we'll call BinarySearch. Give it one property, a CArray which we'll just call A. Things are a little trickier than they were back in Chapter 2 because a search now needs to hand back two pieces of information: First, was the desired key found? If so, at what index? If not, at what index should it be inserted so as to maintain the array in sorted order? So we'd like BinarySearch to have a Find method that will return two values, Index (an integer) and Found (a boolean). But a subroutine can't return two values, so we'll package up this returned value as a class called IndexAndFound, consisting of two properties, Index (an integer) and Found (a boolean), and with a constructor that sets them:
Sub indexAndFound(index as integer, found as boolean)
self.index = index
self.found = found
The BinarySearch class is shown as Example 4-4. The constructor points A at the correct CArray. FindBinaryRecursive is identically the algorithm from Chapter 2, except that we are now looking at strings, not integers, and we must change slightly the way we speak in order to access an element of the array because it is now a CArray of Pairs. The public interface to BinarySearch is the Find method: it wraps the call to FindBinaryRecursive in a test for boundary conditions and in an adjustment of the Index in case we don't find the desired key (in that case, remember, the Index must say where to insert a new Pair), and hands back the result as an IndexAndFound.
Example 4-4: BinarySearch class
Sub binarySearch(a as cArray)
self.a = a
Function find(whatStr as string) As indexAndFound
dim index as integer
dim found as boolean
dim foundStr as string
if a.ubound < 0 or whatStr < a.get(0).key then
return new indexAndFound(0, false)
index = findBinaryRecursive(0, a.ubound, whatStr)
foundStr = a.get(index).key
found = (foundStr = whatStr)
if not found then
if whatStr > foundStr then
index = index + 1
return new indexAndFound(index, found)
Function findBinaryRecursive(low as integer, hi as integer, whatStr as string) As integer
dim i as integer
dim s as string
i = (low + hi) / 2
s = a.get(i).key
if s = whatStr then
if hi <= low then
if s > whatStr then
return findBinaryRecursive(low, i-1, whatStr)
return findBinaryRecursive(i+1, hi, whatStr)
We come at last to the Map class itself. It has two properties. One is Pairs, the CArray in which the name-value pairs will be kept. The other is B, the BinarySearch instance that will be used to access the binary search algorithm. Both properties exemplify typical uses of the "Has-A" relationship between classes. Map has a CArray because that's its data. Map has a BinarySearch because the binary search algorithm is Map's servant and no one else's, and because this particular BinarySearch instance is to be pointed only at this particular CArray.
The Map class is shown as Example 4-5. A constructor initializes the Pairs CArray, and creates the BinarySearch instance, pointing the latter's CArray pointer at the former; the two properties are thus correctly hooked up at the outset. The Get and Size methods are very simple and are thrown in mostly for debugging purposes. The Refer method is the workhorse of the class; given a key, it either returns its corresponding Value, or else (if no Pair with that key exists) it creates a new Pair with that key at the appropriate index and returns its corresponding Value.
You should now see why a Pair's Value is an IntPtr, not a mere integer. It isn't Map's job to understand how it is being used; it's the job of whoever creates and manipulates the Map instance. Therefore, we don't wish merely to tell the caller of Refer what the value is; we wish to give the caller complete access to that value, so that the caller can modify it as desired. Ideally we'd like to return an integer
ByRef, but a function can't return a
ByRefresult; so we return an instance of a class that wraps the integer and behaves as a pointer to it. This is a REALbasic example of what C++ would call "returning a reference."
Example 4-5: Map class
pairs = new cArray
b = new binarySearch(pairs)
Function refer(s as string) As intptr
dim result as indexAndFound
result = b.find(s)
if not result.found then
if result.index > pairs.ubound then
pairs.append new pair(s)
pairs.insert result.index, new pair(s)
Function get(i as integer) As intptr
Function size() As integer
To test Map for the purposes of our original problem, we make a utility subroutine Increment which accepts an IntPtr and increments the integer it points to:
Sub increment(ip as intptr)
ip.v = ip.v + 1
Here's a test routine:
dim m as map
dim i,u as integer
m = new map
u = m.size
for i = 0 to u
msgbox m.get(i).key + ":" + str(m.get(i).value.v)
Sure enough, we are counting the word occurrences; we get
a:1, and:1, is:2, and so forth, as expected. Aside from exemplifying many powerful REALbasic class techniques, the Map class is of great practical utility; I really did write it originally to handle the task of counting the occurrences of distinct words in a document, and on my machine it can accumulate the data for a 4500-word document, containing about 1400 distinct words, in slightly over a second.
1. For a list of what built-in classes can be subclassed, see "The Class Hierarchy" later in this chapter.
2. Insert your own Bill Clinton joke here.
3. I have coined this term based on a suggestion by Quinn.
4. But if you try this with a DoubleScoreBox in the window, the DoubleScoreBox will misbehave: it will execute ScoreBox's Increase handler, not DoubleScoreBox's Increase handler. That's because ScoreBox is declared as an Increaser and DoubleScoreBox isn't. It's as if class interfaces had the power to disable the virtual method mechanism between superclass and subclass. The workaround is to declare DoubleScoreBox, too, as implementing Increaser; but I regard the fact that this is necessary as a bug.
5. This example should help immigrants from the C++ world, seeking the REALbasic equivalent of such devices as multiple inheritance, class templates and pointer-to-function. The example is lifted entirely from Thomas Tempelmann's Sort class, http://www.tempel.org/rb/#SortInterface, which is well worth downloading and studying; it certainly opened my eyes as to what class interfaces could do.
6. I owe this observation to Steve Fyfe.
7. The built-in objects are variously treated in the chapters devoted to their functionality. For example, for the Keyboard object, see Chapter 19.
8. For local constants, see Chapter 2.
9. Dates are discussed in Chapter 5.
10. It happens that in this example there's another workaround, namely, to cast the instance as a MyClass. This hides from REALbasic the existence of the method that takes the wrong parameters. So, if
cis the MySubClass instance, you can say
myClass(c).greetMe(3). However, this use of casting up, effectively delivering a class-directed message from outside the instance, is probably to be deprecated (though it was quite standard in Version 1 of REALbasic).
11. Insert your own Monica Lewinski joke here.
Back to: Sample Chapter Index
Back to: REALbasic: The Definitive Guide, 2nd Edition
© 2001, O'Reilly & Associates, Inc.