【Grasshopper基础15】“右键菜单似乎不太对劲”

距离上一篇文章已经过去了挺久的,很长时间没有写GH基础部分的内容了,原因其一是本职工作太忙了,进度也有些落后,白天工作累成马,回家只想躺着;其二则是感觉GH基础系列基本上也介绍得差不多了,电池二次开发的一些基本操作(功能/外观)都介绍得差不多了,再加上前几期写的数据类型,这基本上就囊括了所有二次开发需要用到的内容。

不过,理论知识和实践总归是有一些差距的,在CSDN上还是会偶尔收到私信问一些细节问题的二开爱好者们。这些问题确实是做电池二次开发的时候遇到的,但它们本身可能与电池的二次开发没有关系:其中有一部分是C#代码本身的编程逻辑问题,还有一部分是有关于Rhino的SDK的问题,另外还有一些关于Windows Form、WPF等前端框架的问题。有些问题会被反复地问到,所以笔者决定还是多多将大家遇到的有共性的问题也做一系列解答,方便读者在还没有遇到这些类似的问题的时候,能够有那么一点点印象,当真正碰到这些问题的时候,能够找对解决问题的方向,少走一些弯路。

这篇文章要讲的问题是有关于右键菜单的菜单项的回调函数的问题,这个问题的根源是来自
C#代码编程本身,也是十分具有迷惑性,相信没有完整看过C#基础知识直接上手二开的爱好者们在第一次遇到这个问题的时候肯定十分地困惑。下面就来看具体问题吧。

近期经常收到一个问题 —— “为什么我添加的右键菜单项有Bug?” “我用了一个for循环去添加菜单项,想一次性添加x个菜单项,并在菜单被点击的时候执行 xxxx,但是结果总是不变,而且不对,这是不是GH出Bug了?”

相信有不少二开的小伙伴会做这样的一个需求:需要一个电池,这个电池需要依照情况输出若干个确定的值,具体输出哪个值需要用右键菜单来指定。类似于 ValueList 电池那样可以通过选择来输出若干个指定值其中的一个。

Snipaste_2023-08-29_15-18-27

Snipaste_2023-08-29_15-30-28

要实现这个功能,最简单直观的就是在电池中加入一个属性叫 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。

Rhino_6mowQGoDy5

?????

“这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();
    }
}

Snipaste_2023-08-29_16-09-17

甚至,在广为人知的另一门编程语言 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 类型作为循环控制变量

这样一来,这个电池的工作就正常了:

Rhino_pbpZsxoWW4

为什么会是这样的

细心的读者已经发现了,在上面的例子中,我们都使用了 匿名函数。没错,问题就是出在 匿名函数 中。

匿名函数写起来十分方便,但其实在它简单的语法背后,编译器为我们做了许多额外的事情。其中之一就是对其中的变量做 “变量捕获 (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。

Snipaste_2023-08-29_16-38-22

  • 匿名函数是: (int input) => input * x
  • 匿名函数的输入变量是 input
  • 匿名函数体是 input * x

匿名函数体中包含了两个变量,inputx。因为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");
    }
}

下次再见 🦀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/102792.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【微服务部署】08-监控与告警

文章目录 1. PrometheusOperator1.1 优势1.2 配置脚本1.3 部署脚本 2. Granfana实现监控看板2.1 Granfana核心特性2.2 部署文件 3. prometheus-net收集自定义指标3.1 组件包3.2 使用场景 目前Kubernetes中最流行的监控解决方案是使用Prometheus和AlertManager 1. PrometheusOpe…

尚硅谷SpringMVC (5-8)

五、域对象共享数据 1、使用ServletAPI向request域对象共享数据 首页&#xff1a; Controller public class TestController {RequestMapping("/")public String index(){return "index";} } <!DOCTYPE html> <html lang"en" xmln…

INDEMIND:“大+小”多机协同,实现机器人商用场景全覆盖

随着商用清洁机器人进入越来越多的场景中&#xff0c;单一的中型机器人并不能有效覆盖所有区域&#xff0c;更加细分化的产品组合正在成为新的趋势。 产品形态的“新趋势” 在商用场景中&#xff0c;目前的商用清洁机器人几乎均是中大型的产品形态&#xff0c;较大的体型意味…

性能测试(测试系列10)

目录 前言&#xff1a; 1.什么是性能测试 1.1生活中遇到的软件问题 1.2性能测试的定义 1.3性能测试和功能测试有什么区别 1.4性能的好坏的区分 1.5影响一个软件性能的因素 2.为什么要进行性能测试 3.性能测试常见的术语以及衡量指标 3.1并发 3.2用户数 3.3响应时间 …

Vulnhub: Ragnar Lothbrok: 1靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.226 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.226 作者提示修改hosts文件 目录爆破 gobuster dir -u http://armbjorn -w /usr/share/wordlists/dirbuster/directory-l…

HFSS 3维曲线导入

HFSS 3维曲线导入 简介环境参考代码使用结果 简介 如图一所示&#xff0c;CST中可以通过导入和到出由任意点组成的曲线&#xff0c;但是HFSS中貌似不能导入&#xff08;如图二所示&#xff09;&#xff0c;如果我们要将matlab的产生的曲线的点的数据导入特变麻烦&#xff0c;特…

英码深元“三位一体”AI场景化解决方案,助力多地化工园区快速实现智慧化转型!

我国是世界公认的化工大国&#xff0c;同时也是崛起中的化工强国。近年来多起重大爆炸事故暴露出我国化工园区安全问题突出&#xff0c;特别是在安全风险管控数字化转型、智能化升级方面存在明显短板和不足&#xff0c;尤其突出的痛点&#xff1a;化工园区的日常管理方式较为粗…

【DRONECAN】(三)WSL2 及 ubuntu20.04 CAN 驱动安装

【DRONECAN】&#xff08;三&#xff09;WSL2 及 ubuntu20.04 CAN 驱动安装 前言 这一篇文章主要介绍一下 WSL2 及 ubuntu20.04 CAN 驱动的安装&#xff0c;首先说一下介绍本文的目的。 大家肯定都接触过 ubuntu 系统&#xff0c;但是我们常用的操作系统都是 Windows&#x…

python unitest自动化框架

以下举一个最简单的unitest实例&#xff0c;包含备注&#xff0c;自己拉取代码运行一次就知道原理了 import unittest import osclass TestSample(unittest.TestCase):classmethoddef setUpClass(cls) -> None:print(整个测试类只执行一次)def setUp(self) -> None:prin…

睿趣科技:抖音开网店卖玩具怎么样

近年来&#xff0c;随着社交媒体平台的飞速发展&#xff0c;抖音作为一款短视频分享应用也迅速崭露头角。而在这个充满创业机遇的时代背景下&#xff0c;许多人开始探索在抖音平台上开设网店&#xff0c;尤其是卖玩具类商品&#xff0c;那么抖音开网店卖玩具究竟怎么样呢? 首先…

QT的介绍和优点,以及使用QT初步完成一个登录界面

QT介绍 QT主要用于图形化界面的开发&#xff0c;QT是基于C编写的一套界面相关的类库&#xff0c;进程线程库&#xff0c;网络编程的库&#xff0c;数据库操作的库&#xff0c;文件操作的库…QT是一个跨平台的GUI图形化界面开发工具 QT的优点 跨平台&#xff0c;具有较为完备…

leetcode做题笔记107. 二叉树的层序遍历 II

给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序遍历 。 &#xff08;即按从叶子节点所在层到根节点所在的层&#xff0c;逐层从左向右遍历&#xff09; 思路一&#xff1a;递归调换顺序 int** levelOrderBottom(struct TreeNode* root, int* returnSize, i…

技术干货 —— 手把手教你通过缓存提升 API 性能

许多开发者都希望能够彻底搞清楚 API 的工作方式&#xff0c;以及如何利用缓存 API 请求来提升业务&#xff0c;但是当这个需求进入实现阶段时&#xff0c;许多人就会发现手头并没有合适的工具和恰当的方法&#xff0c;所以我们今天就为大家做一个全面的讲解&#xff1a; ① 几…

数据结构(Java实现)-字符串常量池与通配符

字符串常量池 在Java程序中&#xff0c;类似于&#xff1a;1&#xff0c; 2&#xff0c; 3&#xff0c;3.14&#xff0c;“hello”等字面类型的常量经常频繁使用&#xff0c;为了使程序的运行速度更快、更节省内存&#xff0c;Java为8种基本数据类型和String类都提供了常量池。…

【数据结构】栈---C语言版(详解!!!)

文章目录 &#x1f438;一、栈的概念及结构&#x1f344;1、栈的概念定义&#x1f344;2、动图演示&#x1f332;入栈&#x1f332;出栈&#x1f332;整体过程 &#x1f438;二、栈的实现&#x1f438;三、数组结构栈详解&#x1f34e;创建栈的结构⭕接口1&#xff1a;定义结构…

R语言中缺失值的处理

目录 一.寻找缺失值 1.complete.cases() 2.manyNAs 二.缺失值的处理 1.直接删除 2.填补缺失值 一.寻找缺失值 1.complete.cases() #会展现缺失值 algae[!complete.cases(algae),] 2.manyNAs > manyNAs(algae) [1] 62 199 #表示第62条和第199条都有很多缺失值>m…

Systrace分析App性能学习笔记

学习Gracker Systrace系列文章&#xff0c;总结使用Systrace分析App性能的方法。推荐想通过Systrace学习Framework的同学&#xff0c;去看原文。 文章目录 概述Systrace使用流程Systrace 文件生成图形方式(不推荐)命令行方式 Systrace分析快捷键使用帧状态线程状态查看线程唤醒…

Dubbo 应用切换 ZooKeeper 注册中心实例,流量无损迁移

首先思考一个问题&#xff1a;如果 Dubbo 应用使用 ZooKeeper 作为注册中心&#xff0c;现在需要切换到新的 ZooKeeper 实例&#xff0c;如何做到流量无损&#xff1f; 本文提供解决这个问题的一种方案。 场景 有两个基于 Dubbo 的微服务应用&#xff0c;一个是服务提供者&…

RT-Thread 内核移植

内核移植 内核移植就是将RTT内核在不同的芯片架构、不同的板卡上运行起来&#xff0c;能够具备线程管理和调度&#xff0c;内存管理&#xff0c;线程间同步等功能。 移植可分为CPU架构移植和BSP&#xff08;Board support package&#xff0c;板级支持包&#xff09;移植两部…

四轴飞行器的电池研究(MatlabSimulink仿真)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…