我们都知道.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集合插入缓存项,也就是说缓存项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语法糖的作用时刻在函数末尾,这会带来误解。