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)
.
2 Answers 2
I think the itself class might be mis-named, because it is really 'Empty-able' not Nullable or 'Nothing-able'.
You have to keep in mind that Empty, Null, and Nothing are very different concepts in VB6. Setting and object to Nothing is basically just syntactic sugar for releasing the pointer to the Object. This is the same as asking for ObjPtr() to return Null for that instance (although there is no way to test this in VB6 - see the code and explanation below).
Null is actually better to conceptualize in VB6 as a type rather than an uninitialized variable, as the code below demonstrates:
Dim temp As Variant
'This will return "True"
Debug.Print (temp = Empty)
'This will return "False"
Debug.Print (IsNull(temp))
temp = Null
'This will return "True"
Debug.Print (IsNull(temp))
'This will return "Null"
Debug.Print (TypeName(temp))
This brings me to the explanation of why your class should really be referred to as 'Empty-able'. A Variant is best thought of as an object with 2 properties - a type and a pointer. If it is uninitialized, it basically has a pointer to Nothing and a type of Empty. But is isn't Null, because the Variant itself still exists with its default "properties".
However if I do Set n = Nothing, not only Debug.Print n Is Nothing will print False, the instance gets reset to a Nullable and ...the setter (Public Property Set Value) does not get called
This is because of VB6's obnoxious default behavior when you use a reference to an object that was set to nothing. It "helpfully" creates a new object for you as can be verified by the code below - before the second call to ObjPtr(temp), it implicitly runs Set temp = New Test
. You should be able to verify this with a Debug.Print in Class_Initialize().
Private Sub Testing()
Dim temp As New Test
Debug.Print (ObjPtr(temp))
Set temp = Nothing
'The code below instantiates a new Test object, because it is used after being released.
Debug.Print (ObjPtr(temp))
End Sub
VB6 treats setting an Object equal to Nothing as a special case, so it never calls the Property Set. What is it basically doing is: AddressOf(n) = AddressOf(Nothing)
.
EDIT: Excellent explanation of how Variants work under the hood here.
-
\$\begingroup\$ +1 I was just coming to the same conclusions! Wow I did miss the memo... or .NET has successfully "corrupted" my VB6 mind! \$\endgroup\$Mathieu Guindon– Mathieu Guindon2014年02月14日 02:15:07 +00:00Commented Feb 14, 2014 at 2:15
-
1\$\begingroup\$ More accurate to say that .NET "uncorrupted" VB6. \$\endgroup\$Comintern– Comintern2014年02月14日 02:16:31 +00:00Commented Feb 14, 2014 at 2:16
-
1\$\begingroup\$ You seem to know your VB6.. I'd be curious to read your input on my
List<T>
class :) \$\endgroup\$Mathieu Guindon– Mathieu Guindon2014年02月14日 02:21:04 +00:00Commented Feb 14, 2014 at 2:21 -
\$\begingroup\$ It's where I cut my teeth in programming. I'll take a look at it tonight. You can do some wild stuff with VB6 like in-line assembly and opening files as memory mapped arrays. The wheels really came off when they introduced AddressOf, it lets you break out of the walls of the runtime. \$\endgroup\$Comintern– Comintern2014年02月14日 02:26:42 +00:00Commented Feb 14, 2014 at 2:26
-
1\$\begingroup\$ Building on your answer I added my own, feel free to comment ;) and PS - feel free to join us (CR regulars) anytime in The 2nd Monitor! \$\endgroup\$Mathieu Guindon– Mathieu Guindon2014年02月14日 03:01:30 +00:00Commented Feb 14, 2014 at 3:01
Adding to @Comintern's excellent answer, the private type doesn't need an IsNull
member, since the class only accepts value types, the correct semantics for "null" values is vbEmpty
.
The Set
accessor is therefore not only wrong, it's also ambiguous - not only in attempting to assign Nothing
to a value type, but also because Value
being the default member, it's not immediately obvious what this does:
Set MyNullable = Nothing
The solution is simple: get rid of the Set
accessor altogether:
Private Type tNullable
Value As Variant
TItem As String
End Type
Private this As tNullable
Option Explicit
Public Property Get Value() As Variant
Value = this.Value
End Property
Public Property Let Value(val As Variant)
If ValidateItemType(val) Then this.Value = val
End Property
HasValue
can then be rewritten like this:
Public Property Get HasValue() As Boolean
HasValue = Not IsEmpty(this.Value)
End Property
And IsTypeSafe
should accept type name "Empty":
Private Function IsTypeSafe(val As Variant) As Boolean
IsTypeSafe = this.TItem = vbNullString _
Or this.TItem = TypeName(val) _
Or TypeName(val) = "Empty"
End Function
As a result we can now do this:
Dim n As New Nullable
n = False 'n.ToString returns "Nullable<Boolean>"; n.HasValue returns True
n = Empty 'n.ToString returns "Nullable<Boolean>"; n.HasValue returns False
Set n = Nothing 'n.ToString returns "Nullable<Variant>"; n.HasValue returns False
And now the bad naming for the class becomes more than just obvious.
The ToString
method should therefore be tweaked to no longer hard-code the type's name:
Public Function ToString() As String
ToString = StringFormat("{0}<{1}>", TypeName(Me), IIf(this.TItem = vbNullString, "Variant", this.TItem))
End Function
And the class should be renamed to Emptyable
... regardless of how ugly that is: VB6 just isn't .NET.
-
1\$\begingroup\$ Value is not always the default member though. It's the runtime that decides which type is the most appropriate. Other than that a definite +1. \$\endgroup\$user28366– user283662014年02月14日 08:07:40 +00:00Commented Feb 14, 2014 at 8:07
-
\$\begingroup\$ I see i see :) only recently I have found out that Value is not a default member if there is a _Default property defined in a class. It is the runtime that decides what the default property is going to be. You explicitly use
.Value
so it should not affect anything which is GOOD but I thought it was worth mentioning :) \$\endgroup\$user28366– user283662014年02月14日 12:14:35 +00:00Commented Feb 14, 2014 at 12:14
Dim n As New SomeClass
is the exact same thing as doingDim n As SomeClass
followed bySet n = New SomeClass
. Unfortunately both are the same asDim n As SomeClass
followed byDebug.Print (n)
\$\endgroup\$MyBoolean
aVariant
rather than aBoolean
. \$\endgroup\$