近期经常收到一个问题 —— “为什么我添加的右键菜单项有Bug?” “我用了一个for循环去添加菜单项,想一次性添加x个菜单项,并在菜单被点击的时候执行 xxxx,但是结果总是不变,而且不对,这是不是GH出Bug了?”
相信有不少二开的小伙伴会做这样的一个需求:需要一个电池,这个电池需要依照情况输出若干个确定的值,具体输出哪个值需要用右键菜单来指定。类似于 ValueList
电池那样可以通过选择来输出若干个指定值其中的一个。
要实现这个功能,最简单直观的就是在电池中加入一个属性叫 ComponentPropertyValue
,然后在右键菜单中改变它,并调用 ExpireSolution
,同时,SolveInstance
函数中依照这个属性来赋值:
private int ComponentPropertyValue { get; set; }
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
menu.Items.Add(new ToolStripMenuItem("1", null,
(o, e) => { ComponentPropertyValue = 1; this.ExpireSolution(true); }));
menu.Items.Add(new ToolStripMenuItem("2", null,
(o, e) => { ComponentPropertyValue = 2; this.ExpireSolution(true); }));
menu.Items.Add(new ToolStripMenuItem("3", null,
(o, e) => { ComponentPropertyValue = 3; this.ExpireSolution(true); }));
menu.Items.Add(new ToolStripMenuItem("4", null,
(o, e) => { ComponentPropertyValue = 4; this.ExpireSolution(true); }));
menu.Items.Add(new ToolStripMenuItem("5", null,
(o, e) => { ComponentPropertyValue = 5; this.ExpireSolution(true); }));
}
protected override void SolveInstance(IGH_DataAccess DA)
{
// 这里为了举例方便设置为该数值的平方
// 实际可能会有较为复杂的运算逻辑
DA.SetData(0, ComponentPropertyValue * ComponentPropertyValue);
}
显然,作为一个写过一段时间代码的正常人,应该能想到使用一个 for
循环来改写函数 AppendAdditionalComponentMenuItems
中的代码:
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
for (var i = 1; i < 6; i++)
{
// 将对应的列表项的文字和赋值语句换成 i 即可
menu.Items.Add(new ToolStripMenuItem($"{i}", null,
(o, e) => { ComponentPropertyValue = i; this.ExpireSolution(true); }));
}
}
但是这个时候运行代码就会出现一个现象,无论选哪个,最后出来的结果都会是36。
?????
“这GH是出Bug了!”
其实不然,即便是一个控制台应用程序,下面这段代码也会只输出一个值:
static void Main()
{
var list = new List<Action>();
for (var x = 0; x < 10; x++)
{
list.Add(() => Console.WriteLine(x));
}
foreach (var action in list)
{
action();
}
}
甚至,在广为人知的另一门编程语言 Python
中,以及其他许多编程语言中,都会有这种情况。(在 Python
中,这种现象称之为“闭包延时绑定”,可自行搜索Python延时绑定关键词来查询相关底层知识)
我们先说怎么解决这个问题,再来谈这个问题是什么原因导致的。
如何解决
解决的方法很简单,只需要额外增加一个局部变量即可:
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
for (var i = 1; i < 6; i++)
{
var j = i; // 增加一个额外的变量j,令其值等于i,然后在lambda函数中使用j即可
menu.Items.Add(new ToolStripMenuItem($"{j}", null,
(o, e) => { ComponentPropertyValue = j; this.ExpireSolution(true); }));
}
}
简而言之,就是在 for
循环内部作用域,创建一个额外的临时变量(上例中的j
),令其等于循环控制变量(上例中的i
),然后在循环内部作用域使用这个额外的临时变量即可。
这样一来,这个电池的工作就正常了:
为什么会是这样的
细心的读者已经发现了,在上面的例子中,我们都使用了 匿名函数。没错,问题就是出在 匿名函数 中。
匿名函数写起来十分方便,但其实在它简单的语法背后,编译器为我们做了许多额外的事情。其中之一就是对其中的变量做 “变量捕获 (Captures)”。
变量捕获描述的是这样一个过程:
上面两句话阐述了两个问题:
- 什么样的变量会被捕获
- 被捕获变量的行为是什么
下面看一个例子:
var x = 10;
Func<int, int> lambda = (int input) => input * x;
x += 10;
var result = lambda(5);
Console.WriteLine(result);
我们使用 Visual Studio 中的 C# Interactive 来执行上面的代码,可以看到,lambda(5)
的结果是100,而不是50。
- 匿名函数是:
(int input) => input * x
- 匿名函数的输入变量是
input
- 匿名函数体是
input * x
匿名函数体中包含了两个变量,input
和x
。因为input
是匿名函数的输入变量,所以它不是被捕获的变量。x
不是匿名函数的输入变量,所以它将会被匿名函数捕获。
在我们使用lambda(5)
调用匿名函数时,被捕获变量x
的值是匿名函数函数调用时的值(20,因为在调用前我们使用x += 10
改变了x
),而非匿名函数被定义的时候的值(10)。因此,最后的结果是 5 * 20 = 100
。
通过这个例子,我们可以看出:
因此,在的Grasshopper电池菜单项的问题上,我们构造菜单项时,是嵌套在 for
循环中,构造匿名函数时,由于循环变量i
并不是匿名函数的输入参数,所以它将会被捕获!我们通过 for
循环构造了5个菜单项,但他们的回调函数捕获的是同一个循环变量 i
。
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
for (var i = 1; i < 6; i++)
{
menu.Items.Add(new ToolStripMenuItem($"{i}", null,
(o, e) => { ComponentPropertyValue = i; this.ExpireSolution(true); }));
}
}
进一步的,在菜单被点击的时候,回调函数被触发,此时匿名函数内的i
的值会是匿名函数被调用时候的值(此时,构造菜单项的 for
循环早已完成,因此循环变量停留在了最后一次 for
循环的值6)。这也是为什么我们在之前出现,任何一个菜单项点击都是6的结果的原因。
老规矩,上代码
using System;
using System.Windows.Forms;
using Grasshopper.Kernel;
namespace GrasshopperPluginExample01
{
public class ProvideValues : GH_Component
{
public ProvideValues() : base("ProvideValues", "Val",
"ProvideValues",
"Params", "DigitalCrab")
{
}
private int ComponentPropertyValue;
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager) { }
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddIntegerParameter("Out", "O", "output value", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
DA.SetData(0, ComponentPropertyValue * ComponentPropertyValue);
}
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
//menu.Items.Add(new ToolStripMenuItem("1", null, (o, e) => { ComponentPropertyValue = 1; this.ExpireSolution(true); }));
//menu.Items.Add(new ToolStripMenuItem("2", null, (o, e) => { ComponentPropertyValue = 2; this.ExpireSolution(true); }));
//menu.Items.Add(new ToolStripMenuItem("3", null, (o, e) => { ComponentPropertyValue = 3; this.ExpireSolution(true); }));
//menu.Items.Add(new ToolStripMenuItem("4", null, (o, e) => { ComponentPropertyValue = 4; this.ExpireSolution(true); }));
//menu.Items.Add(new ToolStripMenuItem("5", null, (o, e) => { ComponentPropertyValue = 5; this.ExpireSolution(true); }));
for (var i = 1; i < 6; ++i)
{
var j = i;
menu.Items.Add(new ToolStripMenuItem($"{j}", null, (o, e) => { ComponentPropertyValue = j; this.ExpireSolution(true); }));
}
}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => new("7805627F-6422-457D-969D-C5E19B124D87");
}
}
下次再见 🦀