有没有办法从IComparer中导出IEqualityComparer?

时间:2015-06-15 17:12:17

标签: c# iequalitycomparer icomparer memorycache

TL; DR我正在寻找一种从IEqualityComparer<T>获取IComparer<T>的方法,无论哪个数据类型为T,如果Tstring,则包括不区分大小写的选项string。或者我需要一个不同的解决方案来解决这个问题。

以下是完整的故事:我正在使用LFU策略实现简单的通用缓存。要求是必须能够选择缓存是区分大小写还是不区分大小写 - 如果.ToLower().GetHashCode()恰好是缓存键的数据类型(这是不必要的)。在解决方案中,我主要开发缓存,我期望数百亿的缓存查找,以及最多100,000个条目的缓存大小。由于这些数字,我立即停止使用导致分配的任何字符串操作(例如IComparer等),而是选择使用IEqualityComparerpublic 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)中找到增加使用次数的条目。

欢迎任何有关如何解决此问题的建议。我会感谢任何想法,包括不同的设计。

4 个答案:

答案 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的AddTouch的实现:

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; }
    }
}

在我看来,AddTouch似乎在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,而不是在构造函数中使用IComparerIComparer。然后基于GetHashCode()if(IComparer==0)的{​​{3}}。

尽管我会说它很小,但是当GetHashCode() = lambda返回0时,您仍有HashCode不匹配的风险。如果您想让代码用户对其非常清楚,则可以随时扩展该策略通过在构造函数中使用两个lambda:IComparer用于Func<T,T,int>IComparerIEqualityComparer代表Func<T,int>