Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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).

share|improve this question
1  
VB6 has the nasty habit of instantiating a new object if a method is called on something that is Nothing. I'm curious to see your test function for the Set n = Nothing bit. I suspect the surprising behavior is there, not in the class itself. –  Comintern Feb 14 at 1:31
    
@Comintern I've edited with my latest findings (although that's starting to be more on StackOverflow's grounds) –  Mat's Mug Feb 14 at 1:33
    
I thought this deserved a longer-winded explanation. See below. :-) –  Comintern Feb 14 at 2:11
1  
Dim n As New SomeClass is the exact same thing as doing Dim n As SomeClass followed by Set n = New SomeClass. Unfortunately both are the same as Dim n As SomeClass followed by Debug.Print (n) –  Comintern Feb 14 at 2:13

2 Answers 2

up vote 10 down vote accepted

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.

share|improve this answer
    
+1 I was just coming to the same conclusions! Wow I did miss the memo... or .NET has successfully "corrupted" my VB6 mind! –  Mat's Mug Feb 14 at 2:15
1  
More accurate to say that .NET "uncorrupted" VB6. –  Comintern Feb 14 at 2:16
1  
You seem to know your VB6.. I'd be curious to read your input on my List<T> class :) –  Mat's Mug Feb 14 at 2:21
    
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. –  Comintern Feb 14 at 2:26
1  
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! –  Mat's Mug Feb 14 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.

share|improve this answer
1  
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. –  vba4all Feb 14 at 8:07
    
Thanks! I used a procedure attribute to make sure :) –  Mat's Mug Feb 14 at 12:12
    
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 :) –  vba4all Feb 14 at 12:14

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.