Eric Lippert recently talked about some of the fun and exciting things that go on when you use parameterless default properties in VBScript. He notes that the fact that VBScript is always late-bound means that the behavior of code in VBScript is often different than early-bound code in VB6. He goes on to say:
I hear you exclaiming “The fact that a difference in available type information leads to a difference in run-time behaviour violates a basic principle of programming language design! Namely, the principle that late-bound calls have exactly the same semantics as early-bound calls!” […] Indeed, as I’ve mentioned before, VB6 and VBScript violate this principle in several places.
Actually, it’s not just VB6 and VBScript that violate this — it’s VB 2002, VB 2003, etc. too. Although we try to adhere to the principle that “early-bound behavior = late-bound behavior” in general, it’s really a question of tradeoffs. The simplest example is the way that overload resolution works. Let’s say you have two methods:
Sub Foo(o As Object)
Sub Foo(s As String)
Now, if I declare a local variable x that is typed as Object, the compiler will look at the two overloads and say “well, obviously the Object overload is the better match because the type of the variable is Object.” But let’s say instead you deferred the call until run-time and at runtime the value stored in x is of type String. Now, the run-time late binder doesn’t actually know what the declared type of the variable x is. All it knows is that the argument that got passed into the late binding helper is of type String. So, naturally, it’s going to choose the String overload as being the best match because it’s got a String to pass to it. So in this case, early-bound behavior <> late-bound behavior.
This isn’t to say that it wouldn’t be possible to make the two behaviors the same, it’s just a real question as to whether it would be desirable. Late binding is not exactly the fastest thing in the world compared to early bound calls. Deferring work until runtime comes with a cost, and we’d prefer to keep that cost to a minimum. To “fix” this situation would require creating type objects that represent the static type of the expressions we’re late binding over so that the late binder could exactly replicate the early-bound behavior. This adds even more overhead to a process that’s not super lightweight to begin with. And, really, does it matter? Yes, you’re getting a different overload called, but it’s hard to argue that the behavior is really wrong.
(Another example of the late-bound/early-bound split is interfaces. Because interfaces have no identity of their own, the late binder can’t see them unless, again, the compiler were to give it extra static type information. We chose instead to not allow late binding against interfaces.)
In the end, we view “early-bound = late-bound” as a goal rather than a rule. Probably 95% of cases work the same whether you choose to do them early-bound or late-bound, and the other 5% should work is a way that the user would consider “correct.” So, we hope, no one really ever had to notice that static typing and dynamic typing aren’t exactly the same thing…