Question

The normal expected semantics of a reference type is that it should behave as an object identifier. If some variable holds a reference to the 5483rd object a program created, passing that variable to a method should give it a reference to the 5483rd object. VB.NET mostly works like this, but with a rather curious exception: even in the Option Strict On dialect, attempting pass such a variable of type Object to a method which takes a parameter of that type, copy one Object variable to another, or otherwise cause an "rvalue" of type Object [to borrow C terminology] to be stored to an "lvalue" of that type, will sometimes result in the compiler storing a reference to a different object.

Is there any nice way to avoid this in cases where code does not know, and shouldn't have to care, about the types of objects it's dealing with?

If one can have either of the involved operands be of a type other than Object, there is no problem. Within a generic method, variables of its generic type will also work correctly even when that type happens to be Object. Parameters which are of the generic type, however, will be treated as Object when the type is invoked using that type.

Consider the methods:

Function MakeNewWeakReference(Obj As Object) As WeakReference
   Return New WeakReference(Obj)
End Function 

Function GetWeakReferenceTargetOrDefault(WR as WeakReference, DefaultValue as Object) _
     As Object
   Dim WasTarget as Object = WR.Target
   If WasTarget IsNot Nothing Then Return WasTarget
   Return DefaultValue
End Function

One would expect that the first function would return a WeakReference that will remain alive as long as the passed-in object is. One would further expect that if the second function is given a WeakReference that is still alive, the method would return a reference that would keep it alive. Unfortunately, that assumption will fail if the reference refers to a boxed non-primitive value type. In that case, the first method will return a weak reference to a new copy of the boxed value that won't be kept alive by the original reference, and the second will return a new copy of the boxed value that won't keep the one in the weak reference alive.

If one changed the methods to be generic:

Function MakeNewWeakReference(Of T As Class)(Obj As T) As WeakReference
   Return New WeakReference(Obj)
End Function 

Function GetWeakReferenceTargetOrDefault(Of T As Class)(WR as WeakReference, _
             DefaultValue as T) As T
   Dim WasTarget as T = TryCast(WR.Target, T)
   If WasTarget IsNot Nothing Then Return WasTarget
   Return DefaultValue
End Function

that would avoid the problem within the methods, even if one were to invoke MakeNewWeakReference(Of Object) or GetWeakReferenceTargetOrDefault(Of Object). Unfortunately, if one were to try to use either method with a parameter of type Object, and either the thing being stored (in the first case) or the variable it was being stored to (in the second) was also type Object, the problem would still occur at the method's invocation or when storing its return value. If one put all of one's code into a generic class and only ever used it with a type parameter of Object, but made sure to always TryCast things of type Object to the generic type (such operation should never actually fail if the generic type happens to be Object) that would work to solve the problem, but would be rather ugly. Is there a clean way to specify that a variable should be allowed to hold a reference to any type of heap object the way Object can, but should always behave with reference semantics the way all other reference types do?

BTW, some directly-runnable test code:

Sub ObjTest(O1 As Object)
    Debug.Print("Testing type {0} (value is {1})", O1.GetType, O1)
    Dim O2 As Object
    Dim wr As New WeakReference(O1)

    O2 = O1 ' source and destination are type Object--not identity preserving

    Debug.Print("Ref-equality after assignment: {0}", O2 Is O1)
    Debug.Print("Ref-equality with itself: {0}", Object.ReferenceEquals(O1, O1))
    GC.Collect()
    Debug.Print("Weak reference still alive? {0}", wr.IsAlive)
    Debug.Print("Value was {0}", O1) ' Ensure O1 is still alive
End Sub

Sub ObjTest()
    ObjTest("Hey")
    ObjTest(1)
    ObjTest(1D)
End Sub

There's no real reason why the type of object given to the ObjTest(Object) method should have to care what kind of object it's given, but all three tests that print true with a class object like String or a primitive value type like Int32 fail with a non-primitive value type like Decimal. Is there any nice way to fix that?

Était-ce utile?

La solution

(I removed all this part because it is not applicable anymore to the new text of the question)

--- SAMPLE CODE (original question)

Public Class Form1

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Dim Obj As Object

        'Argument as Object treated as String
        Obj = "converting into string although still is an object"
        Dim outString As String = ObjToString(Obj)

        'Is, complex behaviour; equals (=), always the same
        Obj = "This one is 1"
        Dim is1 As Integer = IsVsEqual(Obj, False)  '1
        Dim equal1 As Integer = IsVsEqual(Obj, True) '1
        Obj = 1.0d 'This one 2
        Dim outIndex2 As Integer = IsVsEqual(Obj, False) '2
        Dim equal2 As Integer = IsVsEqual(Obj, True)  '1

    End Sub

    Private Function ObjToString(obj As Object) As String

        Dim nowIWantString As String = obj.ToString()
        nowIWantString = nowIWantString & " -- now string 100%"

        Return nowIWantString
    End Function

    Private Function IsVsEqual(obj As Object, tryEqual As Boolean) As Integer

        Dim obj2 As Object = obj
        Dim outIndex As Integer = 0
        If (tryEqual) Then
            If (obj2 = obj) Then
                outIndex = 1
            Else
                outIndex = 2
            End If
        Else
            If (obj2 Is obj) Then
                outIndex = 1
            Else
                outIndex = 2
            End If
        End If

    Return outIndex
End Function

End Class

--- ANSWER TO THE UPDATED QUESTION

I have to recognise that I am somehow impressed with the results you are showing. I haven't ever looked at all this in detail but the fact of having two different treatments for two different groups of types; and getting to the point of provoking ReferenceEquals(sameObject, sameObject) = False is certainly curious. Just a quick summary of your example:

Dim O1 As Object = new Object
If Not Object.ReferenceEquals(O1, O1) Then 
    'This object will show the "quirky behaviour"
End If

Making an Object Type variable going through this condition is as easy as doing O1 = 2D. You have also observed that, in these cases, the WeakReference has to be defined slightly different: wr = New WeakReference(CType(quirkyObj, ValueType)).

All this is certainly interesting (more than what I thought before reading this last question :)), although can be avoided by relying on codes like this one (or the one above):

Public Function dealWithNumObjects(a As Object) As Object

    Dim outObject As Object = a

    If (TypeOf a Is Double) Then
        'Do operations as double
    ElseIf (TypeOf a Is Decimal) Then
        'Do operations as decimal
    ElseIf (TypeOf a Is Integer) Then
        'Do operations as integer
    End If

    Return outObject
End Function

Which can be used like this:

Dim input As Object
input = 5D 'Decimal
Dim outputDecimal As Decimal = DirectCast(dealWithNumObjects(input), Decimal)
input = 5.0 'Double
Dim outputDouble As Double = DirectCast(dealWithNumObjects(input), Double)
input = 5 'Integer
Dim outputInteger As Integer = DirectCast(dealWithNumObjects(input), Integer)

This approach looks just at values and thus being quirky or not does not really matter (Decimal is quirky but neither Double nor Integer and this method works fine with all of them).

In summary: before reading your example, I would have said: avoid problems and just use objects as "temporary holders of values", convert them into the target type ASAP and deal with the target type. After reading your answer, I do recognise that your methodology seems quite solid ("pretty ugly"? Why? I like the ReferenceEquals approach but if you don't like it and just want to determine if the type is primitive you can rely on O1.GetType().IsPrimitive) and might have some applicability. I cannot come up with a better way to do things than in your example: you are able to locate the "quirky" types and to keep a WeakReference. I guess that this is the maximum you can get under these conditions.

Autres conseils

Note that VB injects calls to System.Runtime.CompilerServices.RuntimeHelpers.GetObjectValue which is documented to exactly do what you're noticing: Return a "boxed copy of obj if it is a value class; otherwise obj itself is returned."

In fact the documentation goes on to say, if the value type is immutable it returns the same object passed in. (It does not explain how immutability is determined, and it's an InternalCall :-( )

It seems Decimal, Date, and, of course, a user-defined Structure are seen as mutable by the CLR.

To actually attempt to answer your question: VB.NET does not call GetObjectValue, but rather directly uses the MSIL box command when using a generic type:

Sub Assign(Of T)(ByRef lvalue As T, ByRef rvalue As T)
  lvalue = rvalue
  If Not Object.ReferenceEquals(lvalue, rvalue) Then _
    Console.WriteLine("Ref-equality lost even generically!")
End Sub

This does NOT write anything for the types I've tried, but GetObjectValue is called at the callsite :-(

(BTW This is one case where ReferenceEquals is available when Is is not.)

The comments from the reference source:

  // GetObjectValue is intended to allow value classes to be manipulated as 'Object'
  // but have aliasing behavior of a value class.  The intent is that you would use
  // this function just before an assignment to a variable of type 'Object'.  If the
  // value being assigned is a mutable value class, then a shallow copy is returned
  // (because value classes have copy semantics), but otherwise the object itself
  // is returned.
  //
  // Note: VB calls this method when they're about to assign to an Object
  // or pass it as a parameter.  The goal is to make sure that boxed
  // value types work identical to unboxed value types - ie, they get
  // cloned when you pass them around, and are always passed by value.
  // Of course, reference types are not cloned.

I don't have further comments to make at this stage -- just consolidating into one place.

I had this link stored in my Chrome bookmarks: RuntimeHelpers.GetObjectValue why needed. I don't recall how long ago I stored it.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top