Generic objects in twinBASIC
One of the exciting new features in the twinBASIC language is its support for generic objects. In a nutshell, generics allow you to pass types as parameters in addition to simply passing values.
This is particularly useful if you want to define a common data structure in the form of a class. The class won't care what type(s) you pass to it, so long as you are consistent with the types for a given instance of the class.
Generic programming can be a bit difficult to wrap your head around. Rather than spend this article discussing it, I'll let you follow that link and go down the Wikipedia rabbit hole on your own.
What I'm interested in here is showing you a quick practical example of a generic class in twinBASIC. In doing so, I hope you'll see how powerful this feature can be given the right circumstances.
The Right Circumstances
The Dictionary object in the Microsoft Scripting Runtime is a hash table structure that lets you store data as key-value pairs. The object is a bit unsafe, though, as the key and value are both Variant types. Because of this, you can use absolutely anything as both key and value. You can even mix types within the same instance of the class.
This is madness. Let's put a stop to it.
The sample below is not a full implementation of a generic dictionary class; I'll leave that as an exercise for the reader (or maybe the subject of a future article). Rather, it's just enough to demonstrate the syntax of generic objects in twinBASIC so that you can do your own experimenting.
Class Dict (Of TKey, TValue) Dim mDict As Scripting.Dictionary Public Sub New() Set mDict = New Scripting.Dictionary End Sub Public Sub Add(Key As TKey, Value As TValue) mDict.Add(Key, Value) End Sub Public Property Get Item(Key As TKey) As TValue Return mDict.Item(Key) End Property End Class
The new syntax that makes this whole thing work when defining a generic class is the
Class <ClassName> (Of <TypeA>, <TypeB>, <TypeC>) declaration line. Notice that the types that we define by name in that line can then be referenced elsewhere within the class when defining properties and methods:
A common naming convention is to use the single capital letter 'T' to represent the type if there is only a single type accepted by the generic class. If there are multiple types (as in our example above), the convention is to use a leading capital 'T' followed by a descriptive name for the type.
In this case, the first type variable,
TKey, refers to the "type of the key" our dictionary will accept. The second variable,
TValue, refers to the "type of the value" our dictionary will accept.
Using the class
Let's start with a simple example. We will create a dictionary that uses a long integer for a key and a string as the value.
Sub TestDict() Dim Lookup As Dict(Of Long, String) Set Lookup = New Dict(Of Long, String) Lookup.Add 1, "A" Lookup.Add 5, "E" Lookup.Add 9, "I" Debug.Print Lookup.Item(5) End Sub
Notice that we need to provide the types both when declaring the object (the
Dim Lookup line) and when creating an instance of the object (the
Set Lookup line).
Reusing the class
This is where we get to see why we would write generics in the first place. Let's say we want to create a different kind of lookup table. This one will be keyed using a string. The string key will be used to retrieve an oVehicle object.
Sub TestVehicleDict() Dim CarLookup As New Dict(Of String, oVehicle) CarLookup.Add "Bullitt", NewVehicleObject("Ford", "Mustang", 1968) CarLookup.Add "ECTO-1", NewVehicleObject("Cadillac", "Miller-Meteor", 1950) Debug.Print CarLookup.Item("Bullitt").Year Debug.Print CarLookup.Item("ECTO-1").Model End Sub
In this example, we used the
As New syntax to automatically generate an instance of the variable the first time it is referenced. This works just as well with our generic object as the
Set approach we took in the previous example.
And just to prove that the compiler is actually enforcing the type safety, let's try calling
CarLookup.Add() with the same arguments we used to call
The compiler doesn't complain about using
1 as the key, since twinBASIC can implicitly coerce that into the String the CarLookup object expects. However, it is not able to coerce
"A" into an oVehicle object, and it makes sure we know it.
Avoiding subtle bugs with the added type safety
Using a fully-implemented version of this generic dictionary class in place of the anything-goes default version found in the scripting runtime can help us avoid subtle bugs like this one:
In the above article, I detailed a bug that can appear if you use a textbox control as the key to a standard dictionary. As a developer, you may assume that the default .Value property of the control is being passed to the dictionary. But in fact, it's passing a reference to the textbox object itself and not the textbox's value. This goes back to the fact that the Key argument is a Variant, so literally anything you pass will be used as the key.
Here's the problematic code from that article:
Set EmployeePhoneNums = CreateObject("Scripting.Dictionary") Me.tbLastName.Value = "Jones" EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234" Me.tbLastName.Value = "Smith" EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-6789"
To avoid the bug completely, we could simply change the first line from...
Set EmployeePhoneNums = CreateObject("Scripting.Dictionary")
Set EmployeePhoneNums = New Dict(Of String, String)
Now, when the following line gets called...
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234"
...twinBASIC will see that the key must be a String. It will then coerce the textbox object (Me.tbLastName) into a String by fetching its default property,
.Value, and using that as the key. Presto change-o, the bug is gone!
Full example with semantic highlighting
Here's a screenshot of the full twinBASIC code with all that juicy semantic highlighting goodness: