C#和VB.Net之间对象装箱/比较引用的不同

5
今天我在VB.NET中遇到了一个关于装箱和引用比较的行为,这让我感到意外。为了说明问题,我编写了一个简单的程序,尝试以原子方式更新任何类型的变量。
以下是C#程序(https://dotnetfiddle.net/VsMBrg):
using System;

public static class Program
{

    private static object o3;

    public static void Main()
    {
        Console.WriteLine("Hello World");

        Test<DateTimeOffset?> value = new Test<DateTimeOffset?>();
        Console.WriteLine(value.Value == null);
        DateTimeOffset dt1 = new DateTimeOffset(2017, 1, 1, 1, 1, 1, TimeSpan.Zero);
        DateTimeOffset dt2 = new DateTimeOffset(2017, 1, 2, 1, 1, 1, TimeSpan.Zero);

        Console.WriteLine(value.TrySetValue(null, dt1));
        Console.WriteLine(value.Value == dt1);

        // this should fail
        Console.WriteLine(value.TrySetValue(null, dt2));
        Console.WriteLine(value.Value == dt1);

        // this should succeed
        Console.WriteLine(value.TrySetValue(dt1, dt2));
    }
}

public class Test<T>
{


    public T Value {
        get { return (T)System.Threading.Volatile.Read(ref _value); }
    }


    private object _value;

    public bool TrySetValue(T oldValue, T newValue)
    {
        object curValObj = System.Threading.Volatile.Read(ref _value);
        if (!object.Equals((T)curValObj, oldValue))
            return false;
        object newValObj = (object)newValue;
        return object.ReferenceEquals(System.Threading.Interlocked.CompareExchange(ref _value, newValObj, curValObj), curValObj);
    }

}

这个程序的输出是:
Hello World
True
True
True
False
True
True

这是预期的,一切似乎都正常工作。 以下是同样的程序的VB.NET版本(https://dotnetfiddle.net/lasxT2):
Imports System

Public Module Module1

    private o3 as object

    Public Sub Main()
        Console.WriteLine("Hello World")

        Dim value As New Test(Of DateTimeOffset?)
        Console.WriteLine(value.Value is nothing)
        Dim dt1 As New DateTimeOffset(2017, 1, 1, 1, 1, 1, TimeSpan.Zero)
        Dim dt2 As New DateTimeOffset(2017, 1, 2, 1, 1, 1, TimeSpan.Zero)

        Console.WriteLine(value.TrySetValue(Nothing, dt1))
        Console.WriteLine(value.Value = dt1)

        ' This should fail
        Console.WriteLine(value.TrySetValue(Nothing, dt2))
        Console.WriteLine(value.Value = dt1)

        ' This should succeed
        Console.WriteLine(value.TrySetValue(dt1, dt2))
    End Sub
End Module

public class Test(Of T)


    Public readonly Property Value As T
        Get
            Return CType(Threading.Volatile.Read(_value), T)
        End Get
    End Property

    Private _value As Object

    Public Function TrySetValue(oldValue As T, newValue As T) As Boolean
        Dim curValObj As Object = Threading.Volatile.Read(_value)
        If Not Object.Equals(CType(curValObj, T), oldValue) Then Return False
        Dim newValObj = CObj(newValue)
        Return Object.ReferenceEquals(Threading.Interlocked.CompareExchange(_value, newValObj, curValObj), curValObj)
    End Function

end class

这里的输出是:

Hello World
True
True
True
False
True
False

这里的最后一句话是错误的,这意味着该集合未起作用。 我做错了什么还是问题出在VB.NET上?
(注:忽略“Volatile reads / writes”,此示例没有线程,因此不受线程影响)

编辑: 如果我将T更改为整数,那么一切都可以正常工作:
(dotnetfiddle.net/X6uLZs)。 此外,如果我将T更改为自定义类,则也可以正常工作:
dotnetfiddle.net/LnOOme

1个回答

4
我认为这个问题的原因实际上是VB的Object处理方式,在某些情况下,它更类似于C#的dynamic而不是纯粹的Object。具体来说,如果我将TrySetValue重写为:
  Public Function TrySetValue(oldValue As T, newValue As T) As Boolean
    Dim curValObj As Object = _value 'Threading.Volatile.Read(_value)
    Console.Write(Object.ReferenceEquals(curValObj,_value))
    If Not Object.Equals(CType(curValObj, T), oldValue) Then Return False
    Dim newValObj = CObj(newValue)
    Return Object.ReferenceEquals(Threading.Interlocked.CompareExchange(_value, newValObj, curValObj), curValObj)
  End Function

我们从未预料到Console.WriteLine会打印False。但这正是它所做的。将此代码反编译回C#(使用Reflector),我获得了以下代码:
public bool TrySetValue(T oldValue, T newValue)
{
    object objectValue = RuntimeHelpers.GetObjectValue(this._value);
    Console.Write(object.ReferenceEquals(RuntimeHelpers.GetObjectValue(objectValue), RuntimeHelpers.GetObjectValue(this._value)));
    if (!object.Equals(Conversions.ToGenericParameter<T>(objectValue), oldValue))
    {
        return false;
    }
    object obj3 = newValue;
    return object.ReferenceEquals(RuntimeHelpers.GetObjectValue(Interlocked.CompareExchange(ref this._value, RuntimeHelpers.GetObjectValue(obj3), RuntimeHelpers.GetObjectValue(objectValue))), RuntimeHelpers.GetObjectValue(objectValue));
}

哦,亲爱的。所有这些对GetObjectValue的调用都是做什么的?好吧,它们产生的效果是使装箱值类型的副本被创建,因此curValObj从未包含与_value引用相同对象的实际引用,因此,在处理实际对象引用时,Interlocked.CompareExchange永远无法起作用。
我目前想不出重写此代码以执行所需操作的好方法。也许我们可以看到进一步原因,为什么CompareExchangeObject重载会警告我们:

不要将此重载与值类型一起使用。


@Crowcoder - 这个小代码示例中意外复制的数量让我怀疑任何VB代码是如何工作的 :-( - Damien_The_Unbeliever
从CLR源代码中:“技术上,我们可以在此处返回装箱的DateTimes和Decimals而无需复制它们,但VB意识到这将对其客户造成破坏性影响。因此,请复制它们”。 - Hans Passant
@HansPassant 这个评论在哪里? - Dalibor Čarapić
https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/objectnative.cpp#L56 - Hans Passant
关于您的评论“不要在值类型中使用此重载。”- 我认为这是为了避免使用隐式装箱调用该方法。无论如何,我认为它不应该适用于此场景,而且C#版本按预期工作,所以这真的是一个VB.NET问题。 - Dalibor Čarapić
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接