65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 泛型实现缓存集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (21投票s)

2004年7月11日

CPOL

6分钟阅读

viewsIcon

160068

downloadIcon

1797

本文介绍了我如何使用泛型实现一个具有用户定义大小限制的通用缓存集合。

Sample Image - GenericCache.jpg

概述

这篇文章的初衷是我希望学习.NET新版本和Framework 2.0中新增的泛型功能。虽然我现在觉得编写一篇关于泛型的完整教程可能还为时过早,但这篇文章展示了我如何使用泛型来解决我最近遇到的一个问题。

引言

在我目前的工作中,我最近不幸地发现了一个关于我们主要开发的内存问题。我们在任何时候都会使用数百个位图。很久以前,有人认为为了提高性能,我们会将位图缓存到内存中。这个想法不错,但我的当前项目似乎将这项技术推向了一个新的高度,现在我们因为缓存了数百兆字节的图像而内存不足。

解决方案?

思考这个问题,很明显我们不需要缓存所有图像,只需要我们正在处理的图像。尝试在没有任何缓存的情况下运行系统,证明了最初添加缓存支持的原因——由于大量访问硬盘,性能开始变得迟缓。然而,如果一个图像很久没有被使用过,那么我们可以很容易地再次从磁盘读取它(就像第一次使用时一样),从而将频繁和最近访问的图像保留在缓存中。

在工作中,经过几个小时的编码,我使用STL和模板实现了一个非常基本的C++缓存。然而,本文从我开始思考STL和新的C#泛型有多么相似这一点开始。

Generics

我非常不喜欢在C#中编程的一点是,每当我使用一个集合(我经常这样做)时,我必须担心我正在添加和检索的类型,否则我必须创建自己的类型安全集合实现。每当我使用C++的STL时,这从来都不是问题。不断地检查和转换对象使我的代码显得杂乱无章。

最基本的一点是,泛型允许我指定我要处理的类型。以下面两个例子为例,第一个是C# 1.x,第二个是C# 2.x。

ArrayList items = new ArrayList();
items.Add("a string");
items.Add(123);
items.Add(this);
List<string> items = new List<string>();
items.Add("a string");
items.Add(123);  // Compile time error!!
items.Add(this);  // Compile time error!!

使用泛型,我可以指定我想要的列表类型,在本例中是字符串。然后编译器将确保我不会开始添加许多不兼容的类型。

Cache<Key,Value>

我创建的缓存实现使用了cache<Key,Value>签名,这看起来非常像哈希表或字典的签名。我想要的是将缓存的数据与某种标识符(例如,我缓存的位图的文件名)关联起来。实际的类定义如下所示

   public class Cache 
      where Key: IComparable
      where Value: ICacheable
   {

请注意,与典型的类定义相比,有一些变化。新的where关键字允许对泛型的使用进行细化。在上面的代码中,我已经指定我希望Key类型至少支持IComparable(这对于我在集合中搜索键是必需的)。

我还定义了值需要实现ICacheable的需求。ICacheable接口是我创建的一个非常简单的接口

 public interface ICacheable
 {
  int BytesUsed { get; }
 }

它真正需要做的就是提供BytesUsed属性,以便缓存知道其表示的数据有多大。这可以在Add方法中看到。

  public void Add(Key k, Value v)
  {
    // Check if we're using this yet
    if (ContainsKey(k))
    {
      // Simple replacement by removing and adding again, this
      // will ensure we do the size calculation in only one place.
      Remove(k);
    }

    // Need to get current total size and see if this will fit.
    int projectedUsage = v.BytesUsed 
                       + this.CurrentCacheUsage;
      
    if (projectedUsage > maxBytes)
      PurgeSpace(v.BytesUsed);

    // Store this value now..
    StoreItem(k, v);
  }

与任何容器类一样,有许多用于获取或更改集合成员的其他方法。已实现的方法包括

  • void Remove(Key k)
  • void Touch(Key k)
  • Value GetValue(Key k)
  • bool ContainsKey(Key k)
  • Value this[Key k]
  • void PurgeAll()

枚举器

为了访问缓存中存储的成员,实现了一些枚举器,这些枚举器允许获取键、值以及键值对。C# 2.0中枚举器的实现变得非常容易。

  • IEnumerator<Value> GetEnumerator()
  • IEnumerable<Key> GetKeys()
  • IEnumerable<Value> GetValues()
  • IEnumerable<KeyValuePair<Key, Value>> GetItems()
 public IEnumerable<Value> GetValues()
 {
  foreach (KeyValuePair<Key, Value> i in cacheStore)
   yield return i.Value;
 }

注意新的yield关键字。这实际上允许程序执行的控制权被传回给(或“yield”给)调用者。下次访问枚举器时,执行将从集合中的同一点继续。在上面的例子中,我正在访问内部存储cacheStore,并使用foreach命令对其进行枚举,但是,我正在将每个值暴露给GetValues()的调用者。虽然这可能需要一些理解,但相信我,这比以前需要的东西要简单得多!

属性

缓存类只有几个属性,它们的功能大体上是显而易见的。

  • int MaxBytes { get; set; }
  • int Count{ get; } // 获取缓存中当前项目的数量
  • int CurrentCacheUsage{ get; }

性能

由于使用的是微软的 Beta 软件,无法用具体数字来量化此类的性能。编译器及其生成的代码在未来可能会变得更快……另外,没有两种不同的实现,很难直接比较泛型代码和非泛型代码。我已经用许多兆字节的位图测试了这段代码,其缓存性能和预期一样好,没有注意到性能影响。需要注意的是,一旦一个项目被从缓存中淘汰,运行时系统及其垃圾回收可能需要一段时间才能将其对象释放。这个缓存实现的功能是允许你控制对象的生命周期!

结论

此处使用的缓存类实现非常基础,可以从许多方面进行进一步增强。对我来说,实现的解决方案还不错,并且允许我对应用程序使用多少内存来保持大型资源在内存中保持活动状态有一定的控制。

Visual Studio 2005 和 C# 2.0

撰写本文时,C# 2.0 还很新。我上周才从微软下载了 Beta 版本。如果将来这篇文章还在,请注意研究本文档内容与最终发布版本之间的任何差异!

如果您想下载 Beta 版本,请参阅 MSDN

我没有包含此代码的二进制文件,因为我预计在每个 Beta 版本之间框架都会发生变化。提供的两个 C# 源文件包含实现和测试应用程序,将它们放入一个空白的控制台解决方案中即可进行测试!

© . All rights reserved.