Smalltalk80LanguageImplementation:Chapter 06
- Chapter 6 Protocol for all Objects
Protocol for all Objects
Object***
Magnitude
Character
Date
Time
Number
Float
Fraction
Integer
LargeNegativeInteger
LargePositiveInteger
SmallInteger
LookupKey
Association
Link
Process
Collection
SequenceableCollection
LinkedList
Semaphore
ArrayedCollection
Array
Bitmap
DisplayBitmap
RunArray
String
Symbol
Text
ByteArray
Interval
OrderedCollection
SortedCollection
Bag
MappedCollection
Set
Dictionary
IdentifyDictionary
Stream
PositionableStream
ReadStream
WriteStream
ReadWriteStream
ExternalStream
FileStream
Random
File
FileDirectory
FilePage
UndefinedObject
Boolean
False
True
ProcessorScheduler
Delay
SharedQueue
Behavior
ClassDescription
Class
MetaClass
Point
Rectangle
BitBit
CharacterScanner
Pen
DisplayObject
DisplayMedium
Form
Cursor
DisplayScreen
InfiniteForm
OpaqueForm
Path
Arc
Circle
Curve
Line
LinearFit
Spline
Everything in the system is an object. The protocol common to all objects in the system is provided in the description of class Object. This means that any and every object created in the system can respond to the messages defined by class Object. These are typically messages that support reasonable default behavior in order to provide a starting place from which to develop new kinds of objects, either by adding new messages or by modifying the response to existing messages. Examples to consider when examining Object's protocol are numeric objects such as 3 or 16.23, collections such as 'this is a string' or #(this is an array), nil or true, and class-describing objects such as Collection or SmallInteger or, indeed, Object itself.
The specification of protocol for class Object given in this chapter is incomplete. We have omitted messages pertaining to message handling, special dependency relationships, and system primitives. These are presented in Chapter 14.
Testing the Functionality of an Object
Every object is an instance of a class. An object's functionality is determined by its class. This functionality is tested in two ways: explicit naming of a class to determine whether it is the class or the superclass of the object, and naming of a messageselector to determine whether the object can respond to it. These reflect two ways of thinking about the relationship among instances of different classes: in terms of the class/subclass hierarchy, or in terms of shared message protocols.
testing functionality | |
class | Answer the object which is the receiver's class. |
isKindOf: aClass | Answer whether the argument, aClass, is a superclass or class of the receiver. |
isMemberOf: aClass | Answer whether the receiver is a direct instance of the argument, aClass. This is the same as testing whether the response to sending the receiver the message class is the same as (==) aClass. |
respondsTo: aSymbol | Answer whether the method dictionary of the receiver's class or one of its superclasses contains the argument, aSymbol, as a messageselector. |
Object instance protocol |
Example messages and their corresponding results are
expression | result |
3 class | SmallInteger |
#(this is an array) isKindOf: Collection | true |
#(this is an array) isMemberOf: Collection | false |
#(this is an array) class | Array |
3 respondsTo: #isKindOf: | true |
#(1 2 3) isMemberOf: Array | true |
Object class | Object class |
Comparing Objects
Since all information in the system is represented as objects, there is a basic protocol provided for testing the identity of an object and for copying objects. The important comparisons specified in class Object are equivalence and equality testing. Equivalence ( == ) is the test of whether two objects are the same object. Equality ( = ) is the test of whether two objects represent the same component. The decision as to what it means to be "represent the same component" is made by the receiver of the message; each new kind of object that adds new instance variables typically must reimplement the = message in order to specify which of its instance variables should enter into the test of equality. For example, equality of two arrays is determined by checking the size of the arrays and then the equality of each of the elements of the arrays; equality of two numbers is determined by testing whether the two numbers represent the same value; and equality of two bank accounts might rest solely on the equality of each account identification number.
The message hash is a special part of the comparing protocol. The response to hash is an integer. Any two objects that are equal must return the same value for hash. Unequal objects may or may not return equal values for hash. Typically, this integer is used as an index to locate the object in an indexed collection (as illustrated in Chapter 3). Anytime = is redefined, hash may also have to be redefined in order to preserve the property that any two objects that are equal return equal values for hash.
comparing | |
== anObject | Answer whether the receiver and the argument are the same object. |
= anObject | Answer whether the receiver and the argument represent the same component. |
~= anObject | Answer whether the receiver and the argument do not represent the same component. |
~~ anObject | Answer whether the receiver and the argument are not the same object. |
hash | Answer an Integer computed with respect to the representation of the receiver. |
Object instance protocol |
The default implementation of = is the same as that of ==.
Some specialized comparison protocol provides a concise way to test for identity with the object nil.
testing | |
isNil | Answer whether the receiver is nil. |
notNil | Answer whether the receiver is not nil. |
Object instance protocol |
These messages are identical to == nil and ~~ nil, respectively. Choice of which to use is a matter of personal style.
Some obvious examples are
expression | result |
nil isNil | true |
true notNil | true |
3 isNil | false |
#(a b c) = #(a b c) | true |
3 = (6/2) | true |
#(1 2 3) class == Array | true |
Copying Objects
There are two ways to make copies of an object. The distinction is whether or not the values of the object's variables are copied. If the values are not copied, then they are shared (shallowCopy); if the values are copied, then they are not shared (deepCopy).
copying | |
copy | Answer another instance just like the receiver. |
shallowCopy | Answer a copy of the receiver which shares the receiver's instance variables. |
deepCopy | Answer a copy of the receiver with its own copy of each instance variable. |
Object instance protocol |
The default implementation of copy is shallowCopy. In subclasses in which copying must result in a special combination of shared and unshared variables , the method associated with copy is usually reimplemented , rather than the method associated with shallowCopy or deepCopy.
As an example, a copy (a shallow copy) of an Array refers to the same elements as in the original Array, but the copy is a different object. Replacing an element in the copy does not change the original. Thus
expression | result |
a ← #('first' 'second' 'third') | ('first' 'second' 'third') |
b ← a copy | ('first' 'second' 'third') |
a = b | true |
a == b | false |
(a at: 1) == (b at: 1) | true |
b at: 1 put: 'newFirst' | 'newFirst' |
a = b | false |
a ← 'hello' | 'hello' |
b ← a copy | 'hello' |
a = b | true |
a == b | false |
Figure 6.1 shows the relationship between shallow and deep copying. Tofurther illustrate the distinction between shallowCopy and deepCopy, take as an example a PersonnelRecord Suppose it is defined to include the variable insurancePlan, an instance of class insurance. Suppose further that each instance of Insurance has a value associated with it representing the limit on medical coverage. Now suppose we have created employeeRecord as a prototypical instance of a PersonnelRecord. By "prototypical" we mean that the object has all of the initial attributes of any new instance of its class, so that instances can be created by simply copying it rather than sending a sequence of initialization messages. Suppose further that this prototypical instance is a class variable of PersonnelRecord and that the response to creating a new PersonnelRecord is to make a shallow copy of it; that is, the method associated with the message new is ↑employeeRecord copy.
As a result of evaluating the expression
joeSmithRecord ← PersonnelRecord new
joeSmithRecord refers to a copy (in particular, a shallow copy) of employeeRecord.
The prototype employeeRecord and the actual record joeSmithRecord share a reference to the same insurance plan. Company policy may change. Suppose PersonnelRecord understands the message changeInsuranceLimit: aNumber, which is implemented by having the prototypical instance of PersonnelRecord, empioyeeRecord, reset its insurance plan limit on medical coverage. Since this insurance plan is shared, the result of evaluating the expression
PersonnelRecord changeInsuranceLimit: 4000
is to change the medical coverage of all employees. In the example, both the medical coverage referenced by employeeRecord and that referenced by its copy, joeSmithRecord, is changed. The message changeInsuranceLimit: is sent to the class PersonnelRecord because it is the appropriate object to broadcast a change to all of its instances.
Accessing the Parts of an Object
There are two kinds of objects in the Smalltalk-80 system, objects with named variables and objects with indexed variables. Objects with indexed variables may also have named instance variables. This distinction is explained in Chapter 3. Class Object supports six messages intended to access the indexed variables of an object. These are
accessing | |
at: index | Answer the value of the indexed instance variable of the receiver whose index is the argument, index. If the receiver does not have indexed variables, or if the argument is greater than the number of indexed variables, then report an error. |
at: index put: anObject | Store the argument, anObject, as the value of the indexed instance variable of the receiver whose index is the argument, index. If the receiver does not have indexed variables, or if the argument is greater than the number of indexed variables, then report an error. Answer anObject. |
basicAt: index | Same as at: index. The method associated with this message, however, cannot be modified in any subclass. |
basicAt: index put: anObject | Same as at: index put: anObject. The method associated with this message, however, cannot be modified in any subclass. |
size | Answer the receiver's number of indexed variables. This value is the same as the largest legal index. |
basicSize | Same as size. The method associated with this message, however, cannot be modified in any subclass. |
Object instance protocol |
Notice that the accessing messages come in pairs; one message in each pair is prefixed by the word basic meaning that it is a fundamental system message whose implementation should not be modified in any subclass. The purpose of providing pairs is so that the external protocol, at:, at:put:, and size, can be overridden to handle special cases, while still maintaining a way to get at the primitive methods. (Chapter 4 includes an explanation of "primitive" methods, which are methods implemented in the virtual machine for the system.) Thus in any method in a hierarchy of class descriptions, the messages, basicAt:, basicAt:put:, and basicSize, can always be used to obtain the primitive implementations. The message basicSize can be sent to any object; if the object is not variable length, then the response is 0.
Instances of class Array are variable-length objects. Suppose letters is the Array #(a b d f j m p s). Then
expression | result |
letters size | 8 |
letters at: 3 | d |
letters at: 3 put: #c | c |
letters | (a b c f j m p s) |
Printing and Storing Objects
There are various ways to create a sequence of characters that provides a description of an object. The description might give only a clue as to the identity of an object. Or the description might provide enough information so that a similar object can be constructed. In the first case (printing), the description may or may not be in a well-formatted, visually pleasing style, such as that provided by a Lisp pretty-printing routine. In the second case (storing), the description might preserve information shared with other objects.
The message protocol of the classes in the Smalltalk-80 system support printing and storing. The implementation of these messages in class Object provides minimal capability; most subclasses override the messages in order to enhance the descriptions created. The arguments to two of the messages are instances of a kind of Stream; Streams are presented in Chapter 12.
printing | |
printString | Answer a String whose characters are a description of the receiver. |
printOn: aStream | Append to the argument, aStream, a String whose characters are a description of the receiver. |
storing | |
storeString | Answer a String representation of the receiver from which the receiver can be reconstructed. |
storeOn: aStream | Append to the argument, aStream, a String representation of the receiver from which the receiver can be reconstructed. |
Object instance protocol |
Each of the two kinds of printing is based on producing a sequence of characters that may be shown on a display screen, written on a file, or transferred over a network. The sequence created by storeString or storeOn: should be interpretable as one or more expressions that can be evaluated in order to reconstruct the object. Thus, for example, a Set of three elements, $a, $b, and $c, might print as
Set ($a $b $c)
while it might store as
(Set new add: $a, add: $b, add: $c)
Literals can use the same representation for printing and storing. Thus the String 'hello' would print and store as 'hello'. The Symbol #name prints as name, but stores as #name.
For lack of more information, the default implementation of printString is the object's class name; the default implementation of storeString is the class name followed by the instance creation message basicNew, followed by a sequence of messages to store each instance variable. For example, if a subclass of Object, say class Example, demon-strated the default behavior, then, for eg, an instance of Example with no instance variables, we would have
expression | result |
eg printString | 'an Example' |
eg storeString | '(Example basicNew)' |
Error Handling
The fact that all processing is carried out by sending messages to objects means that there is one basic error condition that must be handled by the system: a message is sent to an object, but the message is not specified in any class in the object's superclass chain. This error is determined by the interpreter whose reaction is to send the original object the message doesNotUnderstand: aMessage. The argument, aMessage, represents the failed messageselector and its associated arguments, if any. The method associated with doesNotUnderstand: gives the user a report that the error occurred. How the report is presented to the user is a function of the (graphical) interface supported by the system and is not specified here; a minimum requirement of an interactive system is that the error message be printed on the user's output device and then the user be given the opportunity to correct the erroneous situation. Chapter 17 illustrates the Smalltalk-80 system error notification and debugging mechanisms.
In addition to the basic error condition, methods might explicitly want to use the system error handling mechanism for cases in which a test determines that the user program is about to do something unacceptable. In such cases, the method might want to specify an error comment that should be presented to the user. A typical thing to do is to send the active instance the message error: aString, where the argument represents the desired comment. The default implementation is to invoke the system notification mechanism. The programmer can provide an alternative implementation for error: that uses application-dependent error reporting.
Common error messages are supported in the protocol of class Object. An error message might report that a system primitive failed, or that a subclass is overriding an inherited message which it can not support and therefore the user should not call upon it, or that a superclass specifies a message that must be implemented in a subclass.
error handling | |
doesNotUnderstand: aMessage | Report to the user that the receiver does not understand the argument, aMessage, as a message. |
error: aString | Report to the user that an error occurred in the context of responding to a message to the receiver. The report uses the argument, aString, as part of the error notification comment. |
primitiveFailed | Report to the user that a method implemented as a system primitive has failed. |
shouldNotImplement | Report to the user that, although the superclass of the receiver specifies that a messageshould be implemented by subclasses, the class of the receiver cannot provide an appropriate implementation. |
subclassResponsibility | Report to the user that a method specified in the superclass of the receiver should have been implemented in the receiver's class. |
Object instance protocol |
A subclass can choose to override the error-handling messages in order to provide special support for correcting the erroneous situation. Chapter 13, which is about the implementation of the collection classes, provides examples of the use of the last two messages.