I encountered a behavior in VB.NET today regarding boxing and reference comparison that I did not expect. To illustrate I wrote a simple program which tries to atomically update a variable of any type.
Here is a program in 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);
}
}
The output of this program is:
Hello World
True
True
True
False
True
True
This is as expected and everything seems to work fine. Here is the same program in 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
Here the output is:
Hello World
True
True
True
False
True
False
Here the last statement is false which means that the set did not work. Am I doing something wrong here or is the problem in the VB.NET?
(Note: Ignore the Volatile reads/writes, this example has no threads so it is not affected by threading)
Edit:
If I change the T to integer then everything works OK:
(dotnetfiddle.net/X6uLZs).
Also if I change T to a custom class it also works OK:
dotnetfiddle.net/LnOOme
I believe the cause of this issue is actually VB's Object
handling, where in some places it's more similar to C#'s dynamic
than a plain Object
. Specifically, if I re-write TrySetValue
as:
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
We would never expect that Console.WriteLine
to print False
. But that's precisely what it does. Decompiling this code back to C# (using Reflector), I obtain this code:
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));
}
Oh dear. What are all of those calls to GetObjectValue
doing here? Well, the effect they're having is to cause copies to be made of boxed value types and so curValObj
never contains an actual reference to the same object as _value
refers, and so the Interlocked.CompareExchange
can never work when we're dealing with actual object references.
I can't think of a good way of re-writing this code, presently, to do what you want. And perhaps we can see a further reason why the Object
overload of CompareExchange
warns us:
Do not use this overload with value types.
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