案例
我们都知道.NET 运行时内置了常用缓存模块 MemoryCache,它暴露了以下几个属性和方法:
public int Count { get; }
public void Compact(double percentage);
public ICacheEntry CreateEntry(object key);
public void Dispose();
public void Remove(object key);
public bool TryGetValue(object key, out object result);
protected virtual void Dispose(bool disposing);
复制代码
当我们使用常规模式去插值和获取值时很有可能会出现意想不到的问题,例如下的代码:
var mc = new MemoryCache(new MemoryCacheOptions { });
var entry = mc.CreateEntry("MiaoShu");
entry.Value = "喵叔";
var f = mc.TryGetValue("MiaoShu",out object obj);
Console.WriteLine(f);
Console.WriteLine(obj);
复制代码
运行代码后,输出结果如下:
看到输出结果是不是很意外,和你想到不一样。从代码中可以看出使用的是 MemoryCache 原生方法,但一般我们不这么用,而是使用位于同一命名空间的扩展方法 Set,代码如下:
var s = new MemoryCache(new MemoryCacheOptions { });
s.Set("MiaoShu", "喵叔");
var f = s.TryGetValue("MiaoShu", out object obj);
Console.WriteLine(f);
Console.WriteLine(obj);
复制代码
运行代码后,输出如下:
分析
为什么会出现上一小节这种情况呢?下面让我们来看一下 Set 的源码:
public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value)
{
using ICacheEntry entry = cache.CreateEntry(key);
entry.Value = value;
return value;
}
复制代码
扩展方法与原生方法的差异在于 using 关键字,这也就说明了 CacheEntry 继承自 IDisposable,下面我们继续看看 CacheEntry 实现的 Dispose 方法:
public void Dispose()
{
if (!_state.IsDisposed)
{
_state.IsDisposed = true;
if (_cache.TrackLinkedCacheEntries)
{
CacheEntryHelper.ExitScope(this, _previous);
}
// Don't commit or propagate options if the CacheEntry Value was never set.
// We assume an exception occurred causing the caller to not set the Value successfully,
// so don't use this entry.
if (_state.IsValueSet)
{
_cache.SetEntry(this);
if (_previous != null && CanPropagateOptions())
{
PropagateOptions(_previous);
}
}
_previous = null; // we don't want to root unnecessary objects
}
}
复制代码
在上面源码中_cache.SetEntry(this)表示在 MemoryCache 底层的 ConcurrentDictionary<object, CacheEntry>集合插入缓存项,也就是说缓存项 CacheEntry 需要被 Dispose 才能被插入 MemoeyCache。WTF?这是什么鬼设计,IDisposable 接口不是应该用来释放资源吗?为什么使用 Dispose 方法来向 MemoryCache 插值呢?这个问题在 2017 年开始就有人质疑这个设计,但是官方为了不引入 Break Change,一直保持现状到现在。因此根据现状,如果使用 MemoryCache 的原生插值方法,代码需要这么些:
var s = new MemoryCache(new MemoryCacheOptions { });
using (var entry = s.CreateEntry("MiaoShu"))
{
entry.Value = "喵叔";
}
var f = s.TryGetValue("MiaoShu", out object obj);
复制代码
注意,尽量不要使用 C#8.0 推出的不带大括号的 using 语法
using var entry = s.CreateEntry("MiaoShu");
entry.Value = "喵叔";
var f = s.TryGetValue("MiaoShu", out object obj);
复制代码
不带大括号的 using 语法没明确指定 using 的作用范围,会在函数末尾才执行 Dispose 方法,导致执行到 TryGetValue 时缓存项还没插入。
总结
MemoryCache 插值的实现过程很奇葩,我们应尽量使用带明确大括号范围的 using 语法,C#8.0 推出的不带大括号的 using 语法糖的作用时刻在函数末尾,这会带来误解。
评论