4
\$\begingroup\$

I have built my own generic class to implement IEqualityComparer(Of T), which takes a delegate as an input for the constructor. This delegate takes two objects of type T as an input and returns True if they are to be equal.

''' <summary>
''' A generic equality comparer which assess the equality of two objects via the provided delegate. 
''' </summary>
''' <typeparam name="T">The type of the objects to compare.</typeparam>
Public Class GenericEqualityComparer(Of T)
    Implements IEqualityComparer(Of T)


    ''' <summary>
    ''' The delegate invoked to verify equality between two given objects of type <see cref="T"/>.
    ''' </summary>
    Private Comparer As Func(Of T, T, Boolean)


    Private Sub New()
        Throw New NotImplementedException("User other constructor.")
    End Sub

    Public Sub New(Comparer As Func(Of T, T, Boolean))
        Me.Comparer = Comparer
    End Sub


    Public Shadows Function Equals(x As T, y As T) As Boolean Implements IEqualityComparer(Of T).Equals
        If x Is Nothing OrElse y Is Nothing Then
            Return False
        Else
            Return Comparer.Invoke(x, y)
        End If
    End Function


    Public Shadows Function GetHashCode(obj As T) As Integer Implements IEqualityComparer(Of T).GetHashCode
        Return obj.GetHashCode
    End Function


End Class

A example of usage would be

Dim Comparer as New GenericEqualityComparer(Of Person)(function(p, q) p.Name = q.Name And p.Surname = q.Surname)

My concern is over the implementation of GetHashCode(obj As T), which should somehow account for what the delegate focuses on.

Am I right to be concerned? Any ideas for a fix?


So far I was thinking of replacing the delegate signature by Func(T, IList(Of String)): the delegate would be invoked on both objects X and Y, then the resulting values would be stored in their own list, then both list would be compared side by side: if all side-by-side comparisons are correct, then Equals returns True. GetHashCode would run a .GetHashCode on the list of strings returned by the same delegate.

''' <summary>
''' A generic equality comparer which assess the equality of two objects by comparing the projection of their values via a delegate. 
''' </summary>
''' <typeparam name="T">The type of the objects to compare.</typeparam>
Public Class ProjectionEqualityComparer(Of T)
    Implements IEqualityComparer(Of T)


    ''' <summary>
    ''' The delegate invoked to project an object of type <see cref="T"/> onto a list of strings. Each string item correponds to the projection of a particular state or property of the object.
    ''' </summary>
    Private Projector As Func(Of T, IList(Of String))


    Private Sub New()
        Throw New NotImplementedException("User other constructor.")
    End Sub

    Public Sub New(Projector As Func(Of T, IList(Of String)))
        Me.Projector = Projector
    End Sub


    Public Shadows Function Equals(x As T, y As T) As Boolean Implements IEqualityComparer(Of T).Equals
        If x Is Nothing OrElse y Is Nothing Then
            Return False
        Else

            Dim xList As IList(Of String) = Me.Projector.Invoke(x)
            Dim yList As IList(Of String) = Me.Projector.Invoke(y)

            Dim xEnum As IEnumerator(Of String) = xList.GetEnumerator
            Dim yEnum As IEnumerator(Of String) = yList.GetEnumerator
            Do While xEnum.MoveNext = True And yEnum.MoveNext = True
                If xEnum.Current <> yEnum.Current Then Return False
            Loop

            Return True

        End If
    End Function


    Public Shadows Function GetHashCode(obj As T) As Integer Implements IEqualityComparer(Of T).GetHashCode
        Return Me.Projector.Invoke(obj).GetHashCode
    End Function


End Class

A usage would be

Dim OtherComparer as New ProjectionEqualityComparer(Of Person)(function(p) New List(of String) From {p.Name, p.Surname}

This solution is not so satisfying because it adds a level of complexity for anyone who would review it. It is also hard to turn into a metaphor, hence making the documentation not so user-friendly.


Having originally posted the question on SO, @pstrjds suggested to define a HashCode delegate instead of the Comparer or Projector delegates.

I like it, but my understanding is that GetHashCode would accept some collisions, whilst Equals should avoid at all cost. Correct?

Source: https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=netframework-4.8

Do not test for equality of hash codes to determine whether two objects are equal. (Unequal objects can have identical hash codes.) To test for equality, call the ReferenceEquals or Equals method.

\$\endgroup\$
5
\$\begingroup\$

GetHashCode will mostly be used in collections using hash tables internally, like dictionaries or hash sets. If you know that you are only going to test for equality, it is safe to not implement GetHashCode.

  • Instead of letting the projector return a list of values, better store a list of accessor delegates. If you create a list of values, this list will have to be created each time you are comparing values. Accessors are created once.

  • Private Sub New() cannot be called from outside the class. No need to throw an exception. You can even drop it completely, because when you provide any other constructor with parameters, the default constructor (i.e. the constructor without parameters) will not be generated by VB.

  • Calling GetHashCode on a list, only gets a hash code for the list object, without considering the list entries. We must calculate a combined hash code ourselves.

  • Our comparer will be more generic, if we don't assume the values to be of type String and instead operate on Object.

My suggestion:

Public Class ValuesEqualityComparer(Of T)
    Implements IEqualityComparer(Of T)

    ReadOnly _valueAcessors As IList(Of Func(Of T, Object))

    ' We can drop the parameter-less constructor. The other one replaces this one. 
    ' Now, you must provide a an argument for New.
    'Private Sub New()
    'End Sub

    Public Sub New(valueAcessors As IList(Of Func(Of T, Object)))
        _valueAcessors = valueAcessors
    End Sub

    Public Overloads Function Equals(x As T, y As T) As Boolean _
        Implements IEqualityComparer(Of T).Equals

        Return _valueAcessors.All(Function(v) Object.Equals(v(x), v(y)))
    End Function

    Public Overloads Function GetHashCode(obj As T) As Integer _
        Implements IEqualityComparer(Of T).GetHashCode

        Dim hash As Integer = 17
        For Each v In _valueAcessors
            hash = hash * 23 + If(v(obj), 0).GetHashCode()
        Next
        Return hash
    End Function
End Class

You would create a comparer like this

Dim comparer = New ValuesEqualityComparer(Of TestClass)(
    {Function(x) x.Id,
     Function(x) x.Name})

Note, IList is compatible with arrays and List(Of T). I am using an array literal here. See also: How to: Initialize an Array Variable in Visual Basic

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ I never thought I'd learn that much on such a small portion of code ; many thanks! A few questions: 1. Since the accessors are run and a resulting value calculated each time we compare, how is this more efficient? 2.I used to leave my Private Sub New() empty until I started to use static factories, in which case a private constructior does not necessarily mean "don't use me". Is it a "standard" to leave these private constructors empty? 3. I avoided the object type because I was afraid Object.Equals() would run an instance comparison. \$\endgroup\$ – Ama Sep 4 '19 at 5:10
  • \$\begingroup\$ 4. Regarding GetHashCode(), what would be wrong in doing For Dim TempStr as String: Each v In _valueAcessors: TempStr &= v(obj). GetHashCode(): Next: Return TempStr.GetHashCode()? \$\endgroup\$ – Ama Sep 4 '19 at 5:16
  • 1
    \$\begingroup\$ 3. It is each type's responsibility to override its own Equals method. E.g. String is a reference type, but Equals is overridden, so that each character is compared instead of only the reference. You can see how Object.Equals is implemented here. I.e. it first compares the reference. If the reference is different it calls the object's (possibly overridden) Equals method. Just below you can see how ReferenceEquals is implemented. \$\endgroup\$ – Olivier Jacot-Descombes Sep 4 '19 at 12:45
  • 1
    \$\begingroup\$ 4. There are many ways to calculate a hash code. Simple arithmetic is much faster than string manipulation. Each &= creates a new String object on the heap and copies each character from the old TempStr object and the new hash code string (also created on the heap) to the new String object. Then the reference to this new String object is assigned to TempStr. Note: strings are immutable! Every string manipulation creates a new string. Therefore String feels and behaves like a value type, but is in reality a reference type. \$\endgroup\$ – Olivier Jacot-Descombes Sep 4 '19 at 12:55
  • 1
    \$\begingroup\$ Whenever you define a constructor with parameters, the constructor without parameters does not exist any more. Therefore, this prevents anyone from instantiating the object with no parameters. \$\endgroup\$ – Olivier Jacot-Descombes Sep 17 '19 at 14:52

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.