Your question regarding late binding (a.k.a. dynamic binding) of self in Smalltalk (or this in Java parlance) was how it comes that the compiler cannot know what class self refers to.
The answer is that, in any object-oriented languages that support late or dynamic binding (which is most, if not all, OO languages) you don't know what self (or this) will be bound to until you start sending messages. Let's illustrate this with a simple pseudocode example which you can translate to whatever is your favourite OO language.
class Foo :
method m :
print "Hello, I am foo"
class Bar subclass-of Foo :
method m :
print "Hello, I am bar"
So suppose we have a class Foo
implementing some method m
, and some other class Bar
, of which Foo
is the superclass, which also implements a method m
but with a different behaviour. Now suppose, we have a variable z
of type Foo
to which I first assign an object instance of type Foo
, and then a bit later an object instance of type Bar
. So far, no problem: since Bar
is a subclass of Zork
its instances can be used anywhere instances of type Zork
are expected because of the substitutability principle. In particular, you can send a message named m
to either Zork
objects or Bar
objects.
z = new Foo() % z now contains an object of type Foo
z.m() % This will print "Hello, I am foo"
z = new Bar() % z now contains an object of type Bar (subclass of Foo)
z.m() % This will print "Hello, I am bar"
However, as is illustrated by the instructions above, the result of executing m()
will depend on what type of object is actually stored in the variable z
, and you only know this at runtime. Upon the first call to m()
above, z
contains an instance of class Foo
, so the method m()
implemented on class Foo
will be called. The second call to m()
above behaves differently, since in the mean time z
has been reassigned and now contains an instance of class Bar
, so the method m()
implemented on class Bar
will be called.
The problem is that you only know at runtime what type of object instance z
will actually contain. So the choice of which version of the method m()
to be executed (the one of class Foo
or the one of class Bar
) is deferred until runtime, depending on the actual type of the receiver object (in our example: depending on the type/class of the object contained in the variable z
).
I hope this explains the principe of late or dynamic binding a bit better and why you cannot determine this at compile time but only at runtime.
You may also want to read this related discussion which exemplifies and discusses the difference between static and dynamic binding in Java: Static Vs. Dynamic Binding in Java