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

移除一个列表元素时意外修改另一列表的技术问题排查

问题描述

实现撤销功能时,执行this._AllCurrentLinesInCanvas.RemoveAt(this._AllCurrentLinesInCanvas.Count - 1);操作后,发现不仅_AllCurrentLinesInCanvas被修改,原本不应改动的其他历史列表也出现了变化。

相关代码片段

撤销功能核心代码

if (this.threadStep > 0) 
{
    Debug.WriteLine($"thread remove");

    this._AllCurrentLinesInCanvas.RemoveAt(this._AllCurrentLinesInCanvas.Count - 1);

    _renderTargetDrawingSession.Clear(Colors.Transparent);

    foreach (var line in this._AllCurrentLinesInCanvas)
    {
        _renderTargetDrawingSession.DrawInk(line);
    }

    this.threadStep -= 1;
}

笔触绘制时的历史构建代码

private int threadStep = 0; 
private bool isDeepInThread = true;
private int currentTimeline = 0;
private IReadOnlyList<InkStroke> SingleWetLine;
private List<IReadOnlyList<InkStroke>> _AllCurrentLinesInCanvas = new List<IReadOnlyList<InkStroke>>(); 
private List<List<IReadOnlyList<InkStroke>>> _DrawingHistoryThread = new List<List<IReadOnlyList<InkStroke>>>();
private List<List<List<IReadOnlyList<InkStroke>>>> Timelines = new List<List<List<IReadOnlyList<InkStroke>>>>();
private bool clearedByUndoing = false;

private void InkPresenter_StrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args) 
{
    this.SingleWetLine = myInkCanvasObject.myInkSynchronizer.BeginDry(); 

    myInkCanvasObject.myInkSynchronizer.EndDry(); 

    this._AllCurrentLinesInCanvas.Add(this.SingleWetLine.ToList()); 
     
    if (this.threadStep == this._DrawingHistoryThread.Count) 
    {
        this._DrawingHistoryThread.Add(new List<IReadOnlyList<InkStroke>>(this._AllCurrentLinesInCanvas));

        if (this.Timelines.Count == 0 || this.clearedByUndoing) 
        {
            this.Timelines.Add(new List<List<IReadOnlyList<InkStroke>>>(this._DrawingHistoryThread));

            this.currentTimeline = this.Timelines.Count;

            this.clearedByUndoing = false;
        }

        else
        {
            this.Timelines[this.Timelines.Count - 1] = new List<List<IReadOnlyList<InkStroke>>>(this._DrawingHistoryThread);

        }

        this.isDeepInThread = true;

        this.threadStep += 1;
    }

    else // create a new timeline when a fork occurs
    {
        Debug.WriteLine("Fork State Hit");
        
        this._DrawingHistoryThread.Clear();

        this._DrawingHistoryThread.Add(new List<IReadOnlyList<InkStroke>>(this._AllCurrentLinesInCanvas));

        this.Timelines.Add(new List<List<IReadOnlyList<InkStroke>>>(this._DrawingHistoryThread));

        this.isDeepInThread = true;

        this.threadStep = 1;

        this.currentTimeline = this.Timelines.Count; 
    }

    myWin2DCanvas.Invalidate();
}

完整撤销函数

public void Undo(object sender, RoutedEventArgs e)
{

    using (CanvasDrawingSession _renderTargetDrawingSession = myRenderTarget.CreateDrawingSession())
    {

        if (this.threadStep > 0) 
        {
            Debug.WriteLine($"thread remove");

            this._AllCurrentLinesInCanvas.RemoveAt(this._AllCurrentLinesInCanvas.Count - 1);

            _renderTargetDrawingSession.Clear(Colors.Transparent);

            foreach (var line in this._AllCurrentLinesInCanvas)
            {
                _renderTargetDrawingSession.DrawInk(line);
            }

            this.threadStep -= 1;
        }

        else if (this.currentTimeline > 1) // you are at a node
        {
            Debug.WriteLine("node remove");

            this.currentTimeline -= 1;

            this.threadStep = this.Timelines[this.currentTimeline - 1].Count;

            this._AllCurrentLinesInCanvas.Clear();

            Debug.WriteLine($"_All Current builder : {this.currentTimeline - 1}, {this.threadStep - 1}");

            this._AllCurrentLinesInCanvas = this.Timelines[this.currentTimeline - 1][this.threadStep - 1];

            _renderTargetDrawingSession.Clear(Colors.Transparent);

            foreach (var line in this._AllCurrentLinesInCanvas)
            {
                _renderTargetDrawingSession.DrawInk(line);
            }

            this.threadStep -= 1;
        }

        else // at origin -> first line
        {
            Debug.WriteLine("Cleared through undoing");

            _renderTargetDrawingSession.Clear(Colors.Transparent);

            // clear everything

            this.threadStep = 0;

            this._AllCurrentLinesInCanvas.Clear();

            this._DrawingHistoryThread.Clear();

            this.clearedByUndoing = true;

        }

    }   
    myWin2DCanvas.Invalidate();
}
原因分析

问题根源是引用传递导致的列表共享:

  1. 浅拷贝的局限性:在InkPresenter_StrokesCollected方法中,使用new List<T>(existingList)创建历史列表时,只是做了浅拷贝——新列表的外层容器是新的,但内部的IReadOnlyList<InkStroke>元素仍然和原列表共享引用。比如this._DrawingHistoryThread.Add(new List<IReadOnlyList<InkStroke>>(this._AllCurrentLinesInCanvas)),这里添加的新列表和_AllCurrentLinesInCanvas指向相同的笔触列表元素。
  2. 直接引用历史列表:在撤销函数的节点回退分支中,直接将_AllCurrentLinesInCanvas赋值为Timelines中的某个列表引用:this._AllCurrentLinesInCanvas = this.Timelines[this.currentTimeline - 1][this.threadStep - 1];,此时_AllCurrentLinesInCanvas和Timelines中的该列表是同一个内存对象,后续对_AllCurrentLinesInCanvas的修改(如RemoveAt)会直接同步到历史列表中。
解决方法

需要对所有历史状态进行深拷贝,确保每个状态都是独立的副本,互不干扰。

1. 改进历史记录保存逻辑

保存_AllCurrentLinesInCanvas到_DrawingHistoryThread和Timelines时,要逐层拷贝列表元素:

// 替换原有的this._DrawingHistoryThread.Add(new List<>(...))
var copiedCurrentLines = new List<IReadOnlyList<InkStroke>>();
foreach (var strokeGroup in this._AllCurrentLinesInCanvas)
{
    // 拷贝每个笔触列表
    copiedCurrentLines.Add(new List<InkStroke>(strokeGroup));
}
this._DrawingHistoryThread.Add(copiedCurrentLines);

// 替换原有的Timelines.Add(new List<>(...))
var copiedHistoryThread = new List<List<IReadOnlyList<InkStroke>>>();
foreach (var lineList in this._DrawingHistoryThread)
{
    var copiedLineGroup = new List<IReadOnlyList<InkStroke>>();
    foreach (var strokeGroup in lineList)
    {
        copiedLineGroup.Add(new List<InkStroke>(strokeGroup));
    }
    copiedHistoryThread.Add(copiedLineGroup);
}
this.Timelines.Add(copiedHistoryThread);

2. 修改撤销函数的赋值逻辑

回退到历史节点时,不要直接引用Timelines中的列表,而是创建独立副本:

// 替换原有的this._AllCurrentLinesInCanvas = ...
var targetHistoryList = this.Timelines[this.currentTimeline - 1][this.threadStep - 1];
this._AllCurrentLinesInCanvas.Clear();
foreach (var strokeGroup in targetHistoryList)
{
    this._AllCurrentLinesInCanvas.Add(new List<InkStroke>(strokeGroup));
}

3. 确保InkStroke的独立性

如果InkStroke是引用类型且内部状态可能被修改,需要为其实现深拷贝逻辑(比如实现ICloneable接口),避免后续笔触操作影响历史记录中的实例:

public class InkStroke : ICloneable
{
    // 原有属性和方法
    
    public object Clone()
    {
        // 实现深拷贝逻辑,复制所有内部状态
        return new InkStroke
        {
            // 逐一复制属性
        };
    }
}

// 拷贝笔触时使用Clone方法
copiedCurrentLines.Add(strokeGroup.Select(s => (InkStroke)s.Clone()).ToList());

内容的提问来源于stack exchange,提问作者Ajaykrishnan R

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 04:22:03