简体   繁体   English

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

[英]Unexpected equality tests, Equals(a, a) evaluates to false

Given the struct S1:给定结构 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.正如预期的那样, areEqual评估为 true。

Now lets slightly change our struct to S2 (replacing the pointer with a string):现在让我们稍微改变我们的结构为 S2(用字符串替换指针):

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:如果我们将两个结构组合到 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?为什么最后两个相等测试评估为假?

Edit编辑

Bugreport for reference.错误报告供参考。 Fixed in .net 6.0.已在 .net 6.0 中修复。

The actual comparison of the instances is performed by ValueType.Equals .实例的实际比较由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.我们可以看到,如果没有 GC 引用(在您的字段中引用的类),它基本上会执行位级比较,否则它将在您的结构的每个字段上调用 ​​Equals。

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 .当您有一个指针类型的字段(在您的情况下为 int*)并且您使用 Reflection 获取其值时,您获得的值将作为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:所以它会调用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;
}

So the comparison will return false because you have a pointer.因此比较将返回 false,因为您有一个指针。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM