Equality methods of GeometryObject

Equality methods of GeometryObject

RPTHOMAS108
Mentor Mentor
1,501 Views
7 Replies
Message 1 of 8

Equality methods of GeometryObject

RPTHOMAS108
Mentor
Mentor

Hello

 

Can someone confirm what the equality methods of GeometryObject are based upon i.e.

Memory reference

Geometric configuration and/or properties of the class

Id assigned to the class alone (GeometryObject.Id)

etc.

 

I'm specifically interesting in the equality/inequality operators and the overridden Object.Equals:

 

230804.PNG

 

I guess the simple yes / no answer (if it exists) would be: are the equality methods noted above based on memory reference alone? However isn't Object.Equals based on memory reference to begin with? If it is overridden then how is it overridden?

 

I tend to create a lot of class objects whose purpose is to wrap an API object and attach an id due to uncertainty about the equality of the API objects. Some clarification here or in the documentation above would probably be quite helpful in reducing this aspect.

 

Here is a bit more information I should have read beforehand in RevitAPI.chm under GeometryObject.equals (Remarks section):

"This compares the internal identifiers of the geometry, and doesn't compare them geometrically"

 

Now that we've ruled out the geometric configuration we just need to know what the 'internal identifiers' are exactly. Is it the GeometryObject.Id or something else? 

 

What kind of operations affected these identifiers?

Do they exist for every API object created or just the items coming directly from element geometry?

Accepted solutions (1)
1,502 Views
7 Replies
Replies (7)
Message 2 of 8

RPTHOMAS108
Mentor
Mentor

I've now also Reviewed the below from RevitAPI.chm GeometryObject.Id

"This id can be stored and used for future referencing. The reference should be stable between minor geometric changes and modifications, but may not remain valid if there are major changes to the element or its surroundings. Note that the id may be negative(and thus invalid for referencing) if obtained from view - specific geometry, or if obtained from most GeometryObjects created in memory by the API. Negative ids cannot be used for referencing. These integer ids should not be used for comparison purposes(other than to check if they are equivalent or not). Nothing should be assumed about rules about how an element populates the sequence of different numeric values as this may change based on the element's definition."

 

I've noticed previously that geometry from two different elements could have the same id (the id values are low so that is reasonable). That being the case I'm also looking to confirm that the equality methods noted above are not equating two geometry objects with the same id from two different elements as being the same (Internal identifier <> GeometryObject.Id).

 

I guess I could test that.

Message 3 of 8

jeremy_tammik
Alumni
Alumni

Thank you for your very interesting and pertinent question. I passed it on to the development team for you.

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 4 of 8

RPTHOMAS108
Mentor
Mentor

Thanks Jeremy

 

I think knowing more about this could be quite helpful either way. We could test and find answers but it would then remain an unknown aspect to new people encountering the API.

 

Personally I always look at the equals functions in the documentation to see if it has been overridden for a class or not. That then informs me if I can use Linq extensions such as 'distinct' and 'union' with it (so we need to know how it is overridden also).

 

For classes such as ElementId it is inherently obvious but some objects are less so and need more description. If it is just using GeometryObject.Id then I can live with the limitations of that I just need to know that is what it is doing.

0 Likes
Message 5 of 8

jeremy_tammik
Alumni
Alumni
Accepted solution

The development team replies:

  

This is an interesting question, and I'm not sure if this may be something that we want to update:

  

  • GeometryObject.Equals - This compares the memory references of the compared objects.
  • GeometryObject.operator == - This compares the pointers of the Revit-internal geometry objects.
  • GeometryObject.GetHashCode - This is a hash of the pointer to the Revit-internal geometry object.

  

In principle, there could be two different Revit.DB.GeometryObject objects that are both handles to the same Revit-internal geometry object (for example, you retrieve the GeometryObject from the same Reference twice in succession, you would end up with two different GeometryObjects that both are handles to the same Revit-internal object. In this case, GeometryObject.Equals would return false, but GeometryObject.operator == would return true. Both GeometryObjects would have the same hash code.

 

If either comparison method returns true, then that guarantees that they both have the same GeometryObject.Id, which is an attribute of the underlying Revit object. However, two totally unrelated GeometryObjects may have the same Id, since this is not intended to be a globally unique identifier.

   

For completeness, the inequality operator is simply the negation of the equality operator. Two GeometryObjects that internally hold null pointers to Revit-internal objects are considered equal when compared with operator ==.

I think this behavior could be considered reasonable, if we update the documentation to reflect the actual behavior.

  

Also, as noted, it may not make sense to override Object.Equals at all.

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 6 of 8

RPTHOMAS108
Mentor
Mentor

Thanks Jeremy et al for the fast reply to this.

 

I think I'm largely* happy with the status quo, as stated it is probably more important that how it works is better described in the documentation (we then can make the right allowances based on that).

 

Seems like there is no real logic in two managed objects that point to the same unmanaged object not returning true for Equals. I can't think of a reason off of the top of my head as to why the Equals function doesn't work exactly the same way as the == operator. *However the Linq extensions will only be using 'Equals' not == so it might be useful therefore to make Equals the same as ==. Secondly 'GetHashCode' will be called by Linq extensions prior to 'Equals' so therefore it should be overridden to return 0 to force the comparison by 'Equals' or be overridden to perhaps provide a better hashing function related to the unmanaged memory reference (not the managed counterpart).

 

I have a feeling that the default algorithm that Object.GetHashCode uses in .net is fast rather than perfect in terms of comparing two objects as being the same. Since the hash code is based on the reference (64 bit on 64 bit system) but the hash code itself is only 32 bit. So in theory there can be clashes that then need further resolution via 'Equals'. In contrast if you are comparing the value of 64 bit memory pointers directly on unmanaged side then there can be no doubt it is the same object.

 

All I'm aiming for in the end is that when I extract objects from Revit I can then pass those objects through my functions which could perhaps sort them or group them but in the end I can compare what comes out against the original set. I also need to remove items from a List (List<T>.Remove) which is sometimes hard if 'Equals' and 'GetHashCode' has not been overridden (since that method will use the default equality comparer).

 

To deal with these kinds of things I recently wrote the following generic classes where comparisons are conducted during the context of the external command. I think a lot of it comes down to trust in GetHashCode. Obviously the below is for all kinds of things not just geometry objects. The pain of this kind of approach is that you have to create a new list of items of the below classes from the existing list of API objects rather than just knowing you can use the API objects directly.

 

 Public Class RT_GeometryObjWithId(Of T As GeometryObject)
        Inherits RT_ApiObjectWithId(Of T)

        Public ReadOnly Property GeometryId As Integer
        Public Sub New(Obj As T)
            MyBase.New(Obj)
            GeometryId = Obj.Id
        End Sub
  End Class

   Public Class RT_ApiObjectWithId(Of T As APIObject)
        Implements IDisposable

        Public ReadOnly Property UID As Guid = Guid.NewGuid
        Private disposedValue As Boolean
        Private IntAPIObj As T
        Public Property APIObject As T
            Get
                Return IntAPIObj
            End Get
            Set(value As T)
                IntAPIObj = value
            End Set
        End Property
        Public Sub New(Obj As T)
            IntAPIObj = Obj
        End Sub

        Public Overrides Function GetHashCode() As Integer
            'Force consideration of Equals
            'Likely I could leave this alone and rely on the default implementation
            'However I have more trust in comparing two guids than GetHashCode
            Return 0
        End Function
        Public Overrides Function Equals(obj As Object) As Boolean
            Dim Other As RT_ApiObjectWithId(Of T) = TryCast(obj, RT_ApiObjectWithId(Of T))
            If Other Is Nothing Then Return False else
            Return Me.UID = Other.UID
        End Function

        Public Shared Operator =(A As RT_ApiObjectWithId(Of T), B As RT_ApiObjectWithId(Of T)) As Boolean
            Return A.Equals(B)
        End Operator
        Public Shared Operator <>(A As RT_ApiObjectWithId(Of T), B As RT_ApiObjectWithId(Of T)) As Boolean
            Return Not A.Equals(B)
        End Operator

        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not disposedValue Then
                If disposing Then
                    ' TODO: dispose managed state (managed objects)
                    If APIObject IsNot Nothing Then
                        APIObject.Dispose()
                    End If
                End If

                ' TODO: free unmanaged resources (unmanaged objects) and override finalizer
                ' TODO: set large fields to null
                disposedValue = True
            End If
        End Sub

        ' ' TODO: override finalizer only if 'Dispose(disposing As Boolean)' has code to free unmanaged resources
        ' Protected Overrides Sub Finalize()
        '     ' Do not change this code. Put cleanup code in 'Dispose(disposing As Boolean)' method
        '     Dispose(disposing:=False)
        '     MyBase.Finalize()
        ' End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code. Put cleanup code in 'Dispose(disposing As Boolean)' method
            Dispose(disposing:=True)
            GC.SuppressFinalize(Me)
        End Sub
    End Class

 

 

0 Likes
Message 7 of 8

jeremy_tammik
Alumni
Alumni

Thank you for your appreciation and especially your clear analysis and perspective. I passed it on as well.

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 8 of 8

jeremy_tammik
Alumni
Alumni

Interesting and important thoughts, useful code snippet, added to the blog:

  

https://thebuildingcoder.typepad.com/blog/2023/09/add-in-threads-and-geometry-comparison.html#3

  

Thank you!

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes