You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

疑问点

  1. 是否有优雅的方式保持树内叶子节点的RWstruct同步更新?
  2. 是否因为GetRWStructList返回列表再转数组时产生副本导致问题?直接返回数组是否有差异?
  3. 如何实现递归深度优先遍历获取叶子节点?

已尝试的方案

先获取所有叶子节点列表,将结构体复制到数组,调用读取函数后再将数组中的结构体复制回叶子节点:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 12:30:58