我有一个场景,我的服务类与 API 通信并填充列表。此方法是
public async Task<List<T>> Foo()
在这种方法中,我正在执行异步等待以从 API 检索数据并反序列化为列表。
问题是,我想做这样的事情:
if (list is cached in memory)
//serve data from the in memory list
else
//await get data from the API
但是 if 语句第一部分中的返回类型是List<T>
而第二部分的返回类型是Task<List<T>>
我该怎么做?我可以只让我的内存中列表成为任务吗?(也许我可以将列表包装在任务中)?
谢谢大家!
我在这里不讨论技术细节,但await
将其Task<T>
解包到T
中,将async
方法包装成将其返回值T
包装到Task<T>
中。
因此,如果您的方法签名就像您询问的那样
public async Task<List<T>> Foo()
那么你的返回值与你的注释不同:
GetDataFromApi(); // results in Task<List<T>>
await GetDataFromApi(); // results in List<T>
所以无论如何,async Task<List<T>>
的return
声明期望List<T>
而不是Task<List<T>>
public async Task<List<T>> Foo()
{
if (list is cached in memory)
return list; //serve data from the in memory list
else
return await GetDataFromApi(); //await get data from the API
}
如果您不使用异步等待,则可以将结果包装在已完成的任务中:
public Task<List<T>> Foo()
{
if (list is cached in memory)
return Task.FromResult(list); //serve data from the in memory list
else
return GetDataFromApi(); //await get data from the API
}
请注意,在第二个示例中,async
和await
都将被删除。
我经常发现Lazy<T>
在使用只需要在第一次调用时计算值的函数时很有用。
我会考虑使用斯蒂芬·克利里(Stephen Cleary)从他的AsyncEx库中AsyncLazy<T>
,该库是Lazy<T>
的异步版本,非常适合缓存值属性,您可以将方法替换为属性:
public class SomeClass<T>
{
public SomeClass()
{
Foo = new AsyncLazy<List<T>>(async ()=>
{
var data = await GetDataAsync();
return data;
});
}
public AsyncLazy<List<T>> Foo { get; }
}
并像这样使用该属性:
var someClass = /*get SomeClass somehow*/
var foo = await someClass.Foo;
有关异步缓存值的详细信息,请参阅 Stephen Cleary 的异步 OOP 3:属性博客文章。
这称为记忆。函数会记住以前的值,因此不必重新计算它们。这是函数式语言中常用的技术。
您可以创建一个泛型memoize
函数,该函数采用一种方法并处理缓存。这在 C# 7 中很容易编写。在 C# 6 中,您必须定义一个有点丑陋的 Func 变量:
public Func<TIn, TOut> Memoize<TIn, TOut>(Func<TIn, TOut> f)
{
var cache = new Dictionary<TIn, TOut>();
TOut Run (TIn x)
{
if (cache.ContainsKey(x))
{
return cache[x];
}
var result = f(x);
cache[x] = result;
return result;
}
return Run;
}
拥有 Memoize 后,您可以将任何函数(包括任何异步函数)转换为可缓存函数,例如:
async Task<List<Order>> foo(int customerId)
{
..
var items= await ...;
return items
}
var cachedFunc=Memoize<int,Task<List<Order>>>(foo);
...
var orders=await cachedFunc(someId);
var sameOrders=await cachedFunc(someId);
Debug.Assert(orders=newOrders);
您可以通过创建MemoizeAsync
版本来稍微简化代码:
public Func<TIn, Task<TOut>> MemoizeAsync<TIn, TOut>(Func<TIn, Task<TOut>> f)
{
var cache = new Dictionary<TIn, TOut>();
async TOut Run (TIn x)
{
if (cache.ContainsKey(x))
{
return cache[x];
}
var result = await f(x);
cache[x] = result;
return result;
}
return Run;
}
这将创建缓存的函数,而无需在类型列表中指定任务:
var cachedFunc=MemoizeAsync<int,List<Order>>(foo);
更新
这就是您在不测试代码的情况下更改帖子所得到的。感谢塞维的注意
存储任务而不是结果可以使代码更简单,而无需更改调用方:
public Func<TIn, System.Threading.Tasks.Task<TOut>> MemoizeAsync<TIn, TOut>(Func<TIn, Task<TOut>> f)
{
var cache = new ConcurrentDictionary<TIn, System.Threading.Tasks.Task<TOut>>();
Task<TOut> Run (TIn x) => cache.GetOrAdd(x, f);
return Run;
}
在 C# 4 中,相同的代码将是:
public Func<TIn, Task<TOut>> MemoizeAsync<TIn, TOut>(Func<TIn, Task<TOut>> f)
{
var cache = new ConcurrentDictionary<TIn, Task<TOut>>();
return x => cache.GetOrAdd(x, f);
}
记忆并调用此测试函数两次,仅打印出一次消息:
Task<string> test(string x)
{
Console.WriteLine("Working");
return Task.FromResult(x);
}
var cachedFunc=MemoizeAsync<string,string>(test);
var results=await Task.WhenAll(cachedFunc("a"),cachedFunc("a"));
这将打印:
Working
a a
您应该将列表包装在任务中。使用此代码。
if (list is cached in memory)
return Task.FromResult(list);
else
return await GetResultFromApi();//returns Task<List<T>>