Maybe it's just me, but Visual Basic's lack of a proper typecast operator seems to put an unreasonable burden on the conscientious programmer, particularly when dealing with object variables.
Here we'll look at type-casting using polymorphic variables created with the 'Implements' keyword. Act I: Creating Polymorphic Variables
Let's say we have a class CTextFile that Implements CFile, where CFile has properties called PathName and FileName. We have the implementation of these methods in CFile itself, and CTextFile
delegates them to a private CFile object. CTextFile is a collection class (the House of Bricks) that implements a collection of lines. This all works fairly well, except that the only way we can get to the FileName
and PathName properties of CTextFile is through a CFile variable. This makes for some cumbersome coding: Dim oTextFile As CTextFile Dim oAliasForSameFile As CFile ' Sinking feeling already...
Set oTextFile = New CTextFile Set oAliasForSameFile = oTextFile ' Do CFile stuff...
oAliasForSameFile.PathName = "c:\somwhere" oAliasForSameFile.FileName = "markyfile.bat"
' Now do CTextFile stuff...
oTextFile.Add "@echo off" oTextFile.Add "echo What a carry on..." ' ...etc What we really want, of course, is to be able to write something like
this: Dim oTextFile As CTextFile Set oTextFile = New CTextFile
((CFile) oTextFile).PathName = "c:\somwhere" ((CFile) oTextFile).FileName = "markyfile.bat"
oTextFile.Add "@echo off" oTextFile.Add "echo Now that's what I call music!" ' ...etc (I'm borrowing a bit of syntax from C here: (CFile) oTextFile
is supposed to convert the CTextFile reference into a CFile reference for the duration of the statement. It's just like VB's CInt et al, except that I'm imagining it works for user-defined types).
Okay, there is a way around this, but is it really the right way? To get around it we need to implement the PathName and FileName
properties again in CTextFile, delegating them to the mandatory CFile interface functions. For example, here's FileName: Private Property Let CFile_FileName(ByVal RHS As String) '
Implements CFile forces this one m_oFile.FileName = RHS ' Delegate to a real CFile ' Extra stuff here if necessary... End Property
Public Property Let FileName(ByVal RHS As String) CFile_FileName = RHS ' Yawn End Property Well, you get the idea. This works very well and I know this makes sense in terms of COM
Interfaces, but it's so tedious. Is this really the only practical way to make early-bound polymorphic variables? Maybe not... An alternative way to do this is
to forget all about those duplicate properties and methods by writing a function that converts a reference to the derived type into a reference to the base type - a typecast, in C/++ parlance. This works well in
practice, and initially it looks like we can even give it the same name as the class:Public Function CFile(ByVal oThisFile As CFile) As CFile Set CFile = oThisFile End Function
...
CFile(oTextFile).FileName = "markh.txt" ' <--- okay! Actually we can't call it the same as the class because it causes a runtime error in files that contain 'Implements CFile', although
it works elsewhere. If it's going to be a global function we could call it CCFile, which ties up nicely with VB's CInt, CStr, etc. It would be nice to attach this conversion function to the class though (CTextFile in
this case), so how about doing it as a property: Public Property Get CFile() As CFile Set CFile = Me End Property and then
oTextFile.FileName = "markh.txt" ' Nope oTextFile.CFile.FileName = "markh.txt" ' Yes! This is good, although once again it can't really be called
CFile (compile-time error this time) so we have to choose something less elegant like CCFile (doesn't look as good when it's a property) or oCFile. You may think we could do this more easily by making the internal
CFile object a public property of CTextFile. This would allow the following, which LOOKS superfically the same: oTextFile.oFile.FileName = "markh.txt" This isn't right though, because it circumvents the
CFile interface altogether, bypassing any modified behaviour we've coded for the CFile_ interface's properties and methods. For example, let's say that CTextFile modifies the behaviour of CFile's FileName
property by shifting the name to upper case. First the right way: Private Property Get CFile_FileName() As String CFile_FileName = UCase(m_oFile.FileName) End Property
Public Property Get FileName() As String FileName = CFile_FileName End Property and, just to remind ourselves of the 'shortcut' way, oTextFile.oFile.FileName = "markh.txt"
Now we exercise this property with the following code: Dim oFile As New CFile oFile.FileName = "markh.txt"
Dim oTextFile As New CTextFile Set oFile = oTextFile
Debug.Print oFile.FileName
Debug.Print oTextFile.FileName Assuming the first version of CTextFile, this code prints MARKH.TXT MARKH.TXT but with the second version, it prints
MARKH.TXT markh.txt To summarise, we now have four different ways of making early-bound polymorphic variables: (1) Multiple Variables
Set oFile = oTextFile oFile.FileName = "markh.txt" ---> Awful. (2) Duplicate All Properties and Methods
oTextFile.FileName = "markh.txt"
---> Looks the best, but lots of code.
(3) Write a Global Conversion Function
CCFile(oTextFile).FileName = "markh.txt" ---> Looks pretty good (like a C++ cast) and we only write it once, but requires a spurious BAS file to hold the conversion routines.
(4) Write a Conversion Property
oTextFile.oCFile.FileName = "markh.txt"
---> A bit ugly, but still quite good.
The unspoken issue, of course, is efficiency. My own view on efficiency is that it's the least important thing until it actually becomes a problem. What I'd be interested to know is how much of the extra function
calls and type conversions gets optimized out by the compiler. In conclusion, I suppose Implements is better than nothing, but for doing inheritance-type things it really IS
more painful than a proper OO language (NOTE: the 'delegation wizard' argument isn't even worth addressing). The real problem seems to be twofold: (1) There is no typecast operator, so I can't do this, which is
permissible in Java:
(2) All Implemented (base) classes in VB are effectively, in one sense, pure virtual base classes - i.e. if you derive from (Implement) a class you have no choice but to override all of its properties and methods.
The act of delegating to a private instance of the base class (and actually putting code in that class - so the virtualness isn't really 'pure') is just laboriously doing by hand what real inheritance gives you for
free. Which is why Java has inheritance AND Implements. The question is, why doesn't Visual Basic? Act II: Promoting Polymorphic Variables
Moving on from our discussion about creating derived classes (eg. deriving CTextFile from CFile), I've discovered that you can't assign an object reference from a base type variable to a variable of a
derived type: Dim oTextFile As CTextFile Dim oAnyFile As CFile ' Can reference any object that Implements CFile … Set oAnyFile = … ' Something …
Set oTextFile = oAnyFile ' <--- NOT ALLOWED! This is fair enough, of course, since oAnyFile doesn't necessarily contain all the required information (it may be a CFile object, a CTextFile
object, or any other object that Implements CFile). However, in practice it's useful to be able to do this kind of assignment - say, to attach an already-initialised CFile object to a CTextFile one. I guess in C++ you'd
write a constructor for CTextFile that accepts a CFile as an argument. One way is to add a new property called CFile to CFile's interface, so every class that Implements CFile has to have a property
called CFile_CFile. This is a 'pure virtual property': it contains no code in the CFile class itself, and it exists only to ensure that each derived class provides a way of assigning a CFile object to itself. By
providing specific versions of CFile_CFile for each class, we can now write code like this: Dim oNewFile As CFile For Each oThisFile In colListOfFiles
Select Case oThisFile.Extension
Case "EXE": Set oNewFile = New CExecFile Case "TXT": Set oNewFile = New CTextFile
Case "NET": Set oNewFile = New CNetFile Case Else: Set oNewFile = oThisFile ' Stick with a generic CFile
End Select
Set oNewFile.CFile = oThisFile <--- The clever bit m_colFiles.Add oNewFile
Next oThisFile
This is good because oNewFile knows what type of object it contains and hence potentially calls a different version of CFile_CFile each time around the loop. The (mandatory) CFile_CFile
property is different for each derived class, and it tacks whatever extra bits area needed after the assignment of the CFile object to the contained object. For example, here is CTextFile's version of CFile_CFile:
Private Property Set CFile_CFile(ByVal RHS As CFile) Set m_oFile = RHS ' <--- This bit's common to all derived classes
' Extra code here to, say, open the file and read ' the text into a private collection of lines. End Property The nice part, of course, is that the property name (CFile) is
the same as the class name of the object being assigned - although devising a convenient way to access this overridden function revisits all the problems described earlier! If I want to add a public CFile property to
the derived class (CTextFile), for example, I can't call it 'CFile' because it conflicts with the 'Implements CFile' statement. (NOTE: I considered CFile_, but although this works, once again poor old VB gets confused
and the property doesn't appear in the code window's drop-down list. Worse than this, if I make a class the Implements CTextFile it just doesn't work because Implements can't handle underscores in property and method
names.) |