淺談MemoryCache的原生插值方式

.NET運行時內置了常用的緩存模塊: MemoryCache

標準的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 s = new MemoryCache(new MemoryCacheOptions { });
var entry = s.CreateEntry("WeChatID");
entry.Value = "精益碼農";

var f =  s.TryGetValue("WeChatID",out  object obj);

Console.WriteLine(f);
Console.WriteLine(obj);

會輸出如下結果:

是不是很意外。


但是看官們一般不會使用MemoryCache的原生方法,而是使用位於同一命名空間的
擴展方法Set

var s = new MemoryCache(new MemoryCacheOptions { });
s.Set("WeChatID", "精益碼農");
var f = s.TryGetValue("WeChatID", out object obj);

Console.WriteLine(f);
Console.WriteLine(obj);

如此便能正確輸出。

擴展類源碼看一看

 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

這是怎樣的設計模式?
IDisposable接口不是用來釋放資源嗎?
爲啥要使用Dispose方法來向MemoryCache插值?
不能使用一個明確的Commit方法嗎?

這在Github上也有issue討論,從2017年開始就有大佬質疑這是一個反人類的設計思路,官方爲了不引入Break Change,一直保持到現在。


基於此現狀,我們如果使用MemoryCache的原生插值方法, 需要這樣:

 var s = new MemoryCache(new MemoryCacheOptions { });
 using (var entry = s.CreateEntry("WeChatID"))
 {
      entry.Value = "精益碼農";
 }
 var f = s.TryGetValue("WeChatID", out object obj);
 ...

儘量不要使用C#8.0推出的不帶大括號的using語法

 using var entry = s.CreateEntry("WeChatID");
 entry.Value = "精益碼農";
            
 var f = s.TryGetValue("WeChatID", out object obj);
 ...

這種沒明確指定using作用範圍的語法,會在函數末尾才執行Dispose方法, 導致執行到TryGetValue時,緩存項其實還沒插入!!!

Last
  1. MemoryCache插值的實現過程很奇葩
  2. 儘量使用帶明確大括號範圍的using語法,C#8.0推出的不帶大括號的using語法糖的作用時刻在函數末尾,會帶來誤導。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章