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

.NET Maui中移除持续更新集合项避免索引越界错误

问题分析与解决思路

你的核心问题是多线程并发操作ObservableCollection导致的索引越界:Binance Socket的回调在后台线程遍历并更新集合,而RemoveIndicator在UI线程修改集合结构,两者无同步机制,导致遍历过程中集合元素被移除,索引失效触发错误。以下是具体的解决方案:


方案一:线程同步+集合快照

关键改进点

  1. 使用统一锁对象同步所有集合操作,确保同一时间只有一个线程访问集合。
  2. 遍历前创建集合快照(ToList()),避免直接遍历原集合时被修改。
  3. 所有UI相关操作(集合结构修改、元素属性更新)切换到UI线程执行,符合WPF/MAUI等框架的线程模型。

修改后的代码示例

private readonly object _collectionLock = new object();
private readonly Dispatcher _dispatcher = Application.Current.Dispatcher;

public MainViewModel()
{   
    UpdateAllCommand = new Command(async () => await UpdateAll()); 
    RemoveIndicatorCommand = new Command(RemoveIndicator); 
}

async Task UpdateAll()
{
    var result = await socketClient.UsdFuturesApi.SubscribeToMiniTickerUpdatesAsync(Symbol, async data =>
    {
        if (Indicators == null) return;

        // 加锁获取集合快照,避免遍历过程中原集合被修改
        List<Indicator> indicatorsSnapshot;
        lock (_collectionLock)
        {
            indicatorsSnapshot = Indicators.ToList();
        }

        foreach (var indicator in indicatorsSnapshot)
        {
            try
            {
                // 耗时操作放在锁外,避免阻塞UI线程
                var values = await GetIndicator(indicator.Name, indicator.Period);
                
                // 切换到UI线程更新属性,同时检查元素是否仍在集合中
                await _dispatcher.InvokeAsync(() =>
                {
                    lock (_collectionLock)
                    {
                        if (Indicators.Contains(indicator))
                        {
                            indicator.Value = values[^1];
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                // 异常处理逻辑
            }
        }
    });
}

void RemoveIndicator()
{
    // 集合结构修改必须在UI线程执行,先调度再锁
    _dispatcher.Invoke(() =>
    {
        lock (_collectionLock)
        {
            if (Indicators.Count > 0)
            {
                Indicators.RemoveAt(Indicators.Count - 1);
            }
        }
    });
}

方案二:避免遍历原集合(简化版)

如果不想用锁,可以直接遍历集合快照,同时在更新属性前检查元素是否仍在原集合中:

async Task UpdateAll()
{
    var result = await socketClient.UsdFuturesApi.SubscribeToMiniTickerUpdatesAsync(Symbol, async data =>
    {
        if (Indicators == null || Indicators.Count == 0) return;

        // 创建快照,遍历快照而非原集合
        var snapshot = Indicators.ToList();
        foreach (var indicator in snapshot)
        {
            try
            {
                var values = await GetIndicator(indicator.Name, indicator.Period);
                // 切换到UI线程,检查元素是否存在后更新
                await _dispatcher.InvokeAsync(() =>
                {
                    if (Indicators.Contains(indicator))
                    {
                        indicator.Value = values[^1];
                    }
                });
            }
            catch (Exception ex)
            {
                // 异常处理逻辑
            }
        }
    });
}

void RemoveIndicator()
{
    // 集合修改必须在UI线程
    _dispatcher.Invoke(() =>
    {
        if (Indicators.Count > 0)
        {
            Indicators.RemoveAt(Indicators.Count - 1);
        }
    });
}

为什么你之前的lock没生效?

大概率是锁的范围不对:

  • 你可能只在遍历的部分代码加了锁,没有覆盖RemoveIndicator的集合修改操作。
  • 或者使用了不同的锁对象,导致线程同步失效。

内容的提问来源于stack exchange,提问作者Andrei Popa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 19:42:40