属性 与 Variable 作为 ByRef 参数

Property vs. Variable as ByRef parameter

我创建了一个实现 INotifyPropertyChanged 接口的基础 class。此 class 还包含一个通用函数 SetProperty 来设置任何 属性 的值并在必要时引发 PropertyChanged 事件。

Public Class BaseClass
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Function SetProperty(Of T)(ByRef storage As T, value As T, <CallerMemberName> Optional ByVal propertyName As String = Nothing) As Boolean
        If Object.Equals(storage, value) Then
            Return False
        End If

        storage = value
        Me.OnPropertyChanged(propertyName)
        Return True
    End Function

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional ByVal propertyName As String = Nothing)
        If String.IsNullOrEmpty(propertyName) Then
            Throw New ArgumentNullException(NameOf(propertyName))
        End If

        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

End Class

然后我有一个 class,它应该保存一些数据。为了简单起见,它只包含一个属性(在这个例子中)。

Public Class Item
    Public Property Text As String
End Class

然后我有第三个 class 继承自基础 class 并使用数据保存 class。第三个 class 应该是 WPF window.

的 ViewModel

我没有列出 RelayCommand class 的代码,因为你们可能都有自己的实现。请记住,此 class 会在执行命令时执行给定的函数。

Public Class ViewModel
    Inherits BaseClass

    Private _text1 As Item   'data holding class
    Private _text2 As String   'simple variable
    Private _testCommand As ICommand = New RelayCommand(AddressOf Me.Test)

    Public Sub New()
        _text1 = New Item
    End Sub

    Public Property Text1 As String
        Get
            Return _text1.Text
        End Get
        Set(ByVal value As String)
            Me.SetProperty(Of String)(_text1.Text, value)
        End Set
    End Property

    Public Property Text2 As String
        Get
            Return _text2
        End Get
        Set(ByVal value As String)
            Me.SetProperty(Of String)(_text2, value)
        End Set
    End Property

    Public ReadOnly Property TestCommand As ICommand
        Get
            Return _testCommand
        End Get
    End Property

    Private Sub Test()
        Me.Text1 = "Text1"
        Me.Text2 = "Text2"
    End Sub

End Class

然后我的 WPF window 使用 ViewModel class 作为其 DataContext

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding Text1}" Height="24" Width="100" />
        <TextBox Text="{Binding Text2}" Height="24" Width="100" />
        <Button Height="24" Content="Fill" Command="{Binding TestCommand}" />
    </StackPanel>
</Window>

如您所见,此 window 仅包含两个文本框和一个按钮。文本框绑定到属性 Text1Text2 并且按钮应该执行命令 TestCommand.

当执行命令时,属性 Text1Text2 都会被赋予一个值。由于这两个属性都会引发 PropertyChanged 事件,因此这些值应该显示在我的 window.

但我的 window 中只显示值 "Text2"。

属性 Text1 的值为 "Text1",但似乎此 属性 的 PropertyChanged 事件在 [=70] 之前引发=] 得到它的价值。

有什么方法可以改变我的基础 class 中的 SetProperty 函数以在 属性 获得其值后提升 PropertyChanged 吗?

感谢您的帮助。

在 C# 中,等效代码无法编译。 .NET 不习惯通过引用传递属性,原因是像 Eric Lippert 这样的人已经在其他地方研究过了(我依稀记得 Eric 在 SO 的某个地方针对 C# 解决了这个问题,但现在找不到了——笼统地说,它需要一种或另一种奇怪的解决方法,所有这些都有 C# 团队认为不可接受的缺点)。

VB 做到了,但作为一个相当奇怪的特例:我所看到的行为是我所期望的,如果它正在创建一个通过引用传递的临时变量,然后分配它方法完成后 属性 的值。这是一种解决方法(由 Eric Lippert 本人在下面的评论中确认,另请参阅@Martin Verjans 的出色回答),其副作用对于任何不知道 byref/ref 是如何实现的人来说都是违反直觉的在.NET 中。

想想看,他们不能让它正常工作,因为VB.NET和C#(和F#,和IronPython等等)必须相互兼容,因此 VB ByRef 参数必须与从 C# 代码传入的 C# ref 参数兼容。因此,任何解决方法都必须完全由调用者负责。在理智的范围内,这将它限制在调用开始之前和之后可以做的事情 returns。

这是 ECMA 335 (Common Language Infrastructure) standard 必须说的(Ctrl+F 搜索“byref”):

  • §I.8.2.1.1 托管指针和相关类型

    A 托管指针 (§I.12.1.1.2),或 byref (§I.8.6.1.3, §I. 12.4.1.5.2), 可以指向局部变量、参数、复合类型的字段或数组元素。 ...

也就是说,对于编译器来说,ByRef storage As T其实就是代码在内存中放一个值的一个存储位置的地址。它在运行时非常高效,但没有提供 getters 和 setters 的语法糖魔术范围。一个 属性 一对方法,一个 getter 和一个 setter (当然,或者只是一个或另一个)。

因此,正如您所描述的,storageSetProperty() 中获取新值,并且在 SetProperty() 完成后,_text1.Text 具有新值。但是编译器引入了一些神秘的恶作剧,导致事件的实际顺序不是您所期望的。

因此,SetProperty 无法按照您编写的方式在 Text1 中使用。我测试过的最简单的修复方法是直接在 setter 中为 Text1 调用 OnPropertyChanged()

Public Property Text1 As String
    Get
        Return _text1.Text
    End Get
    Set(ByVal value As String)
        _text1.Text = value
        Me.OnPropertyChanged()
    End Set
End Property

没有办法处理这个至少有点丑陋的问题。你可以给 Text1 一个像 Text2 那样的常规支持字段,但是你需要使它与 _text1.Text 保持同步。这比上面的 IMO 更难看,因为你必须保持两者同步,而且你在 Text1 setter 中仍然有额外的代码。

到底发生了什么?

这不起作用,因为属性的行为与字段不同。

当您执行 Me.SetProperty(Of String)(_text2, value) 时,发生的是对字段 _text2 的引用而不是其值被传递,因此 SetProperty 函数可以修改引用中的内容,并且字段被修改。

但是,当您执行 Me.SetProperty(Of String)(_text1.Text, value) 时,编译器会看到 getter 对应 属性,因此它将首先调用 Get 属性 _text1,然后将对 return 值的引用作为参数传递。因此,当您的函数 SetProperty 正在接收 ByRef 参数时,它是来自 getter、 的 return 值,而不是实际字段值 .

据我了解here,如果你说你的属性是ByRef,编译器会在你退出函数调用时自动更改字段ref ...所以这可以解释为什么在你的活动之后它正在改变...

This other blog 似乎证实了这种奇怪的行为。