网站集成支付宝教程,快速网站排名优化,免费学建筑知识网站,广州正规网站制作维护距离上一篇文章已经过去了挺久的#xff0c;很长时间没有写GH基础部分的内容了#xff0c;原因其一是本职工作太忙了#xff0c;进度也有些落后#xff0c;白天工作累成马#xff0c;回家只想躺着#xff1b;其二则是感觉GH基础系列基本上也介绍得差不多了#xff0c;电… 距离上一篇文章已经过去了挺久的很长时间没有写GH基础部分的内容了原因其一是本职工作太忙了进度也有些落后白天工作累成马回家只想躺着其二则是感觉GH基础系列基本上也介绍得差不多了电池二次开发的一些基本操作功能/外观都介绍得差不多了再加上前几期写的数据类型这基本上就囊括了所有二次开发需要用到的内容。 不过理论知识和实践总归是有一些差距的在CSDN上还是会偶尔收到私信问一些细节问题的二开爱好者们。这些问题确实是做电池二次开发的时候遇到的但它们本身可能与电池的二次开发没有关系其中有一部分是C#代码本身的编程逻辑问题还有一部分是有关于Rhino的SDK的问题另外还有一些关于Windows Form、WPF等前端框架的问题。有些问题会被反复地问到所以笔者决定还是多多将大家遇到的有共性的问题也做一系列解答方便读者在还没有遇到这些类似的问题的时候能够有那么一点点印象当真正碰到这些问题的时候能够找对解决问题的方向少走一些弯路。 这篇文章要讲的问题是有关于右键菜单的菜单项的回调函数的问题这个问题的根源是来自 C#代码编程本身也是十分具有迷惑性相信没有完整看过C#基础知识直接上手二开的爱好者们在第一次遇到这个问题的时候肯定十分地困惑。下面就来看具体问题吧。 近期经常收到一个问题 —— “为什么我添加的右键菜单项有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 ListAction();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然后在循环内部作用域使用这个额外的临时变量即可。 笔者提示此外如果循环控制变量上例中的i是引用类型不是int/double/long等值类型这个循环内部的额外临时变量则需要使用复制构造来创建新实例 —— 虽然很少出现使用非 int 类型作为循环控制变量 这样一来这个电池的工作就正常了 为什么会是这样的
细心的读者已经发现了在上面的例子中我们都使用了 匿名函数。没错问题就是出在 匿名函数 中。
匿名函数写起来十分方便但其实在它简单的语法背后编译器为我们做了许多额外的事情。其中之一就是对其中的变量做 “变量捕获 (Captures)”。
变量捕获描述的是这样一个过程 对于匿名函数的函数体中使用到的不存在于函数输入参数的变量匿名函数会捕获该变量的引用。在随后匿名函数被调用时被捕获的变量的值将会是函数调用这一瞬间的值而非匿名函数构造时的值。 上面两句话阐述了两个问题
什么样的变量会被捕获被捕获变量的行为是什么
下面看一个例子
var x 10;
Funcint, 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);}
}下次再见