我有 2 个昂贵的通用方法:
public T DoStuff<T>()
{
//return depends on T.
}
public T DoStuffBasedOnString<T>(string input)
{
//return depends on T.
}
对于给定的类型和字符串,它们的返回值永远不会变化。
这些功能可以记忆吗?
我的出发点是从这里获取的 memoize 函数: https://www.aleksandar.io/post/memoization/
public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> f)
{
var cache = new ConcurrentDictionary<T, TResult>();
return a => cache.GetOrAdd(a, f);
}
我在这里用Linqpad 7写了一个demo,看起来可行。
void Main()
{
var doStuff = () => DoStuff<Car>();
var doStuffMemoized = doStuff.Memoize();
var stopWatch = Stopwatch.StartNew();
var car = doStuffMemoized();
Console.WriteLine($"Retrieving car took: {stopWatch.ElapsedMilliseconds} ms");
stopWatch.Reset();
car = doStuffMemoized(); //second call uses cached result and return almost instantly
Console.WriteLine($"Retrieving car took: {stopWatch.ElapsedMilliseconds} ms");
var doStuffBasedOnString = (string arg) => DoStuffBasedOnString<Car>(arg);
var doStuffBasedOnStringM = doStuffBasedOnString.Memoize(x => x);
stopWatch = Stopwatch.StartNew();
var anotherCar = doStuffBasedOnStringM("Volvo 240");
Console.WriteLine($"Retrieving car took: {stopWatch.ElapsedMilliseconds} ms");
stopWatch.Reset();
anotherCar = doStuffBasedOnStringM("Volvo 240"); //second call uses cached result and return almost instantly
Console.WriteLine($"Retrieving car took: {stopWatch.ElapsedMilliseconds} ms");
}
public class Car {
public string Model { get; set; }
public string Make { get; set; }
}
public T DoStuff<T>() where T : class, new()
{
Thread.Sleep(1000);
var result = Activator.CreateInstance<T>();
return result;
}
public T DoStuffBasedOnString<T>(string input)
{
Thread.Sleep(2000);
var result = Activator.CreateInstance<T>();
return result;
}
public static class FunctionalExtensions
{
public static Func<TOut> Memoize<TOut>(this Func<TOut> @this)
{
var dict = new ConcurrentDictionary<string, TOut>();
return () =>
{
string key = typeof(TOut).FullName;
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this());
}
return dict[key];
};
}
public static Func<T1, TOut> Memoize<T1, TOut>(this Func<T1, TOut> @this, Func<T1, string> keyGenerator)
{
var dict = new ConcurrentDictionary<string, TOut>();
return x =>
{
string key = keyGenerator(x);
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this(x));
}
return dict[key];
};
}
}
我之前写过 Memoize 方法,但零参数在 Memoize 中并不常见。我们只是将完整类型名称缓存到 ConcurrentDictionary 中。对于接受字符串参数的方法中的“密钥生成器”,我只使用 'x => x' ,这是一种方法,您可以在该方法中告诉您如何指定进入 ConcurrentDictionary 的密钥,该密钥控制我们是否应该缓存。
Linqpad 屏幕截图显示其工作原理。 (我也调试了一下)
现在,请注意,一旦您拥有接受一个参数的 memoize 方法,您可能希望拥有也接受多个参数的重载,它们应该相当容易扩展。
我在这里为感兴趣的人写了一篇文章,它最多有四个论点。有一些内置库已经实现了 Memoize。例如,GitHub 库上的 LanguageExt for C#。我见过的大多数示例都只使用字典,只有在您确实怀疑需要它来保证线程安全时才使用 ConcurrentDictionary,有时您的应用程序仅以不必要的方式运行。