我在.Net实体框架中有一个实现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
问题是,设置值10并调用SetProperty会导致向许多其他方法发出通知,但在SetProperty退出并返回到集合之前,该属性的值不会更改。例如,在AccreedSharesValue上设置手表显示值为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
显然,从上面的代码中,编译器从堆栈中弹出一个返回值,然后在调用从IL_0019行的SetProperty返回后将其分配给my值。同样,这似乎只发生在通用方法中。我没有用正常的方法看到这种行为。
我真的认为这应该被贴上微软漏洞的标签吗?
我真的,真的,非常感谢你从IL中了解到这一点。我不再认为这是我的代码,而是写了一些重点测试。
要复制此内容:
把这个最小的、完整的、可验证的例子放在一个新的WPF项目中(或者其他什么)。在每个End Sub
上设置断点,并检查您的locals
窗口。
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
观察
- 这不是一般性与非一般性的问题
- 传递其他对象的属性(即
myClassLevelObject.Name
)与传递Me
的属性(如myClassLevelString
)可能会出现问题 - 其他对象的属性表现一致,尽管令人沮丧,因为它们"滞后"类级变量的更新,直到函数返回
Me
的属性的行为不一致。- 设置
ByRef T
s和ByRef Object
s时,类级别变量会立即更改 - 设置
ByRef String
s时,值"滞后",直到函数返回
- 设置
*我说"可能是个问题",因为我确信这不是一个bug。我们可能只是对…了解不够。。。不管它属于哪个区域
通过.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。