TL; DR我正在寻找一种从IEqualityComparer<T>
获取IComparer<T>
的方法,无论哪个数据类型为T
,如果T
为string
,则包括不区分大小写的选项string
。或者我需要一个不同的解决方案来解决这个问题。
以下是完整的故事:我正在使用LFU策略实现简单的通用缓存。要求是必须能够选择缓存是区分大小写还是不区分大小写 - 如果.ToLower().GetHashCode()
恰好是缓存键的数据类型(这是不必要的)。在解决方案中,我主要开发缓存,我期望数百亿的缓存查找,以及最多100,000个条目的缓存大小。由于这些数字,我立即停止使用导致分配的任何字符串操作(例如IComparer
等),而是选择使用IEqualityComparer
和public class LFUCache<TKey,TValue>
{
private readonly Dictionary<TKey,CacheItem> entries;
private readonly SortedSet<CacheItem> lfuList;
private class CacheItem
{
public TKey Key;
public TValue Value;
public int UseCount;
}
private class CacheItemComparer : IComparer<CacheItem>
{
private readonly IComparer<TKey> cacheKeyComparer;
public CacheItemComparer(IComparer<TKey> cacheKeyComparer)
{
this.cacheKeyComparer = cacheKeyComparer;
if (cacheKeyComparer == null)
this.cacheKeyComparer = Comparer<TKey>.Default;
}
public int Compare(CacheItem x, CacheItem y)
{
int UseCount = x.UseCount - y.UseCount;
if (UseCount != 0) return UseCount;
return cacheKeyComparer.Compare(x.Key, y.Key);
}
}
public LFUCache(int capacity, IEqualityComparer<TKey> keyEqualityComparer,
IComparer<TKey> keyComparer) // <- here's my problem
{
// ...
entries = new Dictionary<TKey, CacheItem>(keyEqualityComparer);
lfuList = new SortedSet<CacheItem>(new CacheItemComparer(keyComparer));
}
// ...
}
,因为它们是标准的BCL功能。此缓存的用户可以将比较器传递给构造函数。以下是代码的相关片段:
keyEqualityComparer
keyComparer
用于管理缓存条目(例如,如果用户想要,则键“ABC”和“abc”相等)。 UseCount
用于管理按CacheItemComparer
排序的缓存条目,以便轻松选择最不常用的缓存条目(在var cache = new LFUCache<string, int>(10000,
StringComparer.InvariantCultureIgnoreCase,
StringComparer.InvariantCultureIgnoreCase);
类中实现)。
自定义比较的正确用法示例:
StringComparer
(看起来很愚蠢,但IComparer<string>
同时实现IEqualityComparer<string>
和keyEqualityComparer
。)问题是如果用户提供不兼容的比较器(即不区分大小写的keyComparer
且区分大小写Tuple<string,DateTime,DateTime>
),那么最可能的结果是无效的LFU统计数据,因此最好降低缓存命中率。另一种情况也不尽如人意。此外,如果密钥更复杂(我会有类似IEqualityComparer<T>.Equals()
的东西),可能会更严重地搞乱它。
这就是为什么我想在构造函数中只有一个比较器参数,但这似乎不起作用。我可以在IComparer<T>.Compare()
的帮助下创建IEqualityComparer<T>.GetHashCode()
,但我坚持CompareInfo
- 这非常重要,如您所知。如果我有权访问比较器的私有属性以检查它是否区分大小写,我会使用Dictionary<TKey,TValue>
来获取哈希码。
我喜欢这种方法有两种不同的数据结构,因为它给我可接受的性能和可控的内存消耗 - 在我的笔记本电脑上大约500.000缓存添加/秒,缓存大小10.000元素。 SortedSet<CacheItem>
仅用于在O(1)中查找数据,lfuList.Min
在O(log n)中插入数据,通过在O(log n)中调用File.WriteAllLines(massagedFileName, linesModified, Encoding.UTF8);
来查找要删除的元素,并在O(log n)中找到增加使用次数的条目。
欢迎任何有关如何解决此问题的建议。我会感谢任何想法,包括不同的设计。
答案 0 :(得分:2)
由于您无法知道不等项是否大于或小于另一项,因此无法从IComparer
实施IEqualityComparer
。
无法从IEqualityComparer
实施IComparer
,因为您无法生成与{{1}一致的哈希码身份。
也就是说,在您的情况下,您不需要同时使用这两种类型的比较器。在计算LRU时,您需要比较自项目用作主要比较器以来的时间,然后根据传入的比较器作为决胜局进行比较。只需删除最后一部分; 没有决胜局。当最近最少使用时,哪个项目离开缓存是不确定的。当您这样做时,您只需要接受IComparer
,而不是IEqualityComparer
。
答案 1 :(得分:2)
正如我在评论中提到的那样,您可以添加一个帮助方法,这可能会使基本用例更简单:
public class LFUCache<TKey,TValue>
{
public static LFUCache<TKey, TValue> Create<TComp>(int capacity, TComp comparer) where TComp : IEqualityComparer<TKey>, IComparer<TKey>
{
return new LFUCache<TKey, TValue>(capacity, comparer, comparer);
}
}
你可以这样使用它:
var cache = LFUCache<string, int>.Create(10000, StringComparer.InvariantCultureIgnoreCase);
答案 2 :(得分:1)
好的下一次尝试。以下是针对LFU的Add
和Touch
的实现:
public class LfuCache<TKey, TValue>
{
private readonly Dictionary<TKey, LfuItem> _items;
private readonly int _limit;
private LfuItem _first, _last;
public LfuCache(int limit, IEqualityComparer<TKey> keyComparer = null)
{
this._limit = limit;
this._items = new Dictionary<TKey,LfuItem>(keyComparer);
}
public void Add(TKey key, TValue value)
{
if (this._items.Count == this._limit)
{
this.RemoveLast();
}
var lfuItem = new LfuItem { Key = key, Value = value, Prev = this._last };
this._items.Add(key, lfuItem);
if (this._last != null)
{
this._last.Next = lfuItem;
lfuItem.Prev = this._last;
}
this._last = lfuItem;
if (this._first == null)
{
this._first = lfuItem;
}
}
public TValue this[TKey key]
{
get
{
var lfuItem = this._items[key];
++lfuItem.UseCount;
this.TryMoveUp(lfuItem);
return lfuItem.Value;
}
}
private void TryMoveUp(LfuItem lfuItem)
{
if (lfuItem.Prev == null || lfuItem.Prev.UseCount >= lfuItem.UseCount) // maybe > if you want LRU and LFU
{
return;
}
var prev = lfuItem.Prev;
prev.Next = lfuItem.Next;
lfuItem.Prev = prev.Prev;
prev.Prev = lfuItem;
if (lfuItem.Prev == null)
{
this._first = lfuItem;
}
}
private void RemoveLast()
{
if (this._items.Remove(this._last.Key))
{
this._last = this._last.Prev;
if (this._last != null)
{
this._last.Next = null;
}
}
}
private class LfuItem
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public long UseCount { get; set; }
public LfuItem Prev { get; set; }
public LfuItem Next { get; set; }
}
}
在我看来,Add
和Touch
似乎在O(1)中,不是吗?
目前我没有看到_first
的任何用例,但也许其他人都需要它。要删除项_last
就足够了。
修改强>
如果您不需要MoveDown操作,也可以执行单个链接列表。
编辑没有一个链接列表无效,因为MoveUp需要Next
指针来更改它的Prev
指针。
答案 3 :(得分:1)
您可以尝试使用chr,pos,cov
NC_024468.2,1,0
NC_024468.2,2,0
NC_024468.2,3,0
.....
NC_024468.2,137690418,7
NC_024468.2,137690419,6
NC_024468.2,137690420,6
和定义了IEqualityComparer
的lambda,而不是在构造函数中使用IComparer
和IComparer
。然后基于GetHashCode()
和if(IComparer==0)
的{{3}}。
尽管我会说它很小,但是当GetHashCode() = lambda
返回0时,您仍有HashCode不匹配的风险。如果您想让代码用户对其非常清楚,则可以随时扩展该策略通过在构造函数中使用两个lambda:IComparer
用于Func<T,T,int>
和IComparer
,IEqualityComparer
代表Func<T,int>
。