有没有办法从IComparer中导出IEqualityComparer

本文关键字:IEqualityComparer IComparer 有没有 | 更新日期: 2023-09-27 17:56:25

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

以下是完整的故事:我正在使用 LFU 策略实现简单的通用缓存。要求是必须能够选择缓存是区分大小写还是不区分大小写 - 如果string恰好是缓存键的数据类型(这不是必需的)。在我主要为其开发缓存的解决方案中,我预计会有数千亿次缓存查找,缓存大小最多为 100.000 个条目。由于这些数字,我立即放弃使用任何导致分配的字符串操作(例如.ToLower().GetHashCode()等),而是选择使用 IComparerIEqualityComparer ,因为它们是标准的 BCL 功能。此缓存的用户可以将比较器传递给构造函数。以下是代码的相关片段:

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));
    }
    // ...
}

keyEqualityComparer用于管理缓存条目(例如,如果用户愿意,键"ABC"和"abc"是相等的)。keyComparer用于管理按UseCount排序的缓存条目,以便轻松选择最不常用的条目(在类CacheItemComparer实现)。

自定义比较的正确用法示例:

var cache = new LFUCache<string, int>(10000,
    StringComparer.InvariantCultureIgnoreCase,
    StringComparer.InvariantCultureIgnoreCase);

(这看起来很愚蠢,但StringComparer实现了IComparer<string>IEqualityComparer<string>。问题是,如果用户给出不兼容的比较器(即不区分大小写的keyEqualityComparer和区分大小写的keyComparer),那么最可能的结果是无效的 LFU 统计信息,因此最多是较低的缓存命中率。另一种情况也低于预期。此外,如果密钥更复杂(我会有类似于Tuple<string,DateTime,DateTime>的东西),则可能会更严重地搞砸它。

这就是为什么我只想在构造函数中只有一个比较器参数,但这似乎不起作用。我能够在IComparer<T>.Compare()的帮助下创建IEqualityComparer<T>.Equals(),但我被困在IEqualityComparer<T>.GetHashCode() - 如你所知,这非常重要。如果我可以访问比较器的私有属性来检查它是否区分大小写,我会使用 CompareInfo 来获取哈希代码。

我喜欢这种具有 2 种不同数据结构的方法,因为它为我提供了可接受的性能和可控的内存消耗——在我的笔记本电脑上,缓存大小为 10.000 个元素,每秒大约增加 500.000 个缓存。 Dictionary<TKey,TValue> 仅用于在 O(1) 中查找数据,SortedSet<CacheItem>在 O(log n) 中插入数据,通过在 O(log n) 中调用 lfuList.Min 来查找要删除的元素,并在 O(log n) 中查找增加使用计数的条目。

欢迎就如何解决此问题提出任何建议。我会欣赏任何想法,包括不同的设计。

有没有办法从IComparer中导出IEqualityComparer

无法

IEqualityComparer实现IComparer,因为您无法知道不相等的项目是大于还是小于另一个项目。

不可能

IComparer实现IEqualityComparer,因为您无法生成符合IComparer身份的哈希代码。

也就是说,您没有必要在您的案例中同时拥有两种类型的比较器。 计算 LRU 时,您将比较自项目用作主要比较器以来的时间,然后基于传递的比较器作为决胜局进行比较。 只需删除最后一部分;没有决胜局。 让它不定义当存在最近最少使用的并列时哪个项目离开缓存。 当你这样做时,你只需要接受一个IEqualityComparer,而不是一个IComparer

正如我在评论中提到的,您可以添加一个帮助程序方法,该方法可能会使基本用例更简单一些:

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);
好的,

下次试试。以下是 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指针。

与其在构造函数中采用IEqualityComparerIComparer,不如尝试采用定义GetHashCode()IComparer和lambda。然后基于 if(IComparer==0)GetHashCode() = lambda 构建IEqualityComparer

虽然我会说它很小,但当IComparer返回 0 时,您仍然有 HashCode 不匹配的风险。如果你想让代码的用户非常清楚,你总是可以通过在构造函数中采用两个lambda来扩展策略:Func<T,T,int>用于IComparerIEqualityComparerFunc<T,int>用于GetHashCode

相关文章:
  • 没有找到相关文章