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 returnsTrue
when the type ofvalue
matches exactly with that ofthis.TItem
. - When this.OptionStrict = False, then
IsTypeSafe
returnsTrue
ifvalue
can be legally converted to the type specified bythis.TItem
; if the value can be converted, it is converted, so as to avoid having aList<Integer>
withByte
values... - When this.TItem = vbNullString, then
IsTypeSafe
returnsTrue
systematically, and thenthis.TItem
becomes the type name ofvalue
.
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)
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\$