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

Xamarin/Mono/CLR运行时异常:不该抛出的KeyNotFoundException

异常问题:字典检查存在后获取仍抛出KeyNotFoundException

This exception should not happen

我在开发Xamarin/Android应用时遇到了诡异的异常:明明先检查了字典中存在指定Key,后续获取该Key时却抛出了KeyNotFoundException,就像条件判断完全失效了一样。

相关代码如下:

public class DataProvider
{
    private readonly DeviceModelFactory factory;
    private readonly Dictionary<string, DeviceModel> deviceCache;
    private readonly Dictionary<string, DeviceSetup> setupCache;

    public DataProvider()
    {
        this.factory = new DeviceModelFactory();
        this.deviceCache = new Dictionary<string, DeviceModel>();
        this.setupCache = new Dictionary<string, DeviceSetup>();
    }

    public DeviceSetup GetDeviceSetupByDataNode(DataNode node)
    {
        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        var fullPath = node.Path;

        lock (this.setupCache)
        {
            if (!this.setupCache.ContainsKey(fullPath))
            {
                this.setupCache.Add(fullPath, this.factory.GetDeviceSetupByDataNode(node));
                this.setupCache[fullPath].SetDataNode(node);
            }
        }

        return this.setupCache[fullPath];
    }

    public DeviceModel GetDeviceModelByDataNode(DataNode node)
    {
        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        var key = node.Path;

        lock (this.deviceCache)
        {
            if (!this.deviceCache.ContainsKey(key))
            {
                this.deviceCache.Add(key, this.factory.GetDeviceModelByDataNode(node));
                this.deviceCache[key].SetDataNode(node);
            }

            try
            {
                return this.deviceCache[key];
            }

            catch(Exception ex)
            {
                Debugger.Break(); // How the f**k is this happening?!
                throw new KeyNotFoundException($"Device cache missing entry for key : {key}");
            }
        }
    }

    public void ClearSetupCache()
    {
        lock (this.setupCache)
        {
            this.setupCache.Clear();
        }
    }

    public void ClearDeviceCache()
    {
        lock (this.deviceCache)
        {
            this.deviceCache.Clear();
        }
    }
}

问题根源排查

这几乎不可能是Mono框架的Bug,更大概率是以下几种场景:

  • Key值不一致:node.Path不是不可变属性,在ContainsKey(key)检查完成后、deviceCache[key]获取前,node.Path的值被其他线程修改,导致实际访问的Key和之前检查的Key完全不同。
  • 字符串隐式差异:node.Path的字符串存在肉眼不可见的差异(比如全角/半角空格、控制字符),或者DataNode.Path的string类型被自定义了Equals/GetHashCode逻辑,导致ContainsKey判断存在,但实际哈希匹配失败。
  • 锁时机问题:虽然代码加了锁,但key的获取在锁外,极端情况下可能出现node.Path在进入锁前后发生变化,导致锁内检查的Key和后续获取的Key不一致。

解决方案

  1. 将Key的获取移到锁内:确保锁内所有操作使用的是同一个Key快照,避免外部修改影响:
public DeviceModel GetDeviceModelByDataNode(DataNode node)
{
    if (node == null)
    {
        throw new ArgumentNullException(nameof(node));
    }

    lock (this.deviceCache)
    {
        var key = node.Path;
        if (!this.deviceCache.ContainsKey(key))
        {
            this.deviceCache.Add(key, this.factory.GetDeviceModelByDataNode(node));
            this.deviceCache[key].SetDataNode(node);
        }

        try
        {
            return this.deviceCache[key];
        }
        catch(Exception ex)
        {
            Debugger.Break();
            throw new KeyNotFoundException($"Device cache missing entry for key : {key}");
        }
    }
}
  1. 使用TryGetValue替代先检查再获取:这是字典操作的最佳实践,彻底避免检查与获取之间的竞态,同时简化代码:
public DeviceModel GetDeviceModelByDataNode(DataNode node)
{
    if (node == null)
    {
        throw new ArgumentNullException(nameof(node));
    }

    lock (this.deviceCache)
    {
        var key = node.Path;
        if (!this.deviceCache.TryGetValue(key, out var model))
        {
            model = this.factory.GetDeviceModelByDataNode(node);
            model.SetDataNode(node);
            this.deviceCache.Add(key, model);
        }
        return model;
    }
}
  1. 验证字符串一致性:在异常捕获块中添加日志,输出Key的哈希值、长度和字符详情,对比字典中所有Key的信息,排查隐式差异:
catch(Exception ex)
{
    Debug.WriteLine($"Target Key: '{key}', HashCode: {key.GetHashCode()}, Length: {key.Length}");
    foreach (var cacheKey in this.deviceCache.Keys)
    {
        Debug.WriteLine($"Cache Key: '{cacheKey}', HashCode: {cacheKey.GetHashCode()}, Length: {cacheKey.Length}");
    }
    Debugger.Break();
    throw new KeyNotFoundException($"Device cache missing entry for key : {key}");
}
  1. 确保DataNode.Path不可变:如果Path是可写属性,修改为只读;或者在使用时创建字符串副本(var key = string.Copy(node.Path);),避免外部修改影响。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 18:05:25