HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Dictionary<TKey, TValue> Implementation

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
dictionaryimplementationtkeytvalue

Problem

This class encapsulates a List (see List implementation here, and KeyValuePair implementation here) and exposes a richer set of members than the typical Scripting.Dictionary, ..not to mention the anemic Collection class.

The class enforces some "type safety", in the sense that if you add a KeyValuePair, then you'll only be allowed to add KeyValuePair instances, the object will raise an error if you try adding, say, a KeyValuePair, or anything that's not a KeyValuePair.

The OptionStrict property enables allowing more flexibility and adding a KeyValuePair to a Dictionary, for example (but not the opposite).

As with the List implementation, this class uses procedure attributes (not shown) which make the Item property the default property (so myDictionary(i) returns the value at index i), and the NewEnum property enables iterating all values with a For Each loop construct.

```
Private Type tDictionary
Encapsulated As List
TKey As String
IsRefTypeKey As Boolean
TValue As String
IsRefTypeValue As Boolean
End Type

Private Enum DictionaryErrors
TypeMismatchUnsafeType = vbObjectError + 1001
End Enum

Private this As tDictionary
Option Explicit

Private Sub Class_Initialize()
Set this.Encapsulated = New List
this.Encapsulated.OptionStrict = True
End Sub

Private Sub Class_Terminate()
Set this.Encapsulated = Nothing
End Sub

Public Property Get Count() As Long
Count = this.Encapsulated.Count
End Property

Public Property Get Keys() As List
Dim result As New List
Dim kvp As KeyValuePair

result.OptionStrict = this.Encapsulated.OptionStrict

For Each kvp In this.Encapsulated
result.Add kvp.Key
Next

Set Keys = result
End Property

Public Property Get Values() As List
Dim result As New List
Dim kvp As KeyValuePair

result.OptionStrict = this.Encapsulated.OptionStrict

For Each kvp In this.Encapsulated
result.Add kvp.value
Next

Set Value

Solution

Although CallByName doesn't seem to have a return value (from the parameter tooltip), it does. This means if IsSafeKeyXxxxxx methods were Public you could use CallByName instead of the Select..Case block.

However exposing all these methods through your Dictionary interface would be rather ugly. How about extracting all these small methods into their own TypeValidator class?

Private Type tValueTypeValidator
    TValue As String
    OptionStrict As Boolean
End Type

Private this As tValueTypeValidator
Option Explicit

Public Property Get TValue() As String
    TValue = this.TValue
End Property

Public Property Let TValue(ByVal value As String)
    this.TValue = value
End Property

Public Property Get OptionStrict() As Boolean
    OptionStrict = this.OptionStrict
End Property

Public Property Let OptionStrict(ByVal value As Boolean)
    this.OptionStrict = value
End Property

Public Function ToString() As String
    ToString = TypeName(Me) & ""
End Function

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

    IsSafeBoolean = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeBoolean Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CBool(value)
    IsSafeBoolean = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeByte = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeByte Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CByte(value)
    IsSafeByte = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeCurrency = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeCurrency Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CCur(value)
    IsSafeCurrency = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeDate = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeDate Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CDate(value)
    IsSafeDate = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeDouble = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeDouble Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CDbl(value)
    IsSafeDouble = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeInteger = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeInteger Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CInt(value)
    IsSafeInteger = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeLong = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeLong Or OptionStrict Then Exit Function

    Dim result As Boolean
    result = CLng(value)
    IsSafeLong = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeSingle = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeSingle Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CSng(value)
    IsSafeSingle = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeString = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeString Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CStr(value)
    IsSafeString = (Err.Number = 0)
    Err.Clear

    On Error GoTo 0
End Function


And since that code was copy-pasted from an existing List class in the first place, there's already a case for reusing that TypeValidator class.

That change would require the ValidateItemType method to be modified as such:

```
Private Function ValidateItemType(kvp As KeyValuePair, Optional ThrowOnUnsafeType As Boolean = False) As Boolean

If this.TKey = vbNullString And this.TValue = vbNullString Then
this.TKey = TypeName(kvp.key)
this.IsRefTypeKey = IsObject(kvp.key)
this.TValue = TypeName(kvp.value)
this.IsRefTypeValue = IsObject(kvp.value)
this.KeyValidator.TValue = this.TKey '<<< here
this.ValueValidator.TValue = this.TValue '<<< here
End If

ValidateItemType = IsTypeSafe(kvp)

Code Snippets

Private Type tValueTypeValidator
    TValue As String
    OptionStrict As Boolean
End Type

Private this As tValueTypeValidator
Option Explicit

Public Property Get TValue() As String
    TValue = this.TValue
End Property

Public Property Let TValue(ByVal value As String)
    this.TValue = value
End Property

Public Property Get OptionStrict() As Boolean
    OptionStrict = this.OptionStrict
End Property

Public Property Let OptionStrict(ByVal value As Boolean)
    this.OptionStrict = value
End Property

Public Function ToString() As String
    ToString = TypeName(Me) & "<" & this.TValue & ">"
End Function

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

    IsSafeBoolean = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeBoolean Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CBool(value)
    IsSafeBoolean = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeByte = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeByte Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CByte(value)
    IsSafeByte = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeCurrency = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeCurrency Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CCur(value)
    IsSafeCurrency = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeDate = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeDate Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CDate(value)
    IsSafeDate = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeDouble = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeDouble Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CDbl(value)
    IsSafeDouble = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeInteger = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeInteger Or this.OptionStrict Then Exit Function

    Dim result As Boolean
    result = CInt(value)
    IsSafeInteger = (Err.Number = 0)

    Err.Clear
    On Error GoTo 0
End Function

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

    IsSafeLong = (this.TValue = vbNullString Or this.TValue = TypeName(value))
    If IsSafeLong Or OptionStrict Then Exit Functi
Private Function ValidateItemType(kvp As KeyValuePair, Optional ThrowOnUnsafeType As Boolean = False) As Boolean

    If this.TKey = vbNullString And this.TValue = vbNullString Then
        this.TKey = TypeName(kvp.key)
        this.IsRefTypeKey = IsObject(kvp.key)
        this.TValue = TypeName(kvp.value)
        this.IsRefTypeValue = IsObject(kvp.value)
        this.KeyValidator.TValue = this.TKey '<<< here
        this.ValueValidator.TValue = this.TValue '<<< here
    End If

    ValidateItemType = IsTypeSafe(kvp)

    If ThrowOnUnsafeType And Not ValidateItemType Then RaiseErrorUnsafeType "ValidateItemType()", kvp.ToString

End Function
Public Property Let OptionStrict(value As Boolean)
    this.Encapsulated.OptionStrict = value
    this.KeyValidator.OptionStrict = value ' <<< here
    this.ValueValidator.OptionStrict = value ' <<< here
End Property
Private Sub Class_Initialize()
    Set this.Encapsulated = New List
    Set this.KeyValidator = New TypeValidator ' <<< here
    Set this.ValueValidator = New TypeValidator ' <<< here
    this.Encapsulated.OptionStrict = True
    this.KeyValidator.OptionStrict = True ' <<< here
    this.ValueValidator.OptionStrict = True ' <<< here
End Sub
Private Type tDictionary
    Encapsulated As List
    TKey As String
    IsRefTypeKey As Boolean
    TValue As String
    IsRefTypeValue As Boolean
    KeyValidator As TypeValidator ' <<< here
    ValueValidator As TypeValidator ' <<< here
End Type

Context

StackExchange Code Review Q#45666, answer score: 5

Revisions (0)

No revisions yet.