返回内存中(缓存)集合或异步等待响应中的任务



我有一个场景,我的服务类与 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
}

请注意,在第二个示例中,asyncawait都将被删除。

我经常发现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>>

最新更新