VB 传递参数值 ByRef 在方法退出之前不会更改值?

VB passing Parameter Value ByRef does not change value until Method exits?

我在 .Net Entity Framework 中有一个 Class 实现了 INotifyPropertyChanged。我发现了一个有趣的问题,我的 属性 更改 setter 触发了一个通知事件,但是更改的值在 setter 退出之前是不可见的。

我执行了以下操作来测试 属性 值是否已更改,设置新值然后将更改通知相关方:

Public Event PropertyChanged(sender As Object, 
         e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

'' This method is called by the Set accessor of each property. 
Private Sub NotifyPropertyChanged(PropertyName As String)
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub

''' <summary>
''' Sets the specified property to a value and raises an event if the property has changed.
''' </summary>
''' <typeparam name="T">Type of the destination field</typeparam>
''' <param name="field">Destination Field to check/update</param>
''' <param name="value">Field is to be set to this value, if it has not changed.</param>
''' <param name="PropertyName">Optional property name - filled in by compiler if left blank.</param>
''' <remarks></remarks>
Protected Function SetProperty(Of T)(ByRef field As T, value As T, _
                                     PropertyName As String, _
                                     Optional SupressEvent As Boolean = False) As Boolean
    If Not EqualityComparer(Of T).Default.Equals(field, value) Then
        field = value
        If Not SupressEvent Then NotifyPropertyChanged(PropertyName)
        Return True
    End If
    Return False
End Function

属性本身调用SetProperty如下:

Public Property AccruedShares As Decimal
    Get
        Return Me.AccruedSharesValue
    End Get
    Set(value As Decimal)
        SetProperty(Of Decimal)(Me.AccruedSharesValue, value, "AccruedShares", Loading)
    End Set
End Property

问题是设置一个值 say 10 并调用 SetProperty 会导致通知许多其他方法,但 属性 的值不会改变,直到 SetProperty 退出并且 returns回集合。例如在 AccruedSharesValue 上设置监视显示值 0。当事件触发时,所有其他方法看到值 0,并且直到代码步出 SetProperty 方法后才更改为 10。这与我通过 ref 工作传递值的看法完全相反。 ByRef 应该立即更改传递的变量的值,而不是在方法退出之后。

有人知道为什么会这样吗?

我拿了一小段代码:

    Public Property Population As Integer
        Get
            Return Me.PopulationValue
        End Get
        Set(value As Integer)
            SetProperty(Of Integer)(Me.PopulationValue, value, "Population")
        End Set
    End Property

然后查看拆机清单:

.method public specialname instance void 
    set_Population(int32 'value') cil managed
{
  // Code size       31 (0x1f)
  .maxstack  5
  .locals init ([0] int32 VB$t_i4$S0)
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  callvirt   instance int32 Armada.DataModels.City::get_PopulationValue()
  IL_0007:  stloc.0
  IL_0008:  ldloca.s   VB$t_i4$S0
  IL_000a:  ldarg.1
  IL_000b:  ldstr      "Population"
  IL_0010:  ldc.i4.0
  IL_0011:  callvirt   instance bool Armada.DataModels.City::SetProperty<int32>(!!0&,
                                                                                !!0,
                                                                                string,
                                                                                bool)
  IL_0016:  pop
  IL_0017:  ldarg.0
  IL_0018:  ldloc.0
  IL_0019:  callvirt   instance void Armada.DataModels.City::set_PopulationValue(int32)
  IL_001e:  ret
} // end of method City::set_Population

从上面的代码可以看出,编译器从堆栈中弹出一个 return 值,然后将其分配给 my 值,在第 [=20 行的 SetProperty 调用 returns 之后=].同样,这似乎只发生在通用方法中。我没有用普通方法看到这种行为。

我真的认为这应该被标记为 Microsoft 错误?

我真的,真的真的感谢你从 IL 中弄清楚这一点。我能够停止拔头发以为这是我的代码并编写一些重点测试。

要重现此内容:

将此 minimal, complete, and verifiable 示例放入新的 WPF 项目(或其他项目)中。在每个 End Sub 上放置断点并检查你的 locals window.

Class ObjectWithName
    Public Property Name As String
End Class

Class MainWindow
    Private myClassLevelString = "Intitial"
    Private myClassLevelObject As New ObjectWithName() With {.Name = "Initial"}

    Public Sub New()
        InitializeComponent()

        'Group 1
        Generic(myClassLevelString, "Generic")
        NonGenericStrings(myClassLevelString, "NonGenericStrings")
        NonGenericObjects(myClassLevelString, "NonGenericObjects")

        'Group 2
        Generic(myClassLevelObject.Name, "Generic")
        NonGenericStrings(myClassLevelObject.Name, "NonGenericStrings")
        NonGenericObjects(myClassLevelObject.Name, "NonGenericObjects")
    End Sub

    Private Sub Generic(Of T)(ByRef p1 As T, p2 As T)
        p1 = p2
        Dim Group1 = myClassLevelString
        Dim Group2 = myClassLevelObject.Name
        ' At this point:
        ' p1 = "Generic"
        ' Group1 = "Generic"
        ' Group2 = "Initial"
    End Sub

    Private Sub NonGenericStrings(ByRef p1 As String, p2 As String)
        p1 = p2
        Dim Group1 = myClassLevelString
        Dim Group2 = myClassLevelObject.Name
        ' At this point:
        ' p1 = "NonGenericStrings"
        ' Group1 = "Generic" <-- This is NOT a typo!
        ' Group2 = "Generic"
    End Sub

    Private Sub NonGenericObjects(ByRef p1 As Object, p2 As Object)
        p1 = p2
        Dim Group1 = myClassLevelString
        Dim Group2 = myClassLevelObject.Name
        ' At this point:
        ' p1 = "NonGenericObjects"
        ' Group1 = "NonGenericObjects" <-- This is not a typo either.
        ' Group2 = "NonGenericStrings"
    End Sub
End Class

观察结果

  1. 这不是一般性与非一般性问题。
  2. 传递其他对象的属性(即 myClassLevelObject.Name)与传递 Me 的属性(即 myClassLevelString)可能是一个问题*。
  3. 通过 "lagging" class 级变量的更新,直到函数 returns.
  4. ,其他对象的属性表现一致,尽管令人沮丧。
  5. Me' 属性的行为 不一致
    • 当设置ByRef Ts和ByRef Objects时,class级变量立即改变。
    • 当设置ByRef Strings时,函数的值"lags"直到return。

* 我说 "might be a problem" 因为我 确定 这不是错误。我们可能只是不太了解......无论这属于什么领域!

.Net 4.6 再次确认,此行为仍然存在。

我正在将一些遗留的 VB6 源代码转换为 .Net。原代码处处使用ByRef(无意改变这个变量,老程序员就是喜欢用ByRef)。

例如,在 VB6 中,下面的工作,

Function GetNewString(ByRef old As String) As String
    return old & "NEW"
End Function

Dim rs As ADO.RecordSet
Call GetNewString(rs.Fields("column1").Value)

虽然它是愚蠢的代码,但它有效。

但是,如果您将所有内容都转换为 VB.Net,并且仍然使用上面的代码,除了记录集(和数据库)被更新之外,它仍然有效!好像

' To repalce ADO.RecordSet (internally uses DataTable / SqlConnection / OdbcConnection)
Dim rs As MyRecordSet 
GetNewString(rs.Fields("column1").Value) ' Database will be updated once here!

Function GetNewString(ByRef old As String) As String
    ' This is what .Net added automatically.
    ' It means rs.Fields("column1").Value = old so the database will be updated!
    old = old  
    return old & "NEW"
End Function

我必须手动将所有此类 ByRef 更改为 ByVal。