C# 特有语法糖
本页只介绍 DarraEtherCAT C# SDK 独有的、利用 .NET 语言特性叠加的便捷写法。它们 不是新功能,只是把已有 API 包装成更符合 C# 习惯的形式(元组解构、record struct、Range 索引、LINQ、pattern matching、async/await、
IObservable<T>、Span<byte>等)。其他语言(C/C++/Java/Python/Rust)的 SDK 没有这些写法 —— 因此本节单独成页,不与通用 API 混在一起。
共存而非替代 —— 旧 API 全部保留。语法糖只是另一种写法。性能敏感场景(PDO 周期内)建议优先用语法糖中的
Span版本,其他场景按可读性选择。
命名空间 —— 全部位于
DarraEtherCAT_Master,主类型DarraEtherCAT及DarraEtherCAT.Slave已带语法糖扩展,引入命名空间即可使用。
一、元组解构 (Deconstruct)
C# 7 起支持的元组解构。DarraEtherCAT.Slave 提供三个 Deconstruct 重载,让用户一行拿全身份/运行态字段。
取 EEPROM 身份 3 元组
| 写法 | 调用 |
|---|---|
| 标准 API | uint v = slave.VendorId; uint p = slave.ProductId; uint r = slave.RevId; |
| 语法糖 | var (vendor, product, revision) = slave; |
foreach (var slave in master.Slaves)
{
var (vendor, product, revision) = slave;
Console.WriteLine($"VID=0x{vendor:X8} PID=0x{product:X8} REV=0x{revision:X8}");
}
取完整 4 元组(含 Serial)
var (vendor, product, revision, serial) = slave;
取运行态视图(索引/名字/状态)
var (idx, name, state) = slave;
if (state != EcState.OP) Console.WriteLine($"#{idx} {name} 不在 OP");
何时用
- 诊断/日志循环:避免重复写
slave.VendorId、slave.ProductId多个属性访问。 - switch / pattern matching:与
is var (v, p, _)配合天然契合。
二、不可变身份记录 (record struct + with)
SlaveIdentityKey 是 readonly record struct,面向应用层,不与 DLL 内存对齐结构 SlaveIdentity 冲突。
| 特性 | record struct |
|---|---|
| 结构体相等性 | 字段相等 ⇒ Equals=true(编译器生成) |
with 表达式 | 派生新值仅改一个字段 |
| 可作为 Dictionary Key | 自动 GetHashCode |
| pattern matching | 字段属性可匹配 |
// 取键并入字典
var key = slave.GetIdentityKey();
if (_configCache.TryGetValue(key, out var cfg)) { ApplyCfg(cfg); }
// with 派生 fuzzy 查表 key (相同 vendor/product, 不同 revision)
var fuzzyKey = key with { Revision = 0 };
// pattern matching
string family = key switch
{
{ VendorId: 0x00000002 } => "Beckhoff",
{ VendorId: 0x000000ABu } => "Copley",
_ => "Unknown"
};
// 仅按 Vendor+Product 比较 (忽略 Revision/Serial)
if (key.MatchesType(otherKey)) { ... }
何时用
- 缓存查表:把"上次扫描到的从站"和"配置"按身份键挂在
Dictionary<SlaveIdentityKey, ...>里。 - 配置 fallback:
with { Revision = 0 }一行写出"忽略 revision"的查表 key。 - 不要替代
SlaveIdentity—— 那个仍用于 P/Invoke 内存对齐。
三、Range 索引和 LINQ 风格筛选
C# 8 起支持 Range 和 Index。我们把"挑出符合条件的从站"全做成 LINQ 扩展。
切片
| 写法 | 调用 |
|---|---|
| 标准 API | for (int i = 1; i < 4; i++) Process(master.Slaves[i]); |
| 语法糖 (ns2.0) | foreach (var s in master.Slaves.Slice(startIndex: 1, count: 3)) Process(s); |
| 语法糖 (.NET Core 3+) | foreach (var s in master.Slaves.Slice(1..4)) Process(s); |
netstandard 2.0 不带
System.Range,所以Slice(start, count)是默认重载;.NET Core 3+通过#if提供额外的Slice(Range)重载。
LINQ 链式过滤
// 所有 CoE 从站
foreach (var s in master.Slaves.WithProtocol(MailboxType.CANopenOverEtherCAT))
Console.WriteLine(s.Name);
// OP 状态 + 组 1 + 在线
var online = master.Slaves
.OnlineOnly()
.WithGroup(1)
.InState(EcState.OP)
.ToList();
// 配 PdoSpan 一起用 (清零所有 OP 从站的输出)
foreach (var s in master.Slaves.InState(EcState.OP))
s.OutputSpan().Clear();
// 调试输出全部身份
foreach (string line in master.Slaves.ToIdentityStrings())
log.Info(line);
提供的扩展
| 扩展 | 作用 |
|---|---|
WithProtocol(MailboxType) | 邮箱协议位匹配(位 OR 逻辑) |
InState(EcState) | 仅指定 EtherCAT 状态 |
WithVendor(uint) / WithProduct(uint) | 按 ID 过滤 |
WithGroup(byte) | 按 0-7 组号过滤 |
OnlineOnly() | 排除 IsLost |
WithDC() | 仅有 DC 同步 |
Slice(int start, int count) | 索引切片(ns2.0 通用) |
Slice(Range) | Range 切片(.NET Core 3+) |
ToIdentityStrings() | 调试用,逐行打印 ID |
何时用
- 手写
foreach + if链 三层以上 → 改成一句 LINQ。 - 不在 PDO 周期内调用 —— LINQ 内部 yield 不分配大对象,但还是会有迭代器开销,1ms 以下 RT 路径建议用普通 for 循环。
四、越界安全索引
Slaves[n] 越界抛 IndexOutOfRangeException。启动早期/热插拔时配置可能不一致,建议改用 nullable 风格。
// 标准 API (越界抛异常)
var s = master.Slaves[2];
// 语法糖 (越界返回 null)
var s = master.Slaves.GetOrNull(2);
if (s is { } slave) slave.SetState(EcState.OP);
// TryGet 风格
if (master.Slaves.TryGet(idx, out var slave)) { ... }
// 1-based EtherCAT SlaveNum 友好访问
var s5 = master.GetSlaveBySlaveNum(5); // 找 SlaveNum=5
// 按 ConfigAddr 站地址查
var slv = master.GetSlaveByConfigAddr(0x1003);
何时用
- 索引来源是用户配置 / 网络扫描 / 远端命令 —— 拿不准长度时永远用
GetOrNull。 - 把 EtherCAT 1-based SlaveNum 直接传过来 —— 用
GetSlaveBySlaveNum。
五、Pattern Matching: AL Status 分类
ALErrorClassifier.Classify(...) 是静态方法。我们把它包装成 EcALState 的扩展方法 + 谓词,配合 switch 表达式直接用。
// 标准 API
if (ALErrorClassifier.Classify(slave.ErrorCode) == ALErrorCategory.Transient)
Retry();
// 语法糖
if (slave.ErrorCode.IsTransient()) Retry();
// switch 表达式
string action = slave.ErrorCode.Category() switch
{
ALErrorCategory.None => "正常",
ALErrorCategory.Transient => "重试",
ALErrorCategory.Configuration => "重配置",
ALErrorCategory.Hardware => "联系用户",
_ => "未知"
};
// 邮箱协议错误识别
if (slave.ErrorCode.IsMailboxError()) HandleMailboxFailure();
// DC 错误识别
if (slave.ErrorCode.IsDcError()) ResetDcSync();
// 是否需要降级到 PreOp
if (slave.ErrorCode.NeedsDowngradeTo(EcState.PreOp))
slave.SetState(EcState.PreOp);
// UI 直接显示中文标签
label.Text = slave.ErrorCode.DescribeCategory();
提供的扩展
| 扩展 | 作用 |
|---|---|
Category() | 等价 ALErrorClassifier.Classify |
IsNone() / IsTransient() / IsConfiguration() / IsHardware() | 谓词 |
IsMailboxError() | 0x0041-0x004F 区间 |
IsDcError() | 0x0030-0x0037 区间 |
NeedsDowngradeTo(EcState) | 0x0021/0x0022/0x0023 |
DescribeCategory() | 中文短描述(UI 用) |
何时用
- 状态机/异常处理:
if链式判断三个以上EcALState值 → switch 表达式。 - UI 标签 / 日志显示 → 直接
DescribeCategory()。
六、async/await 状态切换
SetState / WaitForState 是同步阻塞 API,在 GUI/Service 中容易卡线程。语法糖用 Task.Run 推到线程池,支持 CancellationToken。
// 标准 API (同步阻塞 UI)
master.SetState(EcState.OP);
master.WaitForState(EcState.OP, 10000);
// 语法糖 (UI 不卡)
await master.SetStateAsync(EcState.OP, ct);
await master.WaitForStateAsync(EcState.OP, TimeSpan.FromSeconds(10), ct);
// 一行启动 + 等待 OP, 失败抛异常含错误信息
try
{
await master.GoToOpAsync(TimeSpan.FromSeconds(10), ct);
log.Info("已进入 OP");
}
catch (TimeoutException ex) { log.Error(ex.Message); }
catch (InvalidOperationException ex) { log.Error(ex.Message); }
何时用
- WinForms / WPF / Avalonia / MAUI 等 UI 程序
- ASP.NET Core / 其它服务,希望取消令牌驱动
七、异步资源管理 (CloseAsync / DisposeAsync)
Close() / Dispose() 是同步阻塞(停 PDO + 切 PreOp + Join 线程,秒级)。语法糖提供异步版本:
// 标准 API
master.Close();
master.Dispose();
// 语法糖 (UI 不卡)
await master.CloseAsync(ct);
await master.DisposeAsync(ct);
// 多主站并发释放
await new[] { masterA, masterB, masterC }.DisposeAllAsync();
关于 await using / IAsyncDisposable
netstandard 2.0 不自带 IAsyncDisposable,要用 await using,需要在你自己的项目引入 NuGet 包:
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
然后写包装类:
public sealed class AsyncMaster : IAsyncDisposable
{
public DarraEtherCAT M { get; }
public AsyncMaster(DarraEtherCAT m) { M = m; }
public ValueTask DisposeAsync() => new ValueTask(M.DisposeAsync());
}
await using var m = new AsyncMaster(master);
SDK 主类不直接实现 IAsyncDisposable,避免给所有用户强加 NuGet 依赖。
八、IObservable<T> 事件流
把 event 包装成 IObservable<T>,让用户用 using 自动反订阅,并可链式接 System.Reactive 操作符。
// 标准 API (容易忘记反订阅)
EventHandler<StateChangedEventArgs> h = (s, e) => Log(e.NewState);
master.Events.StateChanged += h;
// ... 别忘了:
master.Events.StateChanged -= h;
// 语法糖 (using 自动反订阅)
using var sub = master.StateChangeStream().Subscribe(
e => Log($"{e.OldState}->{e.NewState}"));
// 从站状态流 (元组事件)
using var sub2 = slave.StateChangeStream().Subscribe(
t => Log($"{t.OldState}->{t.NewState}"));
// CoE 0x10F3 诊断流 (订阅时挂 DiagnosticMessageReceived 事件)
using var sub3 = slave.CoE.DiagnosticStream().Subscribe(
msg => Log($"诊断 0x{msg.DiagCode:X8}"));
// CoE 紧急事件流
using var sub4 = slave.EmergencyStream().Subscribe(
e => Log($"EMCY 0x{e.Code:X4}"));
配合 System.Reactive (可选 NuGet)
引入 System.Reactive 后可以链式过滤/节流/合并:
master.StateChangeStream()
.Where(e => e.NewState == EcState.OP)
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(_ => Beep());
不引 Rx 也能用 —— SDK 自带
Subscribe(Action<T>)扩展 + 极简IObservable<T>实现,零额外依赖。
何时用
- 多个订阅者需要统一管理生命周期 → 把所有
IDisposable放进CompositeDisposable(Rx) 或List<IDisposable>一起 dispose。 - 想用 LINQ-to-Events 操作符(Where/Throttle/Buffer/Merge)→ 引入
System.ReactiveNuGet。
九、Span<byte> 零拷贝 PDO 数据视图
slave.PDO.Inputs / Outputs 是 byte[] 属性,每次访问都分配新数组 + memcpy。在 PDO 周期内(125μs - 1ms)频繁访问会增加 GC 压力。
语法糖直接返回 Span<byte>,零分配、直读 IOmap。
// 标准 API (每次 get 都 new byte[] + memcpy)
byte[] inputs = slave.PDO.Inputs;
int sum = 0;
for (int i = 0; i < inputs.Length; i++) sum += inputs[i];
// 语法糖 (零分配, 直读 IOmap)
var span = slave.InputSpan();
int sum = 0;
for (int i = 0; i < span.Length; i++) sum += span[i];
// 写输出, 直接对 IOmap 写, 不要中间 buffer
var outSpan = slave.OutputSpan();
outSpan[0] = 0xFF;
outSpan[1] = 0x55;
// MDP 模块化设备分模块切片 (零拷贝)
var module1Inputs = slave.InputSlice(0, 8); // 模块 1 输入 8 字节
var module2Outputs = slave.OutputSlice(8, 4); // 模块 2 输出 4 字节
// 拷到/从 stackalloc 不分配
Span<byte> stack = stackalloc byte[16];
slave.CopyInputsTo(stack);
slave.CopyToOutputs(stack);
提供的扩展
| 扩展 | 返回 | 作用 |
|---|---|---|
InputSpan() | ReadOnlySpan<byte> | 输入 PDO 只读视图 |
OutputSpan() | Span<byte> | 输出 PDO 可写视图 |
InputSlice(offset, length) | ReadOnlySpan<byte> | 输入切片 (MDP) |
OutputSlice(offset, length) | Span<byte> | 输出切片 (MDP) |
CopyInputsTo(Span<byte>) | int | 拷到指定 Span (零分配) |
CopyToOutputs(ReadOnlySpan<byte>) | int | 从 Span 拷到 IOmap |
重要警告
- Span 生命周期 = 当前 PDO 周期。状态切换(SafeOp ↔ OP)后 IOmap 重映射,旧 Span 全部失效。
- 不可越过 await / 跨线程。Span 是栈类型,
async方法里跨 await 用 Span 会编译报错。 - 超出
Ibytes/Obytes索引未定义行为(与 IOmap 物理边界对齐)。
何时用
- PDO 周期内热路径 —— GC 优化首选。
- MDP 模块化设备 —— 切片读写避免重复 offset 计算。
- 不要在配置/启动阶段用 —— 那种场景用
slave.PDO.Inputs字节数组更直观。
十、诊断消息 yield 流 (CoE 0x10F3)
把"轮询 → 读取 → 确认"的 while 循环变成 foreach yield 流。
// 标准 API (手写循环)
while (running)
{
if (slave.CoE.PollHasNewDiagnostic())
{
var msgs = slave.CoE.ReadDiagnosticMessages();
foreach (var m in msgs) Process(m);
if (msgs.Count > 0)
slave.CoE.AcknowledgeDiagnostic(msgs[msgs.Count - 1].SubIndex);
}
Thread.Sleep(100);
}
// 语法糖 (yield)
foreach (var msg in slave.CoE.PollDiagnosticMessages(
pollIntervalMs: 100,
autoAcknowledge: true,
stopWhen: () => !running))
{
Process(msg);
}
// CancellationToken 驱动
foreach (var msg in slave.CoE.PollDiagnosticMessages(ct, pollIntervalMs: 100))
Process(msg);
关于 IAsyncEnumerable<T> / await foreach
netstandard 2.0 不自带 IAsyncEnumerable<T>,要用 await foreach 需要:
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
并自行包装:
public static async IAsyncEnumerable<DiagnosticMessage> PollAsync(
this DarraEtherCAT.Slave.CoEInstance coe,
[EnumeratorCancellation] CancellationToken ct, int interval = 100)
{
foreach (var m in coe.PollDiagnosticMessages(ct, interval))
{
yield return m;
await Task.Yield();
}
}
SDK 本体只提供同步 IEnumerable 版本以保持零额外依赖。
何时用
- 后台诊断采集线程 —— 写
foreach比while直观。 - 配 LINQ:
PollDiagnosticMessages(ct).Where(m => m.DiagCode > 0xF000).Take(10)。
文件清单
所有语法糖位于 CSharp/Master/Sugar/,独立 .cs 文件,不修改任何已有 SDK 代码:
| 文件 | 内容 |
|---|---|
SlaveDeconstruct.cs | Slave.Deconstruct 三个重载(partial 追加) |
SlaveIdentityKey.cs | readonly record struct SlaveIdentityKey + slave.GetIdentityKey() |
SlaveLinqExtensions.cs | WithProtocol/InState/WithVendor/WithGroup/OnlineOnly/Slice/... |
SlaveSafeIndex.cs | GetOrNull/TryGet/GetSlaveBySlaveNum/GetSlaveByConfigAddr |
MasterStateAsync.cs | SetStateAsync/WaitForStateAsync/GoToOpAsync |
AlStatusPattern.cs | Category/IsTransient/IsMailboxError/... |
PdoSpan.cs | InputSpan/OutputSpan/InputSlice/OutputSlice/CopyInputsTo/CopyToOutputs |
EventStreams.cs | StateChangeStream/DiagnosticStream/EmergencyStream + 极简 IObservable<T> |
DiagnosticPolling.cs | PollDiagnosticMessages (yield) |
MasterAsyncClose.cs | CloseAsync/DisposeAsync/DisposeAllAsync |
依赖
全部为零额外 NuGet 依赖。可选启用:
| NuGet | 启用什么 |
|---|---|
Microsoft.Bcl.AsyncInterfaces | await foreach (IAsyncEnumerable) / IAsyncDisposable 包装 |
System.Reactive | IObservable.Where/Throttle/Buffer/Merge 等 LINQ-to-Events |
与其他语言 SDK 的关系
| 写法 | C# | C/C++/Java/Python/Rust |
|---|---|---|
| 元组解构 | ✓ | 用各自语言原生方式(C++17 structured binding / Python tuple unpacking) |
| record struct + with | ✓ | C++ 用 struct + designated initializer;其他语言无对应 |
| Range / LINQ | ✓ | 各语言用 stream API / list comprehension 自行实现 |
| async/await | ✓ | Java CompletableFuture / Python asyncio / Rust async fn 风格不同 |
IObservable<T> | ✓ | RxJava / RxPy / 各自 SDK 有自己的事件抽象 |
Span<byte> | ✓ | C/C++ 直接用裸指针;Rust 用 &[u8];Java/Python 用 ByteBuffer |
| Pattern matching | ✓ | 其他语言用 if/else 或 visitor pattern |
核心结论:本页是 C# 用户的便利写法,跨语言一致性放在通用 API 章节。两边都保留,不矛盾。