Search code examples
vb.netgenericsdictionaryicomparableicomparer

How do you use a custom type for a dictionary key?


I have a custom class which uses generics.

I need to use this class as the key of a dictionary as shown in the code example below:

I am able to hit the overridden Object.GetHashCode method, but i'm not sure how to proceed from there. Please help, Thanks.

    Module Module2
    Dim myStore As New Dictionary(Of Pair(Of Long, Integer), String)

    Public Function ContainsItem(id As Long, code As Integer) As Boolean
        Return myStore.ContainsKey(New Pair(Of Long, Integer)(id, code))
    End Function

    Public Class Pair(Of T1, T2)
        Implements IEquatable(Of Pair(Of T1, T2))

        Private v1 As T1
        Private v2 As T2

        Public Sub New(ByVal v1 As T1, ByVal v2 As T2)
            Me.v1 = v1
            Me.v2 = v2
        End Sub

        Public Function first() As T1
            Return v1
        End Function

        Public Function second() As T2
            Return v2
        End Function

        Public Overrides Function GetHashCode() As Integer
            'i hit this break point, but ... 
            'how do i compute an integer hashcode from a long and an Integer?
            Return MyBase.GetHashCode()
        End Function

        Public Overrides Function Equals(obj As Object) As Boolean
            Return MyBase.Equals(obj)
        End Function

        Public Function Equals1(other As Pair(Of T1, T2)) As Boolean Implements IEquatable(Of Pair(Of T1, T2)).Equals
            'just as a test, but the code never gets here.
            Return True
        End Function
    End Class

    Public Sub TestCase()
        Dim a = New Pair(Of Long, Integer)(10, 10)
        myStore.Add(a, "Item 1")

        Dim b = ContainsItem(10, 10)
        'b is always false 
    End Sub
End Module

Solution

  • This implements IEquatable and overrides GetHashCode and Equals:

    ' an assumption about Pair(Of... :
    Public Class Pair(Of T, TT)
        Implements IEquatable(Of Pair(Of T, TT))
    
        Public Property ValueT As T
        Public Property ValueTT As TT
    

    Then, the methods:

    ' basic Equals for this Type
    Public Overrides Function Equals(obj As Object) As Boolean
        If obj.GetType Is GetType(Pair(Of Long, Integer)) Then
            Return Equals1(CType(obj, Pair(Of T, TT)))
        Else
            Return False
        End If
    End Function
    
    ' used by the Dictionary 
    Public Function Equals1(obj As Pair(Of T, 
                     TT)) As Boolean Implements IEquatable(Of Pair(Of T, TT)).Equals
        ' the other thing is Something Else
        If obj.GetType <> GetType(Pair(Of Long, Integer)) Then
            Return False
        End If
    
        'prefer T over TT, testing first
        If Integer.Equals(obj.ValueT, ValueT) = False Then
            Return False
        End If
    
        'T is equal, what about TT:
        Return Long.Equals(obj.ValueTT, ValueTT)
    End Function
    
     ' dictionary will use the hashcode for ContainsKey, Add
    Public Overrides Function GetHashCode() As Integer
        ' https://stackoverflow.com/a/371348/1070452
        ' marc gravell:
        Dim hash As Integer = 13
        hash = (hash * 7) + ValueT.GetHashCode()
        hash = (hash * 7) + ValueTT.GetHashCode()
    
        Return hash
    
        ''msdn (non generic value types):
        'Dim hCode As Long = ValueT Xor ValueTT
        'Return hCode.GetHashCode()
    End Function
    

    Testing:

    Dim a = New Pair(Of Long, Integer)(10, 10)
    Dim b = New Pair(Of Long, Integer)(5, 5)
    ' different object, same values:
    Dim c = New Pair(Of Long, Integer)(10, 10)
    
    Dim mydict As New Dictionary(Of Pair(Of Long, Int32), String)
    mydict.Add(a, "ziggy")
    mydict.Add(b, "zoey")
    
    Console.WriteLine("a==b? {0}", a.Equals(b).ToString)
    Console.WriteLine("a==c? {0}", a.Equals(c).ToString)
    Console.WriteLine("b==c? {0}", b.Equals(c).ToString)
    
    Console.WriteLine("Contains a? {0}", mydict.ContainsKey(a).ToString)
    Console.WriteLine("Contains b? {0}", mydict.ContainsKey(b).ToString)
    ' since the c OBJECT is not in the collection, it SHOULD report false
     ' but since the values are, and thats all that seems to matter:
    Console.WriteLine("Contains c? {0}", mydict.ContainsKey(c).ToString)
    

    Result:

    a==b? False
    a==c? True
    b==c? False
    Contains a? True
    Contains b? True
    Contains c? True
    

    The tests using c are actually false. The object 'c' was never added to the Dictionary, and 'c' is a different object than a. But the overrides used simply tests the objects based on the 2 values which are the same.

    I do not think I would do this, because a<>c. Instead, maybe use a collection class of some sort and avoid redefining Equals. The exact implementation would depend on whats in the collection. This would be a last resort, for me.

    See Also: