移除一个列表元素时意外修改另一列表的技术问题排查
问题描述
实现撤销功能时,执行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(); }
原因分析
问题根源是引用传递导致的列表共享:
- 浅拷贝的局限性:在
InkPresenter_StrokesCollected方法中,使用new List<T>(existingList)创建历史列表时,只是做了浅拷贝——新列表的外层容器是新的,但内部的IReadOnlyList<InkStroke>元素仍然和原列表共享引用。比如this._DrawingHistoryThread.Add(new List<IReadOnlyList<InkStroke>>(this._AllCurrentLinesInCanvas)),这里添加的新列表和_AllCurrentLinesInCanvas指向相同的笔触列表元素。 - 直接引用历史列表:在撤销函数的节点回退分支中,直接将
_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
相关产品推荐
相关产品推荐

