Imagine that you need an array, but alas you need that if an index is out of bounds, it returns nil. You could modify the Smalltalk implementation, but that might break some code in the image, so it is not practical. Why not add a subclass?
"We could subclass from Array, but that class is specifically optimized by the VM (which assumes, among other things, that it does not have any instance variables). So we use its abstract superclass instead. The discussion below holds equally well." ArrayedCollection subclass: NiledArray [ <shape: #pointer> boundsCheck: index [ ^(index < 1) | (index > (self basicSize)) ] at: index [ ^(self boundsCheck: index) ifTrue: [ nil ] ifFalse: [ super at: index ] ] at: index put: val [ ^(self boundsCheck: index) ifTrue: [ val ] ifFalse: [ super at: index put: val ] ] ]
Much of the machinery of adding a class should be
familiar. We see another declaration like
shape: message. This sets up
to have the same underlying
structure of an
Array object; we’ll delay discussing this
until the chapter on the nuts and bolts of arrays. In any
case, we inherit all of the actual knowledge of how to create
arrays, reference them, and so forth. All that we do is
at:put: messages, call our common
function to validate the array index, and do something special
if the index is not valid. The way that we coded
the bounds check bears a little examination.
Making a first cut at coding the bounds check, you
might have coded the bounds check in NiledArray’s methods
twice (once for
at:, and again for
always, it’s preferable to code things once, and then re-use them.
So we instead add a method for bounds checking
use it for both cases. If we ever wanted to enhance the
bounds checking (perhaps emit an error if the index is < 1 and
answer nil only for indices greater than the array size?), we only
have to change it in one place.
The actual math for calculating whether the bounds have been violated is a little interesting. The first part of the expression returned by the method:
(index < 1) | (index > (self basicSize))
is true if the index is less than 1, otherwise it’s false.
This part of the expression thus becomes the boolean object
true or false. The boolean object then receives the message
|, and the argument
(index > (self basicSize)).
| means “or”—we want to OR together the two possible
out-of-range checks. What is the second part of the expression?
index is our argument, an integer; it receives the
>, and thus will compare itself to the value
self basicSize returns. While we haven’t covered the
underlying structures Smalltalk uses to build arrays, we can
briefly say that the
#basicSize message returns the number
of elements the Array object can contain. So the index is checked
to see if it’s less than 1 (the lowest legal Array index) or
greater than the highest allocated slot in the Array. If it
is either (the
| operator!), the expression is true,
From there it’s downhill; our boolean object, returned by
boundsCheck:, receives the
and a code block which will do the appropriate thing. Why do we
at:put: return val? Well, because that’s what it’s
supposed to do: look at every implementor of
and you’ll find that it returns its second parameter. In general, the
result is discarded; but one could write a program which uses it, so
we’ll write it this way anyway.
Smalltalk also offers an
or: message, which
is different in a subtle way from
|. or: takes
a code block, and only invokes the code block if
it’s necessary to determine the value of the
expression. This is analogous to the guaranteed C
|| evaluates left-to-right only as
far as needed. We could have written the expressions
((index < 1) or: [index > (self basicSize)]).
Since we expect both sides of or: to be
false most of the time, there isn’t much reason to
delay evaluation of either side in this case.