Revisited IsTypeSafe method implementation for “type-safe” List

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)

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)

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

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

• 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... – user28366 Nov 20 '13 at 13:59
• @mehow Good to see you over here! Feel free to post a review, +100 bounty ends in 4 days :) – Mathieu Guindon Nov 20 '13 at 14:02
• 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 – user28366 Nov 20 '13 at 14:09
• @mehow now you're making me say if I only knew about these constants before... – Mathieu Guindon Nov 20 '13 at 14:19
• post your own:) as a tip consider a collection with enumerated constants-much easier to work with – user28366 Nov 20 '13 at 15:40

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.

• I've added some "test code" with generated output :) – Mathieu Guindon Nov 19 '13 at 18:29
• @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? – Malachi Nov 20 '13 at 17:21
• It should work in VBA - try Excel (Alt+F11) :) – Mathieu Guindon Nov 20 '13 at 17:24
• 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). – Mathieu Guindon Nov 20 '13 at 17:32
• 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 :) – Mathieu Guindon Nov 23 '13 at 23:14

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.