C#中实现外部修改结构体后同步更新类内树结构中的结构体
背景信息
需要定期从外部PLC读取数据,使用外部库提供的ReadDataFromPlc函数,该函数接收RWstruct数组作为参数,RWstruct定义如下:
namespace extLib { public struct RWstruct { public IntPtr AccessHandle; // 要读取的数据句柄 public byte[] Buffer; // 读取的数据会写入此处 public int ResultCode; } public static int ReadDataFromPlc(RWstruct[] symbolicRW){ // 修改每个结构体中的Buffer } }
程序中用Node类构建树结构表示PLC的数据结构,叶子节点包含一个RWstruct:
class Node { private string _name; private bool _leaf; private RWstruct _rwStruct; // 叶子节点包含rwStruct private List<Node> _childNodes; // 节点要么有子节点,要么是叶子节点 // 返回当前节点及所有子节点的rwStruct列表 public List<RWstruct> GetRWStructList(){ List<RWstruct> returnList = new(); if (_leaf){ returnList.Add(_rwStruct); // 将成员rwStruct加入返回列表 } else { // 递归获取所有子节点的rwStruct foreach(Node childNode in _childNodes){ returnList.AddRange(childNode.GetRWStructList()); } return returnList; } }
获取完整树结构后,将所有叶子节点的RWstruct转为数组传入ReadDataFromPlc:
Node completeTree = getCompleteTree(); // 完整树结构未知 // 将所有叶子节点的RWstruct转为数组: RWstruct[] rwStructArray = completeTree.GetRWStructList().ToArray(); // 可能List.ToArray()是问题所在,因为文档说它会"复制"元素 // 定时(比如每秒)调用读取PLC数据 extLib.ReadDataFromPlc(rwStructArray);
当前问题现象
ReadDataFromPlc可以修改数组中的RWstruct,但树内叶子节点的RWstruct仅byte[] Buffer(引用类型)自动更新,int ResultCode(值类型)未同步:
| 树外的rwStructArray | 树内的Node._rwStruct | |
|---|---|---|
调用ReadDataFromPLC前 | byte[] Buffer = [0, 0]int ResultCode = 0 | byte[] Buffer = [0, 0]int ResultCode = 0 |
调用ReadDataFromPLC后 | byte[] Buffer = [1, 3]int ResultCode = 1 | byte[] Buffer = [1, 3]int ResultCode = 0 |
疑问点
- 是否有优雅的方式保持树内叶子节点的
RWstruct同步更新? - 是否因为
GetRWStructList返回列表再转数组时产生副本导致问题?直接返回数组是否有差异? - 如何实现递归深度优先遍历获取叶子节点?
已尝试的方案
先获取所有叶子节点列表,将结构体复制到数组,调用读取函数后再将数组中的结构体复制回叶子节点:
var leafList = completeTree.GetLeafList(); // 返回所有叶子节点列表 for (int i=0; i<leafList.Count; i++){ // 将结构体复制到扁平数组 rwStructArray[i] = leafList[i]._rwStruct; } extLib.ReadDataFromPlc(rwStructArray); // 读取PLC数据并修改数组内的结构体 // 将修改后的结构体复制回叶子节点 for (int i=0; i<leafList.Count; i++){ leafList[i]._rwStruct = rwStructArray[i]; }
问题根源:结构体的值类型特性
C#中的结构体是值类型,当你调用GetRWStructList()添加_rwStruct到列表时,实际上是创建了结构体的副本,而非引用。后续转数组时,又是对列表中副本的再次复制。所以ReadDataFromPlc修改的是数组中的副本,树内的原始结构体自然不会同步更新值类型字段(比如ResultCode);只有引用类型字段(比如Buffer)因为指向同一个内存地址,才会同步变化。
直接返回数组和返回列表再转数组没有本质差异,无论哪种方式都是在复制值类型的结构体。
优雅的解决方案
方案1:优化遍历逻辑,减少冗余操作
在Node类中实现一次遍历同时收集结构体和对应叶子节点,避免多次遍历树结构:
class Node { // ... 原有成员不变 // 一次遍历收集RWstruct数组和对应叶子节点 public (RWstruct[] RwArray, List<Node> LeafNodes) GetRWStructArrayWithLeafs(){ List<RWstruct> rwList = new(); List<Node> leafList = new(); CollectData(rwList, leafList); return (rwList.ToArray(), leafList); } private void CollectData(List<RWstruct> rwList, List<Node> leafList){ if (_leaf){ rwList.Add(_rwStruct); leafList.Add(this); } else { foreach(var child in _childNodes){ child.CollectData(rwList, leafList); } } } } // 使用方式 var (rwArray, leafNodes) = completeTree.GetRWStructArrayWithLeafs(); extLib.ReadDataFromPlc(rwArray); // 同步修改后的结构体回叶子节点 for(int i=0; i<leafNodes.Count; i++){ leafNodes[i]._rwStruct = rwArray[i]; }
方案2:用类封装RWstruct(彻底解决值类型副本问题)
将RWstruct包装成引用类型的类,这样传递的是实例引用,修改时直接作用于树内的原始数据:
// 新增封装类 public class RWStructContainer { public extLib.RWstruct Data { get; set; } } // 修改Node类的成员 class Node { // ... 其他成员不变 private RWStructContainer _rwContainer; // 替换原有的RWstruct成员 // 叶子节点初始化时给_rwContainer赋值 // ... // 获取供外部库调用的RWstruct数组 public extLib.RWstruct[] GetRWStructArray(){ List<extLib.RWstruct> list = new(); CollectRWData(list); return list.ToArray(); } private void CollectRWData(List<extLib.RWstruct> list){ if (_leaf){ list.Add(_rwContainer.Data); } else { foreach(var child in _childNodes){ child.CollectRWData(list); } } } } // 使用方式 var rwArray = completeTree.GetRWStructArray(); extLib.ReadDataFromPlc(rwArray); // 此时因为_rwContainer.Data是结构体,调用时仍会产生副本,所以需要同步回容器 // 可以在遍历收集时同时记录容器列表 public List<RWStructContainer> GetRWContainers(){ List<RWStructContainer> containers = new(); CollectContainers(containers); return containers; } private void CollectContainers(List<RWStructContainer> containers){ if (_leaf){ containers.Add(_rwContainer); } else { foreach(var child in _childNodes){ child.CollectContainers(containers); } } } // 优化后的使用方式 var containers = completeTree.GetRWContainers(); var rwArray = containers.Select(c => c.Data).ToArray(); extLib.ReadDataFromPlc(rwArray); // 同步回容器 for(int i=0; i<containers.Count; i++){ containers[i].Data = rwArray[i]; }
递归深度优先遍历的实现
上面的GetRWStructArrayWithLeafs、CollectData方法都是深度优先遍历的典型实现:
- 优先处理当前节点:如果是叶子节点,加入目标列表;
- 若当前节点有子节点,递归遍历每个子节点;
- 子节点遍历完成后,再回到当前节点处理下一个子节点,确保优先遍历完一个分支的所有节点,符合深度优先的特性。
总结
由于结构体是值类型,无法通过传递副本修改原始实例,核心解决方案都是将数组中修改后的结构体同步回树内的叶子节点。通过优化遍历逻辑减少树的遍历次数,或用类封装结构体减少代码冗余,都能实现更优雅的同步效果。
内容的提问来源于stack exchange,提问作者Tobi

