当前位置: 首页 > news >正文

设计一个 .NET 可用的弱引用集合(可用来做缓存池使用)

我们有弱引用 WeakReference<T> 可以用来保存可被垃圾回收的对象,也有可以保存键值对的 ConditionalWeakTable

我们经常会考虑制作缓存池。虽然一般不推荐这么设计,但是你可以使用本文所述的方法和代码作为按垃圾回收缓存的缓存池的设计。


本文内容

    • 设计思路
    • 设计原则
    • 设计实践
      • 分析踩坑
        • `IList`
        • `ICollection`
        • `IEnumerable`
        • `object`
      • 动手
    • 完整代码

设计思路

既然现有 WeakReference<T>ConditionalWeakTable 可以帮助我们实现弱引用,那么我们可以考虑封装这两个类中的任何一个或者两个来帮助我们完成弱引用集合。

ConditionalWeakTable 类型仅仅在 internal 级别可以访问到集合中的所有的元素,对外开放的接口当中是无法拿到集合中的所有元素的,仅仅能根据 Key 来找到 Value 而已。所以如果要根据 ConditionalWeakTable 来实现弱引用集合那么需要自己记录集合中的所有的 Key,而这样的话我们依然需要自己实现一个用来记录所有 Key 的弱引用集合,相当于鸡生蛋蛋生鸡的问题。

所以我们考虑直接使用 WeakReference<T> 来实现弱引用集合。

自己维护一个列表 List<WeakReference<T>>,对外开放的 API 只能访问到其中未被垃圾回收到的对象。

设计原则

在设计此类型的时候,有一个非常大的需要考虑的因素,就是此类型中的元素个数是不确定的,如果设计不当,那么此类型的使用者可能写出的每一行代码都是 Bug。

你可以参考我的另一篇博客了解设计这种不确定类型的 API 的时候的一些指导:

  • 如何为非常不确定的行为(如并发)设计安全的 API,使用这些 API 时如何确保安全

总结起来就是:

  • 必须提供一个单一的方法,能够完成一些典型场景下某一时刻确定性状态的获取
  • 绝不能提供一些可能多次调用获取状态的方法

那么这个原则怎么体现在此弱引用集合的类型设计上呢?

设计实践

分析踩坑

IList<T>

我们来看看 IList<T> 接口是否可行:

public class WeakCollection<T> : IList<T> where T : class
{
    public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
    public int Count => throw new NotImplementedException();
    public bool IsReadOnly => throw new NotImplementedException();
    public void Add(T item) => throw new NotImplementedException();
    public void Clear() => throw new NotImplementedException();
    public bool Contains(T item) => throw new NotImplementedException();
    public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
    public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
    public int IndexOf(T item) => throw new NotImplementedException();
    public void Insert(int index, T item) => throw new NotImplementedException();
    public bool Remove(T item) => throw new NotImplementedException();
    public void RemoveAt(int index) => throw new NotImplementedException();
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

this[]CountIsReadOnlyContainsCopyToIndexOfGetEnumerator 这些都是在获取状态,AddClearRemove 是在修改状态,而 InsertRemoveAt 会在修改状态的同时读取状态。

这么多的获取和修改状态的方法,如果提供出去,还指望使用者能够正常使用,简直是做梦!违背以上两个原则。

ICollection<T>

那我们看看 IList<T> 的底层集合 ICollection<T>,实际上并没有解决问题,所以依然排除不能用!

    public class WeakCollection<T> : ICollection<T> where T : class
    {
--      public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
        public int Count => throw new NotImplementedException();
        public bool IsReadOnly => throw new NotImplementedException();
        public void Add(T item) => throw new NotImplementedException();
        public void Clear() => throw new NotImplementedException();
        public bool Contains(T item) => throw new NotImplementedException();
        public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
        public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
--      public int IndexOf(T item) => throw new NotImplementedException();
--      public void Insert(int index, T item) => throw new NotImplementedException();
        public bool Remove(T item) => throw new NotImplementedException();
--      public void RemoveAt(int index) => throw new NotImplementedException();
        IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
    }

不过,AddRemove 方法可能我们会考虑留下来,但这就不能是继承自 ICollection<T> 了。

IEnumerable<T>

IEnumerable<T> 里面只有两个方法,看起来少多了,那么我们能用吗?

public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();

这个方法仅供 foreach 使用,本来如果只是如此的话,问题还不是很大,但针对 IEnumerator<T> 有一大堆的 Linq 扩展方法,于是这相当于给此弱引用集合提供了大量可以用来读取状态的方法。

这依然非常危险!

使用者随时可能使用其中一个扩展方法得到了其中一个状态,随后使用另一个扩展方法得知其第二个状态,例如:

// 判断集合中是否存在 IFoo 类型以及是否存在 IBar 类型。
var hasFoo = weakList.OfType<IFoo>().Any();
var hasBar = weakList.OfType<IBar>().Any();

对具有并发开发经验的你来说,以上方法第一眼就能识别出这是不正确的写法。然而类型既然已经开放出去给大家使用了,那么这就非常危险。关键是这不是一个并发场景,于是开发者可能更难感受到在同一个上下文中调用两个方法将得到不确定的结果。对于并发可以使用锁,但对于弱引用,没有可以使用的相关方法来快速解决问题。

因此,IEnumerable<T> 也是不能继承的。

object

看来,我们只能继承自单纯的 object 基类了。此类型没有对托管来说可见的状态,于是谁也不会多次读取状态造成状态不确定了。

因此,我们需要自行实现所有场景下的 API。

动手

弱引用集合我们需要这些使用场景:

  • 向弱引用集合中添加一个元素 此场景下仅仅修改集合而不需要读取任何状态。
  • 向弱引用集合中移除一个元素 既然可以在参数中传入元素,说明此元素一定没有会垃圾回收;因此只要集合中还存在此元素,一定可以确定地移除,不会出现不确定的状态。
  • 在弱引用集合中找到符合要求的一个或多个元素 一旦满足要求,必须得到完全确定的结果,且在此结果保存的过程中一直生效。

可选考虑下面这些场景:

  • 清除所有元素 通常是为了复用某个缓存池的实例。

一定不能实现下面这些方法:

  • 判断是否存在某个元素 因为判断是否存在通常不是单独的操作,通常会使用此集合继续下一个操作,因此一定不能直接提供。
  • 其他在本文前面已经喷过不能添加进来的方法

于是,我们的 API 设计将是这样的:

public class WeakCollection<T> where T : class
{
    public void Add(T item) => throw new NotImplementedException();
    public bool Remove(T item) => throw new NotImplementedException();
    public void Clear() => throw new NotImplementedException();
    public T[] TryGetItems(Func<T, bool> filter) => throw new NotImplementedException();
}

完整代码

此类型已经以源代码包的形式发布到了 NuGet 上,你可以安装以下 NuGet 包阅读和使用其源代码:

  • Walterlv.Collections.Source

安装后,你可以在你的项目中使用其源代码,并且可以直接使用 Ctrl + 鼠标点击的方式打开类型的源代码,而不需要进行反编译。


我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 使用 C# 中的 dynamic 关键字调用类型方法时可能遇到的各种问题
  • 程序员可能会使用的各种命名规则
  • System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”
  • System.InvalidOperationException:“寄宿的 HWND 必须是指定父级的子窗口。”
  • 在使用 .NET Remoting 技术开发跨进程通信时可能遇到的各种异常
  • 使用 SetParent 跨进程设置父子窗口时的一些问题(小心卡死)
  • System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。
  • 解决 WPF 嵌套的子窗口在改变窗口大小的时候闪烁的问题
  • WPF 的 Application.Current.Dispatcher 中,为什么 Current 可能为 null
  • WPF 的 Application.Current.Dispatcher 中,Dispatcher 属性一定不会为 null
  • 提高使用 Visual Studio 开发效率的键盘快捷键
  • WPF 不要给 Window 类设置变换矩阵(分析篇):System.InvalidOperationException: 转换不可逆。
  • WPF 不要给 Window 类设置变换矩阵(应用篇)
  • git fetch 失败,因为 unable to resolve reference 'refs/remotes/origin/xxx': reference broken
  • .NET/C# 阻止屏幕关闭,阻止系统进入睡眠状态
  • 【腾讯Bugly干货分享】从0到1打造直播 App
  • Angular2开发踩坑系列-生产环境编译
  • Angular数据绑定机制
  • Cumulo 的 ClojureScript 模块已经成型
  • HomeBrew常规使用教程
  • javascript 总结(常用工具类的封装)
  • JavaScript设计模式之工厂模式
  • SpiderData 2019年2月13日 DApp数据排行榜
  • WinRAR存在严重的安全漏洞影响5亿用户
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 干货 | 以太坊Mist负责人教你建立无服务器应用
  • 工程优化暨babel升级小记
  • 基于游标的分页接口实现
  • 记录一下第一次使用npm
  • 记一次和乔布斯合作最难忘的经历
  • 将 Measurements 和 Units 应用到物理学
  • 问题之ssh中Host key verification failed的解决
  • 想晋级高级工程师只知道表面是不够的!Git内部原理介绍
  • 小程序开发之路(一)
  • 一个项目push到多个远程Git仓库
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • ​第20课 在Android Native开发中加入新的C++类
  • ​力扣解法汇总946-验证栈序列
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #vue3 实现前端下载excel文件模板功能
  • (floyd+补集) poj 3275
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (Python第六天)文件处理
  • (八)五种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (六)c52学习之旅-独立按键
  • (五)c52学习之旅-静态数码管
  • (一)kafka实战——kafka源码编译启动
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .net mvc 获取url中controller和action
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件