Inspired by, and perhaps slightly improving on this answer by IllidanS4 (and superseding another answer of my own), you can just use the following. I tested it as working with both x86
and amd64
in .NET 4.7.
public static unsafe bool RefEquals<T1, T2>(ref T1 a, ref T2 b)
{
TypedReference pa = __makeref(a), pb = __makeref(b);
return *(IntPtr*)&pa == *(IntPtr*)&pb;
}
That's it for the actual code contribution of this article.
You can stop reading here if you're not interested in a more general discussion on the subject of contrasting class
versus struct
use of ref
("ByRef") referencing.
Note: Nullable<T> issues are unrelated to these observations and are not discussed here
Besides condensing two functions into one, the main difference is that this version only considers the first field of the TypedReference value type when checking for equivalence, because that's the extent of the managed pointer it wraps. The second IntPtr-sized field of the TypedReference
should arguably be ignored because it just records the Type
of the respective target, which "conventionally" perhaps should never affect the outcome of a reference equality determination.
The use of scare-quotes there is meant to highlight the possibility of a choice between design alternatives: namely, whether to heed or ignore claimed Type
information in cases where reference equality would otherwise be affirmed. General precedent is for ignoring Type
, such as in the case of comparing variegated null references, where the widely expected behavior is to conflate all typed nulls as co-equal.
if (default(Exception) == default(RankException))
{
/* always true */
}
But is this "precedent" even relevant? Does typed null
really have any Type
--or for that matter, runtime existence--to then be "ignored" at all? Indeed, extending the earlier reasoning to realistic runtime ByRef comparisons might not be so straightforward. First let's look at the concept of nullability focusing exclusively on what it means in the context of managed "ref
" referencing.
An initial observation could be that one's intuitions about nullability--as concerning reference- versus value-type instances--might be reversed when dealing with managed references to each (respectively): for value types, where null
values are inherently nonsensical, null
reference values becomes extremely relevant and useful, whereas for reference types, for which the notion of null
is normally pervasive, it's nearly impossible and almost certainly wrong to ever deal with a null
-valued ByRef
reference. Here's why: because a ref
variable inherently admits the possibility of both reading and writing its target, its type is constrained to be the intersection of the polymorphic co-variance and contra-variance for which it would otherwise be eligible. Obviously, the result of this intersective collapse is a single Type
, now necessarily invariant.
Now for value types, the domain of read/write access that ref
gives you is your program's data. Since .NET doesn't care what you do in that domain, you can bend the rules and coerce these managed pointers to a certain degree. You can generally muck about to set them to null
(despite C#'s protestations) interconvert them with IntPtr
, retarget them at well, and the like. For an example, I'll return to IllidanS4's code and the issue of whether conflicting Type
information should be disregarded in cases where reference equality is confirmed. As I noted, the issue is moot for his original code since it can't be called with non-identical types. But for the purpose of discussion, here's a version that relaxes the generic type constraint so that the function can actually be entered with disjoint types, but still compares the two TypedReference
images in full, presumably then to fail all the newly admitted cases:
public static unsafe bool RefEqualsFull<T1, T2>(ref T1 a, ref T2 b)
{
TypedReference ra = __makeref(a), rb = __makeref(b);
IntPtr* pa = (IntPtr*)&ra, pb = (IntPtr*)&rb;
return pa[0] == pb[0] && pa[1] == pb[1];
}
The difference between this code and my own proposal at the top can be seen with the following:
int i = 1234;
uint* pui = (uint*)&i;
bool b1 = RefEquals(ref i, ref *pui); // TRUE because of RefEq (despite type difference)
bool b2 = RefEqualsFull(ref i, ref *pui); // FALSE despite RefEq (because types differ)
As you can see, with a value type, we CLR
will let us abuse the type system since we are only potentially harming ourselves. Unfortunately, C# does not allow some of the most useful ref
manipulations, and this is still true even with the outstanding new ref local and ref return features in C# 7. For example, C# (and the vs2017
debugger) still strongly resist null ByRef values (even though they're perfectly fine with the CLR and exposed by other languages such as C++/CLI).
This is a tragedy for the most overwhelmingly obviously scenario for the use of null-valued ref
passing, which is to be able to signal special cases when calling APIs that pass pointers. Pervasively with P/Invoke and interop marshaling of legacy data structures, you encounter native APIs that define opt-out behavior where you elect not to receive an "out" parameters by passing in a null pointer for (i.e.):
[DllImport("SomeNativeApi.dll")]
extern unsafe void LegacyAPI([Out, Optional] RECT **ppRect);
If C# (and to be fair I believe the default marshalling is to blame here also) would allow null-valued ref
to flow like any other value, you wouldn't have to switch from efficient, lightweight managed pointers to so-called [formatted classes] or somewhat obtrusive Nullable<T>
just for the sake of supporting some legacy API's whim.
Switching now to the case of using ref
references to reference type (references). There won't be much to say here, because unlike before, the target domain of your read/write ref
access is no longer your data, but rather squarely the realm of managed object references, about which .NET cares very deeply.
Remember, when you do a ByRef comparison where the underlying types are reference ("class") types, your comparison has little to do with the values of the references (i.e. the referred-to object instances) any more--that's what's trivially accomplished with Object.ReferenceEquals
--you're instead asking whether the two reference handles themselves occupy the same memory address (or not). This is rarely any of your business, so the clamps come down and it's nearly impossible to do the sort of rude manipulations shown above.
As a consolation, it is also quite unusual to need ref
access to reference types. So far, I've only come across one practical scenario: discovering at runtime which field of a class instance a certain managed reference "q" might be referring to, if any. In other words:
Given a reference to some ref Object q = ...
...and an instance c = (MyClass)...
of class class MyClass { Object o1; Object o2 }
,
...determine which field in c
, if any, can be accessed by reference via q
(...and do this without reading or writing to the field itself).
This is only possible by enumerating the fields of the instance and using the RefEquals
function shown above to compare the location indicated by "q" with the location of each field in turn. RefEquals
means it will only be possible for the value of "q" to match itself, regardless of its referent (the "value" of the field, whether struct or object-reference) or the referent of the other fields, any of which could be null
, uninitialized, or mutually-shared object instances that would foil a "normal" search.
And finally, to satisfy the curious regarding whether the correct answer to the earlier example is true or false, this seems destined to remain an unresolved matter of preference.