Given the struct S1:
unsafe readonly struct S1
{
public readonly int A1, A2;
public readonly int* B;
public S1(int a1, int a2, int* b)
{
A1 = a1;
A2 = a2;
B = b;
}
}
and an equality test:
int x = 10;
var a = new S1(1, 2, &x);
var b = new S1(1, 2, &x);
var areEqual = Equals(a, b); // true
areEqual
evaluates to true, as expected.
Now lets slightly change our struct to S2 (replacing the pointer with a string):
unsafe readonly struct S2
{
public readonly int A1, A2;
public readonly string C;
public S2(int a1, int a2, string c)
{
A1 = a1;
A2 = a2;
C = c;
}
}
with an analog test:
var a = new S2(1, 2, "ha");
var b = new S2(1, 2, "ha");
var areEqual = Equals(a, b); // true
this evaluates to true as well.
Now the interesting part. If we combine both structs to S3:
unsafe readonly struct S3
{
public readonly int A1, A2;
public readonly int* B;
public readonly string C;
public S3(int a1, int a2, int* b, string c)
{
A1 = a1;
A2 = a2;
B = b;
C = c;
}
}
and test for equality:
int x = 10;
var a = new S3(1, 2, &x, "ha");
var b = new S3(1, 2, &x, "ha");
var areEqual = Equals(a, b); // false
The equality test fails, unexpectedly. Even worse,
Equals(a, a); // false
does also fail the test.
Why do the last two equality tests evaluate to false?
Bugreport for reference. Fixed in .net 6.0.
The actual comparison of the instances is performed by ValueType.Equals. Here is the implementation:
public override bool Equals(object? obj)
{
if (null == obj)
{
return false;
}
Type thisType = this.GetType();
Type thatType = obj.GetType();
if (thatType != thisType)
{
return false;
}
object thisObj = (object)this;
object? thisResult, thatResult;
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (int i = 0; i < thisFields.Length; i++)
{
thisResult = thisFields[i].GetValue(thisObj);
thatResult = thisFields[i].GetValue(obj);
if (thisResult == null)
{
if (thatResult != null)
return false;
}
else
if (!thisResult.Equals(thatResult))
{
return false;
}
}
return true;
}
We can see that basically it performs a bit level comparison if there are no GC references (classes referenced in your fields), otherwise it will invoke Equals on every field of your struct.
When you have a field of pointer type (int* in your case) and you use Reflection to get its value, then the value you get is boxed as a System.Reflection.Pointer.
We can see that it is a class, so no bit-level comparison will be performed.
So it will invoke Pointer.Equals, but unfortunately we can see by the Pointer class source code that it isn't overridden, so the check performed will be if the references of the object are the same:
public sealed unsafe class Pointer : ISerializable
{
// CoreCLR: Do not add or remove fields without updating the ReflectionPointer class in runtimehandles.h
private readonly void* _ptr;
private readonly Type _ptrType;
private Pointer(void* ptr, Type ptrType)
{
Debug.Assert(ptrType.IsRuntimeImplemented()); // CoreCLR: For CoreRT's sake, _ptrType has to be declared as "Type", but in fact, it is always a RuntimeType. Code on CoreCLR expects this.
_ptr = ptr;
_ptrType = ptrType;
}
public static object Box(void* ptr, Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (!type.IsPointer)
throw new ArgumentException(SR.Arg_MustBePointer, nameof(ptr));
if (!type.IsRuntimeImplemented())
throw new ArgumentException(SR.Arg_MustBeType, nameof(ptr));
return new Pointer(ptr, type);
}
public static void* Unbox(object ptr)
{
if (!(ptr is Pointer))
throw new ArgumentException(SR.Arg_MustBePointer, nameof(ptr));
return ((Pointer)ptr)._ptr;
}
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new PlatformNotSupportedException();
}
internal Type GetPointerType() => _ptrType;
internal IntPtr GetPointerValue() => (IntPtr)_ptr;
}
So the comparison will return false because you have a pointer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With