某汽车零部件厂的质检工程师曾反映,生产线速度偶发性波动导致产品尺寸超差,但监控系统的图表刷新延迟超过3秒,等异常被发现时,已经有几十件废品流出。这个问题并不罕见——传统 WPF Chart 控件在高频数据场景下的性能瓶颈,是工业现场最常踩的坑之一。
换用 ScottPlot 5.x 后,同样的 50Hz 采样数据,刷新延迟从 2800ms 降至 28ms 以内,CPU 占用从 72% 降至 11%,报警响应时间缩短了 40%。
读完这篇文章,你将掌握:
生产线速度采集通常走 PLC 或编码器,50Hz 意味着每秒 50 个数据点。如果每来一个数据就触发一次 Refresh(),那就是每秒 50 次完整渲染管道——坐标轴重算 → 数据点转换 → 抗锯齿 → GPU 绘制,UI 线程直接阻塞。
csharp// ❌ 典型性能杀手,别这么写
private void OnSpeedDataReceived(double speed)
{
wpfPlot.Plot.Add.Signal(new double[] { speed }); // 每次都创建新对象
wpfPlot.Refresh(); // 每次都触发完整渲染
}
这段代码运行1小时后,内存里堆积了 18 万个废弃 Plot 对象,GC 压力把界面卡成幻灯片。
生产线速度的报警阈值不是固定值——不同产品型号、不同班次的目标速度各不相同。很多项目把阈值线硬编码进去,换产品型号时得改代码重新发布,这在工厂现场是不可接受的。
默认的白色背景 + 彩色曲线,在车间强光照射下对比度不够。操作员盯着屏幕一个班次,视觉疲劳显著。ISA-101 标准明确要求:暗色背景 + 高对比度状态色。
理解底层逻辑,优化才有方向:
Add.Signal() / Add.SignalXY() 只是注册绘图对象,不会立即渲染Refresh() 才触发完整渲染流程Signal 存储的是数组引用,修改原数组后调用 Refresh() 即可更新显示Refresh(),实现数据与渲染解耦| 要素 | 推荐规格 | 原因 |
|---|---|---|
| 背景色 | #1E1E1E / #2D2D30 | 减少视觉疲劳,适应车间光照 |
| 数据线宽 | 2-3px | 主要观察对象,需清晰可辨 |
| 报警线 | 红色实线 2px / 黄色虚线 1.5px | 符合 ISA-101 色彩语义 |
| 字号 | ≥ 12pt | 操作距离 50-80cm 下可读 |
Refresh()适用场景:单条速度曲线、更新频率 ≤ 10Hz、快速验证业务逻辑。
第一步:NuGet 安装
Install-Package ScottPlot.WPF -Version 5.1.57
第二步:XAML 布局
xml<Window x:Class="AppScottPlot8.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:AppScottPlot8"
mc:Ignorable="d"
xmlns:scottplot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<scottplot:WpfPlot x:Name="SpeedPlot" Grid.Row="0" Margin="5"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,5">
<TextBlock Text="当前速度:" FontWeight="Bold"/>
<TextBlock x:Name="CurrentSpeedText" Foreground="#E74C3C"
FontSize="16" FontWeight="Bold"/>
<TextBlock Text=" m/min" Margin="0,0,20,0"/>
<TextBlock Text="状态:"/>
<TextBlock x:Name="StatusText" FontWeight="Bold"/>
</StackPanel>
</Grid>
</Window>
第三步:后台代码
csharpusing ScottPlot;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Threading;
namespace SpeedMonitor
{
public partial class MainWindow : Window
{
private readonly List<double> _speedData = new();
private readonly List<double> _timeData = new();
private ScottPlot.Plottables.Scatter _speedPlot;
private readonly DispatcherTimer _timer;
private readonly Random _random = new();
private double _currentTime = 0;
// 报警阈值配置(支持运行时修改)
private double _warningSpeed = 85.0; // 警告上限 m/min
private double _alarmSpeed = 95.0; // 报警上限 m/min
private double _minSpeed = 60.0; // 速度下限
public MainWindow()
{
InitializeComponent();
InitializeSpeedChart();
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(100) // 10Hz 刷新
};
_timer.Tick += OnTimerTick;
_timer.Start();
}
private void InitializeSpeedChart()
{
var plt = SpeedPlot.Plot;
// 设置中文字体(必须,否则中文显示为方块)
plt.Font.Set("Microsoft YaHei");
plt.Axes.Bottom.Label.FontName = "Microsoft YaHei";
plt.Axes.Left.Label.FontName = "Microsoft YaHei";
// 工业暗色主题
plt.FigureBackground.Color = new ScottPlot.Color(30, 30, 30);
plt.DataBackground.Color = new ScottPlot.Color(45, 45, 48);
// 层次化网格(主网格存在但不喧宾夺主)
plt.Grid.MajorLineColor = ScottPlot.Colors.Gray.WithAlpha(100);
plt.Grid.MajorLineWidth = 1;
plt.Grid.MinorLineColor = ScottPlot.Colors.Gray.WithAlpha(40);
plt.Grid.MinorLineWidth = 0.5f;
// 坐标轴颜色适配暗色主题
plt.Axes.Color(ScottPlot.Color.FromHex("#C8C8C8"));
// 坐标轴标签
plt.Axes.Bottom.Label.Text = "时间(秒)";
plt.Axes.Left.Label.Text = "速度(m/min)";
plt.Title("生产线速度实时监控", size: 16);
// 初始化速度曲线(暂用空数据)
_speedData.Add(0); _timeData.Add(0);
_speedPlot = plt.Add.Scatter(_timeData.ToArray(), _speedData.ToArray());
_speedPlot.Color = ScottPlot.Color.FromHex("#00C853"); // ISA-101 正常绿
_speedPlot.LineWidth = 2.5f;
_speedPlot.MarkerSize = 0;
_speedPlot.LegendText = "线速度";
// 添加报警阈值线
AddThresholdLines(plt);
// 固定Y轴范围(省掉 AutoScale 的计算开销)
plt.Axes.SetLimitsY(40, 110);
plt.Legend.IsVisible = true;
plt.Legend.BackgroundColor = ScottPlot.Color.FromHex("#2D2D30");
plt.Legend.FontColor = ScottPlot.Color.FromHex("#C8C8C8");
SpeedPlot.Refresh();
}
private void AddThresholdLines(Plot plt)
{
// 警告上限(ISA-101 黄色)
var warningLine = plt.Add.HorizontalLine(_warningSpeed);
warningLine.Color = ScottPlot.Color.FromHex("#FFB900");
warningLine.LineWidth = 1.5f;
warningLine.LinePattern = LinePattern.Dashed;
warningLine.LegendText = $"警告上限({_warningSpeed} m/min)";
// 报警上限(ISA-101 红色)
var alarmLine = plt.Add.HorizontalLine(_alarmSpeed);
alarmLine.Color = ScottPlot.Color.FromHex("#DC322F");
alarmLine.LineWidth = 2f;
alarmLine.LinePattern = LinePattern.Solid;
alarmLine.LegendText = $"报警上限({_alarmSpeed} m/min)";
// 速度下限(蓝色虚线)
var minLine = plt.Add.HorizontalLine(_minSpeed);
minLine.Color = ScottPlot.Color.FromHex("#42A5F5");
minLine.LineWidth = 1.5f;
minLine.LinePattern = LinePattern.Dashed;
minLine.LegendText = $"速度下限({_minSpeed} m/min)";
}
private void OnTimerTick(object sender, EventArgs e)
{
// 模拟生产线速度数据(实际项目替换为 PLC/OPC UA 读取)
double speed = SimulateLineSpeed();
_currentTime += 0.1;
_speedData.Add(speed);
_timeData.Add(_currentTime);
// 滑动窗口:保留最近 300 个点(30秒)
if (_speedData.Count > 300)
{
_speedData.RemoveAt(0);
_timeData.RemoveAt(0);
}
// 更新曲线
SpeedPlot.Plot.Remove(_speedPlot);
_speedPlot = SpeedPlot.Plot.Add.Scatter(_timeData.ToArray(), _speedData.ToArray());
_speedPlot.LineWidth = 2.5f;
_speedPlot.MarkerSize = 0;
// 动态颜色:根据速度状态变化曲线颜色
_speedPlot.Color = GetStatusColor(speed);
// 滑动X轴
SpeedPlot.Plot.Axes.SetLimitsX(_currentTime - 30, _currentTime + 1);
// 更新状态栏
UpdateStatusBar(speed);
SpeedPlot.Refresh();
}
private ScottPlot.Color GetStatusColor(double speed)
{
if (speed >= _alarmSpeed || speed < _minSpeed)
return ScottPlot.Color.FromHex("#DC322F"); // 报警红
if (speed >= _warningSpeed)
return ScottPlot.Color.FromHex("#FFB900"); // 警告黄
return ScottPlot.Color.FromHex("#00C853"); // 正常绿
}
private void UpdateStatusBar(double speed)
{
CurrentSpeedText.Text = $"{speed:F1}";
if (speed >= _alarmSpeed || speed < _minSpeed)
{
StatusText.Text = "⚠ 报警";
StatusText.Foreground = System.Windows.Media.Brushes.Red;
}
else if (speed >= _warningSpeed)
{
StatusText.Text = "△ 警告";
StatusText.Foreground = System.Windows.Media.Brushes.Orange;
}
else
{
StatusText.Text = "✓ 正常";
StatusText.Foreground = System.Windows.Media.Brushes.LightGreen;
}
}
private double SimulateLineSpeed()
{
// 模拟正常波动 + 偶发异常尖峰
double baseSpeed = 75.0;
double noise = (_random.NextDouble() - 0.5) * 8;
double cycle = 5 * Math.Sin(_currentTime * 0.3);
// 10% 概率触发异常尖峰
if (_random.NextDouble() < 0.05) noise += 25;
return Math.Max(30, baseSpeed + noise + cycle);
}
protected override void OnClosed(EventArgs e)
{
_timer?.Stop();
base.OnClosed(e);
}
}
}

⚠️ 踩坑预警:方案一每次更新都调用
Remove+ 重新Add.Scatter,在高频场景下会产生 GC 压力。适合 ≤ 10Hz 的场景,更高频率请用方案二。
两年前,我在一个离散制造车间做上位机改造项目。现场有四十多台设备,PLC 品牌混杂,有西门子、三菱、台达,通信协议也各不相同。客户的需求听起来很简单:"我想在大屏上看到每台设备现在是什么状态,出了问题能报警。"
第一版我做得很粗糙——用一个定时器每隔 5 秒轮询设备,把采集到的信号直接写进数据库,前端读库展示。跑了两周,问题来了:设备断网后状态一直显示"运行中";PLC 偶发抖动,报警状态一秒钟出现又消失,历史记录里全是噪声;更麻烦的是,有台设备的"停机"和"待机"信号用的是同一个寄存器位,不同班次的操作员对状态的理解还不一样。
这些问题的本质,不是采集频率不够,也不是数据库设计不好,而是根本没有"状态机"的概念。 设备的状态不是一个孤立的值,它是一系列事件驱动下的有序迁移。没有状态机,你就永远在追噪声,永远说不清楚"这台设备到底出了什么问题、从什么时候开始的"。
这篇文章,我想把设备状态机的建模思路、表结构设计和 C# 实现完整讲一遍。
很多开发者第一反应是:状态不就是个枚举值吗,Running = 1,Idle = 2,Alarm = 3,存到数据库里不就行了?
这个想法在数据量小、设备少的时候能凑合,但它忽略了三个关键问题:
第一,状态是有来源的。 同样是"停机",是操作员主动按了停止按钮,还是设备因为过温自保护停下来的,还是通信中断导致系统判断为停机?这三种"停机"在业务上的处理方式完全不同。没有来源,维修人员就不知道该去查哪里。
第二,状态是有时序的。 设备不能从"运行"直接跳到"离线",中间一定经历了通信超时的过程。如果你不约束状态迁移的合法路径,前端展示就会出现"刚才还在运行,刷新一下变成离线了"这种让人困惑的情况。
第三,状态是有持续时间的。 报警持续了 3 秒还是 3 小时,对维护决策的意义完全不同。只存当前状态,你永远算不出设备的 OEE,也无法做任何趋势分析。
做法一:只存当前状态,不存历史。 这是最常见的坑。上线第一天 PM 就会问:"这台设备今天报警了几次?每次持续多久?" 你答不上来。
做法二:用定时轮询直接覆盖状态,不做防抖。 PLC 信号天然有抖动,尤其是继电器类型的输入点。没有防抖逻辑,报警记录里会充斥大量持续时间不足 1 秒的"幽灵报警",历史数据完全失去参考价值。
做法三:状态迁移逻辑散落在各处。 有人在采集线程里改状态,有人在 API 里改状态,有人在定时任务里改状态。三个月后没人敢动这块代码,因为不知道改了会影响哪里。
根据我的经验,设备状态机的核心是两张表 + 一个状态机服务:
t_device:设备档案,存静态信息和当前状态快照t_device_state_log:状态变更历史,每次状态迁移写一条记录,记录开始时间、结束时间、持续秒数、触发原因状态迁移逻辑全部收拢到一个 DeviceStateMachine 类里,任何地方想改设备状态,都必须通过这个类,不允许直接 UPDATE t_device SET status = xxx。
这个约束听起来有点强硬,但在项目中执行下来效果非常好——状态变更的来龙去脉一目了然,出了问题三分钟之内能定位到根因。
首先明确五种状态的业务含义:
| 状态 | 枚举值 | 业务含义 |
|---|---|---|
Running | 1 | 设备正在生产,主轴/执行机构处于工作状态 |
Idle | 2 | 设备上电待机,未在生产,等待指令 |
Alarm | 3 | 设备触发报警,需人工干预,可能仍在运行 |
Stopped | 4 | 设备主动或被动停机,执行机构停止 |
Offline | 5 | 通信中断,系统无法获取设备真实状态 |
合法的状态迁移路径如下(只有在这张图里的箭头才允许发生):
用文字描述关键路径:
Offline → Idle:通信恢复,设备重新上线,初始化为待机Idle ↔ Running:操作员启动 / 停止设备Running → Alarm:设备运行中触发报警(报警不一定停机)Alarm → Running:报警解除,恢复运行Alarm → Stopped:报警后设备自保护停机Running / Idle → Stopped:正常停机Stopped → Idle:重启完成,进入待机任意状态 → Offline:通信超时(心跳丢失超过阈值)任何不在上述路径中的状态迁移,状态机应当拒绝执行并记录警告日志,而不是静默接受。这是保证数据可信的关键约束。

信号解析层的存在非常重要。 不同品牌 PLC 的寄存器定义各不相同,把"寄存器值 → 业务事件"的映射逻辑单独抽出来,状态机本身就只需要处理标准化的 DeviceEvent,不用关心底层协议细节。
做数据展示类的桌面工具,早晚会遇到这个坎——用户要看图表。折线图、柱状图、实时曲线,这些东西Tkinter自带的Canvas画起来费劲,效果还不好看。自然而然就想到了matplotlib。但一搜怎么嵌入,发现网上的例子要么过时,要么跑起来窗口一闪而过,要么图表和界面完全对不上号。
不只是matplotlib。PIL/Pillow处理图片显示、ttkbootstrap美化界面、pyqtgraph做高性能实时曲线——这些第三方库各有各的渲染机制,跟Tkinter的主循环整合起来,坑比想象的多。
这篇文章把这几个最常用的融合场景逐一拆解,从原理到代码,每段示例都在Windows环境下验证过。
Tkinter有自己的事件循环(mainloop()),matplotlib有自己的渲染后端,PIL有自己的图像对象体系。把它们揉在一起,本质上是在让三个各自为政的系统协同工作。
问题的根源,几乎都指向同一个地方:渲染时机和主线程的控制权争夺。matplotlib默认用独立窗口显示图表(plt.show() 会阻塞主线程),PIL的 Image 对象不能直接贴到Tkinter控件上,这些都需要用特定的桥接方式绕过去。
知道了根源,解法就清晰了——用各个库提供的"嵌入模式"接口,把渲染权交还给Tkinter主循环来统一调度。
这是最高频的需求。报表工具、数据分析小程序,基本都要用到。
pythonimport tkinter as tk
from tkinter import ttk
import matplotlib
matplotlib.use('TkAgg') # 关键:必须在import pyplot之前设置后端
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import numpy as np
class StaticChartPanel(tk.Frame):
"""
静态图表面板
可作为独立组件嵌入任意Tkinter布局
"""
def __init__(self, parent, figsize=(8, 4), dpi=100, **kwargs):
super().__init__(parent, **kwargs)
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # Windows下显示中文
plt.rcParams['axes.unicode_minus'] = False
# 创建matplotlib Figure对象,不通过plt接口
# 直接用Figure而不是plt.figure(),避免全局状态污染
self.fig = Figure(figsize=figsize, dpi=dpi, facecolor='#f8f9fa')
self.ax = self.fig.add_subplot(111)
# FigureCanvasTkAgg 是连接matplotlib和Tkinter的核心桥梁
self.canvas = FigureCanvasTkAgg(self.fig, master=self)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(fill='both', expand=True)
# 可选:加上matplotlib自带的工具栏(缩放、平移、保存)
self.toolbar = NavigationToolbar2Tk(self.canvas, self)
self.toolbar.update()
# 初始化一个空图表
self._draw_empty()
def _draw_empty(self):
self.ax.set_facecolor('#ffffff')
self.ax.text(
0.5, 0.5, '暂无数据',
transform=self.ax.transAxes,
ha='center', va='center',
fontsize=14, color='#cccccc',
fontproperties='Microsoft YaHei' # Windows下中文字体
)
self.canvas.draw()
def plot_line(self, x_data, y_data, title='', xlabel='', ylabel='', color='#1976d2'):
"""绘制折线图,外部调用这个方法更新图表内容"""
self.ax.clear()
self.ax.plot(x_data, y_data, color=color, linewidth=2, marker='o', markersize=4)
# 样式设置
self.ax.set_title(title, fontproperties='Microsoft YaHei', fontsize=13, pad=10)
self.ax.set_xlabel(xlabel, fontproperties='Microsoft YaHei')
self.ax.set_ylabel(ylabel, fontproperties='Microsoft YaHei')
self.ax.grid(True, alpha=0.3, linestyle='--')
self.ax.set_facecolor('#fafafa')
self.fig.tight_layout()
# draw() 触发重绘,必须显式调用
self.canvas.draw()
def plot_bar(self, categories, values, title='', color='#42a5f5'):
"""绘制柱状图"""
self.ax.clear()
bars = self.ax.bar(categories, values, color=color, alpha=0.85, width=0.6)
# 在柱子顶部标注数值
for bar, val in zip(bars, values):
self.ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(values) * 0.01,
f'{val:.1f}',
ha='center', va='bottom', fontsize=9
)
self.ax.set_title(title, fontproperties='Microsoft YaHei', fontsize=13)
self.ax.set_facecolor('#fafafa')
self.fig.tight_layout()
self.canvas.draw()
# --- 完整使用示例 ---class ReportWindow:
def __init__(self):
self.root = tk.Tk()
self.root.title('销售数据报表')
self.root.geometry('900x600')
self._build_ui()
self._load_sample_data()
def _build_ui(self):
# 左侧控制面板
ctrl_frame = tk.Frame(self.root, width=160, bg='#eceff1')
ctrl_frame.pack(side='left', fill='y', padx=0)
ctrl_frame.pack_propagate(False)
tk.Label(ctrl_frame, text='图表类型', bg='#eceff1',
font=('微软雅黑', 11, 'bold')).pack(pady=(20, 8))
self.chart_type = tk.StringVar(value='line')
for text, val in [('折线图', 'line'), ('柱状图', 'bar')]:
tk.Radiobutton(
ctrl_frame, text=text, variable=self.chart_type,
value=val, bg='#eceff1', font=('微软雅黑', 10),
command=self._refresh_chart
).pack(anchor='w', padx=20)
# 右侧图表区域
chart_frame = tk.Frame(self.root)
chart_frame.pack(side='right', fill='both', expand=True, padx=10, pady=10)
self.chart = StaticChartPanel(chart_frame, figsize=(7, 4.5))
self.chart.pack(fill='both', expand=True)
def _load_sample_data(self):
self.months = ['1月', '2月', '3月', '4月', '5月', '6月']
self.sales = [42.3, 58.1, 51.7, 67.4, 73.2, 69.8]
self._refresh_chart()
def _refresh_chart(self):
if self.chart_type.get() == 'line':
self.chart.plot_line(
self.months, self.sales,
title='2025年上半年销售额(万元)',
xlabel='月份', ylabel='销售额'
)
else:
self.chart.plot_bar(
self.months, self.sales,
title='2025年上半年销售额(万元)'
)
def run(self):
self.root.mainloop()
if __name__ == '__main__':
ReportWindow().run()


这里有个细节很多人会踩——matplotlib.use('TkAgg') 必须在 import matplotlib.pyplot 之前调用,否则后端已经初始化完了,再改就不生效了,还不报错,只是图表显示异常。这个坑我在一个项目里排查了大半天才找到。
上周有个做自动化设备的朋友找我诉苦——他们的点胶机控制系统,每隔几分钟就会莫名丢一批传感器数据,客户投诉不断,排查了两周愣是没找到根儿。我远程看了眼代码,问题一目了然:
DataReceived回调里直接写业务逻辑,串口缓冲区早就撑爆了。
这种问题,我见过太多次了。
先说个让很多人不舒服的真相:大多数串口程序,从架构上就是错的。
典型的"意大利面条"写法长这样——
csharp// 反面教材:千万别这么写
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = port.ReadLine();
ParseProtocol(data); // 解析协议
SaveToDatabase(data); // 写数据库
UpdateUI(data); // 刷界面
}
看着没毛病对吧?但你仔细想想——DataReceived 是硬件中断驱动的回调,它不等人。你在里面做的事情越耗时,下一帧数据到来时上一帧还没处理完,操作系统的接收缓冲区就开始积压。115200 bps 的波特率,理论上每秒能塞进来 14400 字节。你的数据库写入哪怕卡了 50ms,就可能吞掉 720 字节的数据,悄无声息,没有任何报错。
这就是为什么工业现场的数据丢失问题如此难以复现——它不是必现 bug,是概率性的架构缺陷。


咱们换个思路。把整个数据流水线拆成三段:
硬件中断 → [一级缓冲] → 入队 → [二级队列] → 消费线程 → [三级聚合] → UI渲染
每一级各司其职,互不阻塞。这才是工业级串口程序该有的样子。
csharp_port = new SerialPort
{
ReadBufferSize = 65536, // 约 4.5 秒的 115200 bps 数据量
WriteBufferSize = 16384,
ReadTimeout = 500,
};
65536 这个数字不是拍脑袋来的。115200 bps ÷ 8 bits ≈ 14400 B/s,预留 1 秒延迟容量再乘以安全系数 4,取最近的 2 的幂次,刚好 65536。这一级完全由操作系统驱动管理,你的代码还没跑,数据就已经安全落地了。
在C#开发中,我们经常遇到需要在运行时动态创建类型的场景。比如从数据库读取表结构动态生成实体类,或者根据用户配置动态创建数据模型。System.Reflection.Emit命名空间为我们提供了强大的动态类型创建能力。本文将通过详细的示例,带你掌握这项高级技术。
Reflection.Emit是.NET Framework提供的一组API,允许我们在运行时动态创建程序集、模块、类型和方法。它的核心优势包括:
c#// 程序集构建器 - 最顶层容器
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName, AssemblyBuilderAccess.Run);
// 模块构建器 - 包含类型定义
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
// 类型构建器 - 定义具体类型
TypeBuilder typeBuilder = moduleBuilder.DefineType("ClassName", TypeAttributes.Public);
IL(Intermediate Language)是.NET的中间语言,我们需要通过ILGenerator来生成IL指令:
c#// 获取IL生成器
ILGenerator il = methodBuilder.GetILGenerator();
// 常用IL指令
il.Emit(OpCodes.Ldarg_0); // 加载第一个参数(this)
il.Emit(OpCodes.Ldfld, fieldBuilder); // 加载字段值
il.Emit(OpCodes.Ret); // 返回