缓存功能结果

时间:2009-07-22 16:57:57

标签: c# .net caching

为了好玩,我正在玩一个类来轻松缓存功能结果。基本的想法是你可以使用你想要的任何功能 - 虽然你只想将它用于相对昂贵的功能 - 并且可以轻松地将它包装起来使用相对便宜的字典查找,以便以后使用相同的参数运行。真的没什么可说的:

public class AutoCache<TKey, TValue> 
{  
    public AutoCache(Func<TKey, TValue> FunctionToCache)
    {
        _StoredFunction = FunctionToCache;
        _CachedData = new Dictionary<TKey, TValue>();
    }

    public TValue GetResult(TKey Key)
    {
        if (!_CachedData.ContainsKey(Key)) 
            _CachedData.Add(Key, _StoredFunction(Key));
        return _CachedData[Key];
    }

    public void InvalidateKey(TKey Key)
    {
        _CachedData.Remove(Key);
    }

    public void InvalidateAll()
    {
        _CachedData.Clear();
    }

    private Dictionary<TKey, TValue> _CachedData;
    private Func<TKey, TValue> _StoredFunction; 
}

不幸的是,还有一些额外的限制使得它没有那么有用。我们还可以添加一些功能以及实现的其他注意事项。我正在寻找关于如何改进以下任何一点的想法:

  • 这需要一个函数,它为给定的一组参数返回相同的结果(它必须是无状态的)。可能没有办法改变这一点。
  • 它仅限于非常狭窄的代表范围。我们可以扩展它以轻松地工作任何接受至少一个参数并返回值的函数,可能通过在匿名类型中包装参数?或者我们是否需要为我们想要支持的每个Func委托提供额外的实施?如果是这样,我们可以构建一个抽象类来使这更容易吗?
  • 这不是线程安全的。
  • 没有自动失效。这使垃圾收集变得危险。你需要保持它一段时间以使它有用,这意味着你不会真的丢弃旧的和可能不需要的缓存项。
  • 我们可以继承这个,以便在函数只有一个参数的情况下使缓存成为双向的吗?

作为一个参考点,如果我在实际代码中使用它,我认为最可能的地方是作为业务逻辑层的一部分,我使用此代码将数据访问层中的方法包装起来来自查找表的数据。在这种情况下,相对于字典,数据库之旅将是昂贵的,并且几乎总是只有一个“键”值用于查找,因此它是一个很好的匹配。

4 个答案:

答案 0 :(得分:8)

这种自动缓存功能结果的另一个名称是memoization。对于公共接口,请考虑以下几点:

public Func<T,TResult> Memoize<T,TResult>(Func<T,TResult> f)

...只需使用多态就可以将T存储在对象字典中。

扩展委托范围可以通过currying和部分功能应用来实现。像这样:

static Func<T1,Func<T2,TResult>> Curry(Func<T1,T2,TResult> f)
{
    return x => y => f(x, y);
}
// more versions of Curry

由于Curry将多个参数的函数转换为单个参数的函数(但可能返回函数),因此返回值本身有资格进行memoization。

另一种方法是使用反射来检查委托类型,并将元组存储在字典中,而不仅仅是参数类型。一个简单的元组只是一个数组包装器,其哈希码和相等逻辑使用深度比较和散列。

弱引用可以帮助失效,但使用WeakReference键创建字典很棘手 - 最好在运行时的支持下完成(WeakReference值更容易)。我相信那里有一些实现。

通过在内部字典上锁定突变事件可以轻松完成线程安全,但拥有无锁字典可以提高重度并发场景的性能。那个字典可能更难创造 - 尽管有一个有趣的presentation on one for Java here

答案 1 :(得分:2)

哇 - 什么意外 - 我刚刚发布了一个关于opaque keys in C#的问题......因为我正在尝试实现与功能结果缓存相关的东西。多好笑。

这种类型的元编程对于C#来说可能很棘手......特别是因为泛型类型参数会导致代码重复笨拙。您经常最终会在多个地方重复使用不同类型参数的相同代码,以实现类型安全。

所以这是我的方法的变体,它使用我的opaque键模式和闭包来创建可缓存的函数。下面的示例演示了带有一个或两个参数的模式,但它相对容易扩展到更多。它还使用扩展方法来创建用于包装Func&lt;&gt;的透明图案。使用可缓存的Func&lt;&gt;使用AsCacheable()方法。闭包捕获与该函数关联的缓存 - 并使其对其他调用者保持透明。

这种技术有许多与你的方法相同的限制(线程安全,保留引用等) - 我怀疑它们不是很难克服 - 但它支持一种简单的方法来扩展到多个参数,它允许可缓存的函数完全可以用常规函数替换 - 因为它们只是一个包装器委托。

值得注意的是,如果您创建CacheableFunction的第二个实例 - 您将获得一个单独的缓存。这既可以是力量也可以是弱点......因为在某些情况下你可能没有意识到这种情况正在发生。

以下是代码:

public interface IFunctionCache
{
    void InvalidateAll();
    // we could add more overloads here...
}

public static class Function
{
    public class OpaqueKey<A, B>
    {
        private readonly object m_Key;

        public A First { get; private set; }
        public B Second { get; private set; }

        public OpaqueKey(A k1, B k2)
        {
            m_Key = new { K1 = k1, K2 = k2 };
            First = k1;
            Second = k2;
        }

        public override bool Equals(object obj)
        {
            var otherKey = obj as OpaqueKey<A, B>;
            return otherKey == null ? false : m_Key.Equals(otherKey.m_Key);
        }

        public override int GetHashCode()
        {
            return m_Key.GetHashCode();
        }
    }

    private class AutoCache<TArgs,TR> : IFunctionCache
    {
        private readonly Dictionary<TArgs,TR> m_CachedResults 
            = new Dictionary<TArgs, TR>();

        public bool IsCached( TArgs arg1 )
        {
            return m_CachedResults.ContainsKey( arg1 );
        }

        public TR AddCachedValue( TArgs arg1, TR value )
        {
            m_CachedResults.Add( arg1, value );
            return value;
        }

        public TR GetCachedValue( TArgs arg1 )
        {
            return m_CachedResults[arg1];
        }

        public void InvalidateAll()
        {
            m_CachedResults.Clear();
        }
    }

    public static Func<A,TR> AsCacheable<A,TR>( this Func<A,TR> function )
    {
        IFunctionCache ignored;
        return AsCacheable( function, out ignored );
    }

    public static Func<A, TR> AsCacheable<A, TR>( this Func<A, TR> function, out IFunctionCache cache)
    {
        var autocache = new AutoCache<A,TR>();
        cache = autocache;
        return (a => autocache.IsCached(a) ?
                     autocache.GetCachedValue(a) :
                     autocache.AddCachedValue(a, function(a)));
    }

    public static Func<A,B,TR> AsCacheable<A,B,TR>( this Func<A,B,TR> function )
    {
        IFunctionCache ignored;
        return AsCacheable(function, out ignored);
    }

    public static Func<A,B,TR> AsCacheable<A,B,TR>( this Func<A,B,TR> function, out IFunctionCache cache )
    {
        var autocache = new AutoCache<OpaqueKey<A, B>, TR>();
        cache = autocache;
        return ( a, b ) =>
                   {
                       var key = new OpaqueKey<A, B>( a, b );
                       return autocache.IsCached(key)
                                  ? autocache.GetCachedValue(key)
                                  : autocache.AddCachedValue(key, function(a, b));
                   };
    }
}

public class CacheableFunctionTests
{
    public static void Main( string[] args )
    {
        Func<string, string> Reversal = s => new string( s.Reverse().ToArray() );

        var CacheableReverse = Reversal.AsCacheable();

        var reverse1 = CacheableReverse("Hello");
        var reverse2 = CacheableReverse("Hello"); // step through to prove it uses caching

        Func<int, int, double> Average = (a,b) => (a + b)/2.0;
        var CacheableAverage = Average.AsCacheable();

        var average1 = CacheableAverage(2, 4);
        var average2 = CacheableAverage(2, 4);
    }
}

答案 2 :(得分:0)

因为这主要是为了教育价值 - 你应该看看WeakReference类,它允许GC在多线程环境中清除你的类中未使用的句柄。这是.NET中非常常见的缓存模式

那说 - 告诫Emptor!每个缓存都不同。通过构建一个包罗万象的解决方案,你经常会遇到一个病态的情况,你的“缓存”只是一个美化的字典,有许多复杂的辅助方法,使你的代码很难。

答案 3 :(得分:0)

我正在使用这个简单的扩展,在这种情况下使用MemoryCache:

public static class FuncHelpers
{
   /// <summary>
   /// Returns a same function wrapped into cache-mechanism
   /// </summary>
   public static Func<TIn, TRes> Cached<TIn, TRes>(this Func<TIn, TRes> func, 
      Func<TIn,string> keySelector, 
      Func<TIn,CacheItemPolicy> policy)
    {
        var cache = new MemoryCache(Guid.NewGuid().ToString());

        Func<TIn, TRes> f = (item) =>
        {
            var key = keySelector(item);
            var newItem = new Lazy<TRes>(() => func(item));
            var oldItem = cache.AddOrGetExisting(key,newItem , policy(item)) as Lazy<TRes>;
            try
            {
                return (oldItem ?? newItem).Value;
            }
            catch
            {
                // Handle cached lazy exception by evicting from cache.
                cache.Remove(key);
                throw;
            }

        };
        return f;
    }

   //simplified version
   public static Func<TIn, TRes> Cached<TIn, TRes>(this Func<TIn, TRes> func, Func<TIn, string> keySelector,
        TimeSpan duration)
    {
        if (duration.Ticks<=0) return func;
        return Cached(func, keySelector,
          item => new CacheItemPolicy() {AbsoluteExpiration = DateTimeOffset.Now + duration});

    }
}

示例/用法:(缓存持续时间为42秒):

    public class CachedCalculator
    {
        private Func<int, int> _heavyExpensiveMultiplier;

        public Calculator(Func<int,int> heavyExpensiveMultiplier )
        {
            //wrap function into cached one
            this._heavyExpensiveMultiplier 
              = heavyExpensiveMultiplier.Cached(x =>/*key for cache*/ x.ToString(), TimeSpan.FromSeconds(42));
        }

        //this uses cached algorithm
        public int Compute(int x)
        {
            return _heavyExpensiveMultiplier(x);
        }
    }