11
\$\begingroup\$

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 aList<T> for VB6 (here), and before that I implementedString.Format() and a number of string-helper functions (here). Don't go looking for aStringFormat method in the VB6 language specs, that method is the one I've written.

Today I would have liked to be able to declare aNullable<bool> in VB6, so I implemented a class that allowed me to do that. I named this classNullable and it goes like this:

Private Type tNullable    Value As Variant    IsNull As Boolean    TItem As StringEnd TypePrivate this As tNullableOption ExplicitPrivate Sub Class_Initialize()    this.IsNull = TrueEnd Sub

Now before I go any further I have to mention that I have used "procedure attributes" in theValue property, making it the type'sdefault member:

Public Property Get Value() As Variant'default member    Value = this.ValueEnd PropertyPublic Property Let Value(val As Variant) 'damn case-insensitivity...'default member    If ValidateItemType(val) Then        this.Value = val        this.IsNull = False    End IfEnd PropertyPublic 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

TheValidateItemType private method determines whether the type of a value is "ok" to be assigned as the instance'sValue:

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 = resultEnd FunctionPrivate Function IsTypeSafe(val As Variant) As Boolean    IsTypeSafe = this.TItem = vbNullString Or this.TItem = TypeName(val)End Function

That mechanism is borrowed from theList<T> implementation I wrote before, and proved to be working fine. Shortly put, an instance of theNullable class is aNullable<Variant> until it's assigned a value - if that value is aInteger then the instance becomes aNullable<Integer> and remains of that type - so theValue can only be assigned anInteger.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 areHasValue() andToString():

Public Property Get HasValue() As Boolean    HasValue = Not this.IsNullEnd PropertyPublic 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 0End Sub

When called from theimmediate pane, this method outputs the following:

TestNullableNullable<Variant> | HasValue: False | Value: Nullable<Boolean> | HasValue: True | Value: FalseNullable<Boolean> | HasValue: True | Value: TrueNullable<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 doSet n.Value = Nothing, the instance remains aNullable<Boolean> as expected. However if I doSet n = Nothing, not onlyDebug.Print n Is Nothing will printFalse, the instance gets reset to aNullable<Variant> and ...the setter (Public Property Set Value) doesnot 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 NullableSet n = NothingDebug.Print n Is Nothing

OutputsFalse. However this:

Dim n As NullableSet n = New NullableSet n = NothingDebug.Print n Is Nothing

OutputsTrue (both snippets never hit a breakpoint in theSet accessor).

All these years I thoughtDim n As New SomeClass was the exact same thing as doingDim n As SomeClass followed bySet n = New SomeClass. Did I miss the memo?


UPDATE

Don't do this at home.

After a thorough review, it appears anEmptyable<T> in VB6 is absolutely moot. All the class is buying, is aHasValue member, which VB6 already takes care of, with itsIsEmpty() function.

Basically, instead of having aNullable<Boolean> and doingMyNullable.HasValue, just declare aBoolean and assign it toEmpty, and verify "emptiness" withIsEmpty(MyBoolean).

askedFeb 14, 2014 at 0:49
Mathieu Guindon's user avatar
\$\endgroup\$
6
  • 1
    \$\begingroup\$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.\$\endgroup\$CommentedFeb 14, 2014 at 1:31
  • \$\begingroup\$@Comintern I've edited with my latest findings (although that's starting to be more on StackOverflow's grounds)\$\endgroup\$CommentedFeb 14, 2014 at 1:33
  • \$\begingroup\$I thought this deserved a longer-winded explanation. See below. :-)\$\endgroup\$CommentedFeb 14, 2014 at 2:11
  • 1
    \$\begingroup\$Dim n As New SomeClassis 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\$CommentedFeb 14, 2014 at 2:13
  • \$\begingroup\$I think that what you suggest in your "UPDATE" section only works if you makeMyBoolean aVariant rather than aBoolean.\$\endgroup\$CommentedOct 15, 2015 at 16:50

2 Answers2

10
\$\begingroup\$

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 totest 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 runsSet 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 hoodhere.

answeredFeb 14, 2014 at 2:10
Comintern's user avatar
\$\endgroup\$
6
  • \$\begingroup\$+1 I was just coming to the same conclusions! Wow Idid miss the memo... or .NET has successfully "corrupted" my VB6 mind!\$\endgroup\$CommentedFeb 14, 2014 at 2:15
  • 1
    \$\begingroup\$More accurate to say that .NET "uncorrupted" VB6.\$\endgroup\$CommentedFeb 14, 2014 at 2:16
  • 1
    \$\begingroup\$You seem to know your VB6.. I'd be curious to read your input on myList<T> class :)\$\endgroup\$CommentedFeb 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\$CommentedFeb 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 inThe 2nd Monitor!\$\endgroup\$CommentedFeb 14, 2014 at 3:01
7
\$\begingroup\$

Adding to @Comintern's excellent answer, the private type doesn't need anIsNull member, since the class only accepts value types, the correct semantics for "null" values isvbEmpty.

TheSet accessor is therefore not only wrong, it's also ambiguous - not only in attempting to assignNothing to a value type, but also becauseValue being the default member, it's not immediately obvious what this does:

Set MyNullable = Nothing

The solution is simple: get rid of theSet accessor altogether:

Private Type tNullable    Value As Variant    TItem As StringEnd TypePrivate this As tNullableOption ExplicitPublic Property Get Value() As Variant    Value = this.ValueEnd PropertyPublic Property Let Value(val As Variant)    If ValidateItemType(val) Then this.Value = valEnd Property

HasValue can then be rewritten like this:

Public Property Get HasValue() As Boolean    HasValue = Not IsEmpty(this.Value)End Property

AndIsTypeSafe 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 Nullablen = False 'n.ToString returns "Nullable<Boolean>"; n.HasValue returns Truen = Empty 'n.ToString returns "Nullable<Boolean>"; n.HasValue returns FalseSet n = Nothing 'n.ToString returns "Nullable<Variant>"; n.HasValue returns False

And now the bad naming for the class becomes more than just obvious.

TheToString 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 toEmptyable... regardless of how ugly that is: VB6 just isn't .NET.

answeredFeb 14, 2014 at 2:58
Mathieu Guindon's user avatar
\$\endgroup\$
2
  • 1
    \$\begingroup\$Value is not always the default member though. It's theruntime that decides which type is themost appropriate. Other than that a definite +1.\$\endgroup\$CommentedFeb 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\$CommentedFeb 14, 2014 at 12:14

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.