意外的相等测试,Equals(a, a) 评估为 false

Tim*_*imo 8 c#

给定结构 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

和一个平等测试:

int x = 10;
var a = new S1(1, 2, &x);
var b = new S1(1, 2, &x);

var areEqual = Equals(a, b);  // true
Run Code Online (Sandbox Code Playgroud)

areEqual 正如预期的那样,评估为 true。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

with an analog test:

var a = new S2(1, 2, "ha");
var b = new S2(1, 2, "ha");

var areEqual = Equals(a, b);  // true
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

The equality test fails, unexpectedly. Even worse,

Equals(a, a); // false
Run Code Online (Sandbox Code Playgroud)

does also fail the test.

Why do the last two equality tests evaluate to false?

Edit

Bugreport for reference. Fixed in .net 6.0.

Mat*_*ili 5

实例的实际比较由ValueType.Equals执行。这是实现:

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;
}
Run Code Online (Sandbox Code Playgroud)

我们可以看到,如果没有 GC 引用(在您的字段中引用的类),它基本上会执行位级比较,否则它将在您的结构的每个字段上调用 ​​Equals。

当您有一个指针类型的字段(在您的情况下为 int*)并且您使用 Reflection 获取其值时,您获得的值将作为System.Reflection.Pointer装箱。

我们可以看到它是一个类,因此不会进行位级比较。

所以它会调用Pointer.Equals,但不幸的是我们可以通过Pointer类源代码看到它没有被覆盖,所以执行的检查将是对象的引用是否相同:

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;
}
Run Code Online (Sandbox Code Playgroud)

因此比较将返回 false,因为您有一个指针。