7
\$\begingroup\$

Following up on List<T> implementation for VB6/VBA, I'd like some thoughts about the revisited IsTypeSafe function, below.

The previous version pretty much implemented VB.net's Option Strict setting, by only allowing widening numeric type conversions, and preventing implicit conversion between numeric and string data types. This one is more permissive, to a point where I'm torn between ease of use (VB6 isn't .net!) and correctness (it's twisting Option Strict by allowing implicit conversions of in-range values, i.e. a Long with a value of 32 could be added to a List<Byte>... does that help or hinder usability?).

Given the following private type where TItem is the type name of T in List<T>...

Private Type tList
    Encapsulated As Collection
    TItem As String
    OptionStrict As Boolean
End Type

Private this As tList

The IsTypeSafe method's behavior depends on the values of this.OptionStrict and this.TItem:

  • When this.OptionStrict = True (the default value; can be modified through a public property), then IsTypeSafe only returns True when the type of value matches exactly with that of this.TItem.
  • When this.OptionStrict = False, then IsTypeSafe returns True if value can be legally converted to the type specified by this.TItem; if the value can be converted, it is converted, so as to avoid having a List<Integer> with Byte values...
  • When this.TItem = vbNullString, then IsTypeSafe returns True systematically, and then this.TItem becomes the type name of value.
Public Function IsTypeSafe(value As Variant) As Boolean
'Determines whether a value can be safely added to the List.

    IsTypeSafe = this.TItem = vbNullString Or this.TItem = TypeName(value)
    If IsTypeSafe Or this.OptionStrict Then Exit Function

    Select Case this.TItem

        Case "String":
            IsTypeSafe = IsSafeString(value)
            If IsTypeSafe Then value = CStr(value)

        Case "Boolean"
            IsTypeSafe = IsSafeBoolean(value)
            If IsTypeSafe Then value = CBool(value)

        Case "Byte":
            IsTypeSafe = IsSafeByte(value)
            If IsTypeSafe Then value = CByte(value)

        Case "Date":
            IsTypeSafe = IsSafeDate(value)
            If IsTypeSafe Then value = CDate(value)

        Case "Integer":
            IsTypeSafe = IsSafeInteger(value)
            If IsTypeSafe Then value = CInt(value)

        Case "Long":
            IsTypeSafe = IsSafeLong(value)
            If IsTypeSafe Then value = CLng(value)

        Case "Single"
            IsTypeSafe = IsSafeSingle(value)
            If IsTypeSafe Then value = CSng(value)

        Case "Double":
            IsTypeSafe = IsSafeDouble(value)
            If IsTypeSafe Then value = CDbl(value)

        Case "Currency":
            IsTypeSafe = IsSafeCurrency(value)
            If IsTypeSafe Then value = CCur(value)

        Case Else:
            IsTypeSafe = False

    End Select


ErrHandler:
    'swallow overflow errors:
    If Err.Number = 6 Then
        Err.Clear
        IsTypeSafe = False
    End If

End Function

The method uses a bunch of IsSafe[Type](value As Variant) As Boolean functions that are quite redundant - would there be a clever way to shorten that up?

Private Function IsSafeString(value As Variant) As Boolean
    On Error Resume Next

    Dim result As String
    result = CStr(value) 'assigning value would be an undesirable side-effect here!
    IsSafeString = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeDate(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Date
    result = CDate(value) 'assigning value would be an undesirable side-effect here!
    IsSafeDate = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeByte(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Byte
    result = CByte(value) 'assigning value would be an undesirable side-effect here!
    IsSafeByte = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeBoolean(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Boolean
    result = CBool(value) 'assigning value would be an undesirable side-effect here!
    IsSafeBoolean = (Err.Number = 0)        
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeCurrency(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Currency
    result = CCur(value) 'assigning value would be an undesirable side-effect here!
    IsSafeCurrency = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeInteger(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Integer
    result = CInt(value) 'assigning value would be an undesirable side-effect here!
    IsSafeInteger = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeLong(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Long
    result = CLng(value) 'assigning value would be an undesirable side-effect here!
    IsSafeLong = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeDouble(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Double
    result = CDbl(value) 'assigning value would be an undesirable side-effect here!
    IsSafeDouble = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

Private Function IsSafeSingle(value As Variant) As Boolean
    On Error Resume Next

    Dim result As Single
    result = CSng(value) 'assigning value would be an undesirable side-effect here!
    IsSafeSingle = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function

This IsTypeSafe function is called by this code:

Private Function ValidateItemType(value As Variant)

   If this.ItemTypeName = vbNullString Then this.ItemTypeName = TypeName(value)
   ValidateItemType = IsTypeSafe(value)

End Function

Which is called whenever an item is tentatively added to the "type-safe" list:

Public Sub Add(value As Variant)
'Adds an object to the end of the List.

    If Not ValidateItemType(value) Then RaiseErrorUnsafeType "Add()", TypeName(value)

    this.Encapsulated.Add value

End Sub

This mechanism means the List is a List<Variant> until an item is added to it, and then if a Smurf instance is added, the list becomes a List<Smurf> and will fail to add anything other than a Smurf object.


Here's a little "test" method if you want to see the list in action:

Public Sub TestList()

    Dim lst As New List, tmp As List, i As Long

    On Error GoTo ErrHandler
    lst.Add 1, 2, 3, 4, 8

    Debug.Print lst.ToString & " contains " & lst.Count & " items:"
    GoSub EnumerateToDebugOutput

    lst.OptionStrict = False
    Debug.Print "OptionStrict = " & CStr(lst.OptionStrict)

    Debug.Print "OptionStrict set to FALSE should not allow overflow values:"
    Debug.Print lst.ToString & " can take value 32768? (" & TypeName(32768) & ") : " & lst.IsTypeSafe(32768)
    'lst.Add 32768

    Debug.Print
    Debug.Print "OptionStrict set to FALSE should implicitly convert a value that can be safely converted to the type of the list:"
    On Error Resume Next
        Debug.Print "Adding value CByte(16)..."
        lst.Add CByte(16)
    On Error GoTo 0

    GoSub EnumerateToDebugOutput

    lst.OptionStrict = True
    Debug.Print
    Debug.Print "OptionStrict = " & CStr(lst.OptionStrict)
    Debug.Print "OptionStrict set to TRUE should not allow implicit conversion and throw an error if types mismatch:"

    On Error GoTo ErrHandler
    Debug.Print "Adding value CByte(32)..."
    lst.Add CByte(32)

ErrHandler:
    If Err.Number <> 0 Then
        Debug.Print "Number: " & Err.Number
        Debug.Print "Message: " & Err.Description
        Debug.Print "Source: " & Err.Source
        Debug.Print "Content:"
        GoSub EnumerateToDebugOutput

        Resume Next
    End If

    Exit Sub
EnumerateToDebugOutput:
    For i = 1 To lst.Count
        Debug.Print lst(i) & " (" & TypeName(lst(i)) & ")"
    Next
    Debug.Print

    Return

End Sub

...And the generated output:

TestList
List<Integer> contains 5 items:
1 (Integer)
2 (Integer)
3 (Integer)
4 (Integer)
8 (Integer)

OptionStrict = False
OptionStrict set to FALSE should not allow overflow values:
List<Integer> can take value 32768? (Long) : False

OptionStrict set to FALSE should implicitly convert a value that can be safely converted to the type of the list:
Adding value CByte(16)...
1 (Integer)
2 (Integer)
3 (Integer)
4 (Integer)
8 (Integer)
16 (Integer)


OptionStrict = True
OptionStrict set to TRUE should not allow implicit conversion and throw an error if types mismatch:
Adding value CByte(32)...
Number: -2147220503
Message: Type Mismatch. Expected: 'Integer', 'Byte' was supplied.
Source: List<Integer>.ValidateItemType()
Content:
1 (Integer)
2 (Integer)
3 (Integer)
4 (Integer)
8 (Integer)
16 (Integer)
\$\endgroup\$
  • 1
    \$\begingroup\$ reinventing the wheel a bit => the T is very limited though. Instead of all this work you could have used the vb constants for types... \$\endgroup\$ – user28366 Nov 20 '13 at 13:59
  • \$\begingroup\$ @mehow Good to see you over here! Feel free to post a review, +100 bounty ends in 4 days :) \$\endgroup\$ – Mathieu Guindon Nov 20 '13 at 14:02
  • 1
    \$\begingroup\$ not much to review in here except that if you press F2 in VBE and type vbVarType you get a list which you could possibly enumerate. That would replace the the select case statement and with a bit of error handling you can easily tell if type is safe \$\endgroup\$ – user28366 Nov 20 '13 at 14:09
  • \$\begingroup\$ @mehow now you're making me say if I only knew about these constants before... \$\endgroup\$ – Mathieu Guindon Nov 20 '13 at 14:19
  • 1
    \$\begingroup\$ post your own:) as a tip consider a collection with enumerated constants-much easier to work with \$\endgroup\$ – user28366 Nov 20 '13 at 15:40
7
+100
\$\begingroup\$

This code looks very clean,

It looks like it handles edge cases well.

I especially like the way that if it converts to the specified data type that it does it and doesn't make you convert explicitly. that is very slick!

I would definitely like to see some code that puts it into play.

Not much to review here, but very good code and good syntax.

Edit

I did find something that I might do differently.

Instead of On Error Resume Next I would make it more like a Try Catch

Perhaps something like this

Private Function IsSafeDate(value As Variant) As Boolean
'On Error Resume Next
    On Error GoTo ErrHandler

    Dim result As Date
    result = CDate(value) 'assigning value would be an undesirable side-effect here!
    IsSafeDate = True   
    Exit Function    

ErrHandler:
    IsSafeDate = False
Err.Clear
End Function

I think this is a lot clearer as you are not really skating around the errors, and this way you can add an Exit Sub code too if you need it.

I like the Try Catch Method better myself.

\$\endgroup\$
  • 1
    \$\begingroup\$ I've added some "test code" with generated output :) \$\endgroup\$ – Mathieu Guindon Nov 19 '13 at 18:29
  • \$\begingroup\$ @retailcoder is there a way that I can run all that code somewhere nice and simple like so I can play with it a little bit? at work or on a linux machine? \$\endgroup\$ – Malachi Nov 20 '13 at 17:21
  • \$\begingroup\$ It should work in VBA - try Excel (Alt+F11) :) \$\endgroup\$ – Mathieu Guindon Nov 20 '13 at 17:24
  • \$\begingroup\$ You'll have to simulate this.TItem if you don't want to copy the whole List class from the linked post. That said this linked post has all the code in an easy-copy format in revision 1 (see edit history). \$\endgroup\$ – Mathieu Guindon Nov 20 '13 at 17:32
  • 2
    \$\begingroup\$ So your answer is essentially that the IsSafe[Type] functions are not handling errors in a way that's consistent with the error handling code in the IsTypeSafe function (and the rest of the List class' code, for that matter) - and you're right! I guess I was just lazy... well sir, you've just earned yourself 100 rep points :) \$\endgroup\$ – Mathieu Guindon Nov 23 '13 at 23:14
1
\$\begingroup\$

Just two nitpicks after years. :-)

Regarding the error handling in those IsSafe... procedures:

Err.Clear is superfluous there. It is called automatically when a procedure is exited.

The same goes for an enabled error handler, it will be disabled automatically when exiting a procedure, so On Error GoTo 0 is not necessary there too.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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