Xamarin/Mono/CLR运行时异常:不该抛出的KeyNotFoundException
异常问题:字典检查存在后获取仍抛出KeyNotFoundException

我在开发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不一致。
解决方案
- 将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}"); } } }
- 使用
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; } }
- 验证字符串一致性:在异常捕获块中添加日志,输出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}"); }
- 确保
DataNode.Path不可变:如果Path是可写属性,修改为只读;或者在使用时创建字符串副本(var key = string.Copy(node.Path);),避免外部修改影响。
内容的提问来源于stack exchange,提问作者Noel
相关产品推荐
相关产品推荐

