vb.net奇怪的扩展方法行为?

5

我在vb.net中发现了一个小问题,但我无法解决。我有一个带有树形视图的窗体,以下是相关内容:

Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    treeTest.Nodes.Add("a")
    treeTest.Nodes(0).Test()
End Sub

Test 是一个扩展方法:

Imports System.Runtime.CompilerServices
Public Module ExtModule
    <Extension()>
    Public Sub Test(ByRef node As TreeNode)
    End Sub
End Module

如果我使用ByRef,则我的treeview看起来像这样:

http://i.imgur.com/nQk0s.png

而使用ByVal则会得到:

http://i.imgur.com/n2ZSf.png

这似乎完全相反,如果我只是发送一个引用,为什么节点会出现两次,而如果我复制它,它只会出现一次?

2
嗯,你的测试方法目前实际上什么也没做,这使得很难理解你的问题。另外,我怀疑你可能误解了ByRef的实际含义... - Jon Skeet
@Jon Skeet:只需在单个带有树形视图的表单上使用OP的代码即可。已在VS2012中测试。 - igrimpe
@JonSkeet:我也能在VS2010下重现。奇怪... - Victor Zakharov
@igrimpe:有趣的是,它似乎在Form.Load期间调用时发生,但在调用之前不会发生Load。 (我将把它转换为C#以保持清醒...) - Jon Skeet
1
@4285:不,ByVal并没有发送“对象的副本”。您需要了解任何引用类型表达式的值已经只是一个引用,而不是一个对象。请参见http://pobox.com/~skeet/csharp/parameters.html,了解C#视角下的事物,这应该有所帮助——基本上,在C#中,`ref`大致相当于`ByRef`,默认情况相当于`ByVal`。 - Jon Skeet
显示剩余6条评论
2个回答

6

好的,我已经理解了一些内容。

这与扩展方法本身关系不大。更多是关于VB如何处理 ByRef以及 TreeView.Nodes 的一些奇怪行为。

特别地,如果你将下面这个改变,你会得到完全相同的行为:

treeTest.Nodes(0).Test()

to:

ExtModule.Test(treeTest.Nodes(0))

即使您删除ExtensionAttribute,该效果仍然存在。

以下是一些C#代码,演示了相同的效果,而不使用ref参数或扩展方法:

using System.Drawing;
using System.Windows.Forms;

class Test
{
    static void Main()
    {
        TreeView tree = new TreeView { Nodes = { "a" } };
        Form form = new Form { Controls = { tree } };
        form.Load += delegate {
            TreeNode node = tree.Nodes[0];
            tree.Nodes[0] = node;
        };
        Application.Run(form);
    }
}

重要的部分在这里:
TreeNode node = tree.Nodes[0];
tree.Nodes[0] = node;

如果你的空扩展方法有一个ByRef参数,那么你的代码等同于上面的C#代码 - 因为VB通过使用临时变量来模拟“真正”的ByRef行为,然后将其赋回原始属性。

如果你的空扩展方法有一个ByVal参数,那么你的代码就只相当于:

TreeNode node = tree.Nodes[0];
// Do nothing

...并且这不会创建第二个节点。


这不是VB中的一个错误吗?我的意思是,自我赋值通常不应该引起这样的问题。 - Victor Zakharov
@Neolisk:嗯,这就是VB处理ByRef参数的方式。我个人不喜欢它,但这是设计行为。鉴于C#代码,对我来说它看起来更像是TreeNodeCollection中的一个bug。 - Jon Skeet
1
你可能是对的,顺便说一下,这是你帖子的 TL;DR 版本:treeTest.Nodes(0) = treeTest.Nodes(0) 创建了另一个节点,而它不应该这样做。因此出现了所有的问题。感谢你挖掘出来。+1 - Victor Zakharov
@Hans Passant:我认为你的链接与此问题不符。错误出现在TreeView中,可以用C#显示,正如Jon Skeet所证明的那样。 ByRef行为只是使这个错误(bug?奇怪的行为?)可见,但并不是造成它的原因。 - igrimpe
是的,这显然是一个与TreeView有关的问题,而不是与VB或扩展方法有关的问题。我的测试(请参见我的答案)证实了Jon Skeet所说的。 - Olivier Jacot-Descombes

2

我编写了一个小的VB示例并使用Reflector将其反编译为C#代码。这是我得到的结果:

treeView.Nodes.Add("a");
TreeNodeCollection VB$t_ref$S0 = treeView.Nodes;
int VB$t_i4$S0 = 0;
TreeNode VB$t_ref$S1 = VB$t_ref$S0[VB$t_i4$S0];
ref VB$t_ref$S1.Test();
VB$t_ref$S0[VB$t_i4$S0] = VB$t_ref$S1;

它无法编译。因此,我进行了另一个测试。

treeView1.Nodes.Add("a");
treeView1.Nodes[0] = treeView1.Nodes[0];
treeView1.Nodes[0] = treeView1.Nodes[0];
treeView1.Nodes[0] = treeView1.Nodes[0];

每次将节点添加到 Nodes 集合中,都会在视觉上复制该节点;但是,节点计数仍然为 1。这显然是 TreeView 行为中的错误。
注意:显然 VB 允许扩展方法的第一个参数为引用类型。这很麻烦,可能会导致许多意外行为。我的建议:不要在此处使用 ByRef

+1. 有趣的发现。我没想到 Nodes.Count 会保持为1。 - Victor Zakharov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接