Because I was spoiled with C# and the .NET framework, whenever I have to work with VB6 I feel like something's missing in the language. A little while ago I implemented a List<T> for VB6 (here), and before that I implemented String.Format() and a number of string-helper functions (here). Don't go looking for a StringFormat method in the VB6 language specs, that method is the one I've written.
Today I would have liked to be able to declare a Nullable<bool> in VB6, so I implemented a class that allowed me to do that. I named this class Nullable and it goes like this:
Private Type tNullable
Value As Variant
IsNull As Boolean
TItem As String
End Type
Private this As tNullable
Option Explicit
Private Sub Class_Initialize()
this.IsNull = True
End Sub
Now before I go any further I have to mention that I have used "procedure attributes" in the Value property, making it the type's default member:
Public Property Get Value() As Variant
'default member
Value = this.Value
End Property
Public Property Let Value(val As Variant) 'damn case-insensitivity...
'default member
If ValidateItemType(val) Then
this.Value = val
this.IsNull = False
End If
End Property
Public Property Set Value(val As Variant)
'used for assigning Nothing.
'Must be explicitly specified (e.g. Set MyNullable.Value = Nothing; Set MyNullable = Nothing will not call this setter)
Dim emptyValue As Variant
If val Is Nothing Then
this.IsNull = True
this.Value = emptyValue
Else
Err.Raise vbObjectError + 911, "Nullable<T>", "Invalid argument."
End If
End Property
The ValidateItemType private method determines whether the type of a value is "ok" to be assigned as the instance's Value:
Private Function ValidateItemType(val As Variant) As Boolean
Dim result As Boolean
If Not IsObject(val) Then
If this.TItem = vbNullString Then this.TItem = TypeName(val)
result = IsTypeSafe(val)
If Not result Then Err.Raise vbObjectError + 911, "Nullable<T>", StringFormat("Type mismatch. Expected '{0}', '{1}' was supplied.", this.TItem, TypeName(val))
Else
Err.Raise vbObjectError + 911, "Nullable<T>", "Value type required. T cannot be an object."
result = False
End If
ValidateItemType = result
End Function
Private Function IsTypeSafe(val As Variant) As Boolean
IsTypeSafe = this.TItem = vbNullString Or this.TItem = TypeName(val)
End Function
That mechanism is borrowed from the List<T> implementation I wrote before, and proved to be working fine. Shortly put, an instance of the Nullable class is a Nullable<Variant> until it's assigned a value - if that value is a Integer then the instance becomes a Nullable<Integer> and remains of that type - so the Value can only be assigned an Integer. The mechanism can be refined as shown here, to be more flexible (i.e. more VB-like), but for now I only wanted something that works.
The remaining members are HasValue() and ToString():
Public Property Get HasValue() As Boolean
HasValue = Not this.IsNull
End Property
Public Function ToString() As String
ToString = StringFormat("Nullable<{0}>", IIf(this.TItem = vbNullString, "Variant", this.TItem))
End Function
Usage
Here's some test code that shows how the class can be used:
Public Sub TestNullable()
Dim n As New Nullable
Debug.Print StringFormat("{0} | HasValue: {1} | Value: {2}", n.ToString, n.HasValue, n)
n = False
Debug.Print StringFormat("{0} | HasValue: {1} | Value: {2}", n.ToString, n.HasValue, n)
n = True
Debug.Print StringFormat("{0} | HasValue: {1} | Value: {2}", n.ToString, n.HasValue, n)
Set n.Value = Nothing
Debug.Print StringFormat("{0} | HasValue: {1} | Value: {2}", n.ToString, n.HasValue, n)
On Error Resume Next
n = "test" 'expected "Type mismatch. Expected 'T', 'x' was supplied." error
Debug.Print Err.Description
n = New List 'expected "Value type required. T cannot be an object." error
Debug.Print Err.Description
On Error GoTo 0
End Sub
When called from the immediate pane, this method outputs the following:
TestNullable
Nullable<Variant> | HasValue: False | Value:
Nullable<Boolean> | HasValue: True | Value: False
Nullable<Boolean> | HasValue: True | Value: True
Nullable<Boolean> | HasValue: False | Value:
Type mismatch. Expected 'Boolean', 'String' was supplied.
Value type required. T cannot be an object.
Did I miss anything or this is a perfectly acceptable implementation?
One thing did surprise me: if I do Set n.Value = Nothing, the instance remains a Nullable<Boolean> as expected. However if I do Set n = Nothing, not only Debug.Print n Is Nothing will print False, the instance gets reset to a Nullable<Variant> and ...the setter (Public Property Set Value) does not get called - as a result, I wonder if I have written a class with a built-in bug that makes it un-Nothing-able?
Bonus
After further testing, I have found that this:
Dim n As New Nullable
Set n = Nothing
Debug.Print n Is Nothing
Outputs False. However this:
Dim n As Nullable
Set n = New Nullable
Set n = Nothing
Debug.Print n Is Nothing
Outputs True (both snippets never hit a breakpoint in the Set accessor).
All these years I thought Dim n As New SomeClass was the exact same thing as doing Dim n As SomeClass followed by Set n = New SomeClass. Did I miss the memo?
UPDATE
Don't do this at home.
After a thorough review, it appears an Emptyable<T> in VB6 is absolutely moot. All the class is buying, is a HasValue member, which VB6 already takes care of, with its IsEmpty() function.
Basically, instead of having a Nullable<Boolean> and doing MyNullable.HasValue, just declare a Boolean and assign it to Empty, and verify "emptiness" with IsEmpty(MyBoolean).
Dim n As New SomeClassis the exact same thing as doingDim n As SomeClassfollowed bySet n = New SomeClass. Unfortunately both are the same asDim n As SomeClassfollowed byDebug.Print (n)\$\endgroup\$MyBooleanaVariantrather than aBoolean. \$\endgroup\$