0

When compiling code at runtime using expression trees, we may find ourselves needing to check objects of indeterminate types for equality.

If we were to simply code this up by hand for each case, the compiler would take into account many things for us. The most ones that come to mind are:

  1. If a generic overload T1.Equals<T2> or T2.Equals<T1> is available, that is used.
  2. Otherwise, if either type has an implicit operator that would let us apply (1), that is used.
  3. Otherwise, bool Equals(object) is used (taking into account potential overrides, of course).

Regrettably, Expression.Equal(Expression left, Expression right) does not do all these things for us.

How can we achieve the same behavior that the compiler normally provides?

Timo
  • 7,992
  • 4
  • 49
  • 67
  • 1
    You have to read the spec and implement all those checks/searches yourself. There is nothing publicly visible to my knowledge - `dynamic` does exactly that - you may want to read on how that's done. (I'm not sure if that code is available publicly) – Alexei Levenkov Jan 31 '20 at 21:32
  • 1
    With expression trees, you can only create a single Method, for your goal you need to implement an entire class. Either you use TypeBuilder, which needs Know-How, or you generate C# source code file and compile that, that's easier but little slower. – Holger Jan 31 '20 at 21:46
  • On the bright side, I realized that generally one of two scenarios applies. (A) The type is a struct, and to avoid boxing there is merit in using a generic `Equals(T)` overload, signaled by the `IEquatable` interface. (B) The type is a class. If there is a direct `IEquatable` interface, we can use that. If only a _base_ type implements `IEquatable` on itself, the compiler would normally still choose that, but our implementation does not find it. Luckily, it does not matter: `Equals(object)` should always return the same result as `Equals(T)` for a `T` argument. – Timo Mar 05 '20 at 10:29
  • I should note that my previous comment applies in part thanks to the fact that *I'm only comparing objects of an identical compile-time type*. If that were not the case, the operand of the `Equals` call normally might be implicitly converted by the compiler, which would complicate an implementation that does the same at runtime. – Timo Mar 05 '20 at 10:33

1 Answers1

0

Assuming that we are only comparing objects of identical compile-time types, we can follow a simple two-step process when building the call expression:

  1. If the compile-time type implements IEquatable<T>, where T is the type itself, then call the corresponding Equals(T) method.

This prevents boxing on structs that implement IEquatable<T>, which they often do for precisely that reason. It has the added benefit of calling the most obvious Equals overload on classes.

  1. Otherwise, call the regular (possibly overridden) Equals(object) method.

There are two caveats.

The first caveat is that Equals(T) methods in the absence of the IEquatable<T> interface are ignored. This may be considered reasonable: if the developer did not add the interface, it is not entirely clear whether they wanted the method to be used for this purpose or not.

The second caveat is that, for classes, an IEquatable<T> interface on a base class is ignored, whereas the compiler might have preferred an overload corresponding to that. Luckily, we may reasonably expect Equals(object) to return the same result as Equals(T) for a T object. If not, the developer has provided an ambiguous definition of equality, and all bets are off.

Timo
  • 7,992
  • 4
  • 49
  • 67