.NET(C#): Task.Unwrap扩展方法和async Lambda
目录
- Task.Unwrap基本使用
- Task.Factory.StartNew和Task.Run的Unwrap操作
- 使用案例:LINQ中的async Lambda
返回目录
Task.Unwrap基本使用
这个扩展方法定义在TaskExtensions类型中,命名空间在System.Threading.Tasks。Unwrap会把嵌套的Task<Task>或者Task<Task<T>>的结果提取出来。
就像这样,不用Unwrap的话:
static void Main(string[] args)
{
doo();
Task.Delay(-1).Wait();
}
static async void doo()
{
//运行嵌套的Task
//Task返回Task<Task<string>>
//第一个await后result类型为Task<string>
var result = await Task.Run<Task<string>>(() =>
{
var task = Task.Run<string>(() =>
{
Task.Delay(1000).Wait();
return "Mgen";
});
return task;
});
//第二个await后才会返回string
Console.WriteLine(await result);
}
使用Unwrap后,结果可以直接从嵌套Task中提取出来:
static async void doo()
{
//运行嵌套的Task
//Task返回Task<Task<string>>
//await后类型为Task<string>,Unwrap后result类型为string
var result = await Task.Run<Task<string>>(() =>
{
var task = Task.Run<string>(() =>
{
Task.Delay(1000).Wait();
return "Mgen";
});
return task;
}).Unwrap();
//不需要await,result已经是string
Console.WriteLine(result);
}
返回目录
Task.Factory.StartNew和Task.Run的Unwrap操作
简单地讲,Task.Factory.StartNew和Task.Run区别之一就有Task.Run会自动执行Unwrap操作,但是Task.Factory.StartNew不会,Task.Run就是Task.Factory.StartNew的更人性化封装,而Task.Factory.StartNew则是原始的执行。(另外关于更多的区别,推荐PFX Team的一篇非常给力的文章:Task.Run vs Task.Factory.StartNew)。
通过代码来验证:
var task1 = Task.Factory.StartNew(async () => "Mgen");
var task2 = Task.Run(async () => "Mgen");
Console.WriteLine(task1.GetType());
Console.WriteLine(task2.GetType());
输出:
System.Threading.Tasks.Task`1[System.Threading.Tasks.Task`1[System.String]]
System.Threading.Tasks.UnwrapPromise`1[System.String]
可以看到
使用Task.Factory.StartNew会返回原始的Task<Task<string>>。但是Task.Run则会直接返回async Lambda的结果,中间的Unwrap操作会自动进行。
返回目录
使用案例:LINQ中的async Lambda
文章讲述到这里,或许读者在想上述情况会不会很少见?不,任何使用async Lambda的情况都可能会出现上述情况,比如最近在搞一个WinRT的项目,使用LINQ去转换一些数据,但是许多WinRT的API只有异步执行的,这类问题就会出现,下方示例:
我们来进行一个再简单不过的LINQ Select操作,把一堆int转换成string,只不过转换过程是异步的,来看代码:
static void Main(string[] args)
{
doo();
Task.Delay(-1).Wait();
}
static void doo()
{
//int数据
var ints = Enumerable.Range(1, 10);
//转换并输出结果
foreach (var str in ints.Select(async i => await Int2StringAsync(i)))
Console.WriteLine(str);
}
//异步将int转换成string
static async Task<string> Int2StringAsync(int i)
{
return await Task.Run<string>(() => i.ToString());
}
上面代码正确吗?很多人会认为没问题的,Select方法通过一个async Lambda调用异步转换方法,并使用await异步等待结果,那么async Lambda返回string,str类型也是string,最后输出所以字符串。
但事实上程序运行后会输出一连串的“System.Threading.Tasks.Task`1[System.String]”,str变量根本不是string,而是Task<string>。上边的推断错在(上面黄字标注的)“async Lambda返回string”,async Lambda的结果并没有被await,因此它会返回Task<string>,所以Select会返回一些列的Task<string>。
最简单的解决方案是,在处理结果的时候加上await,如下:
Console.WriteLine(await str);
但是在多数情况下,我们使用LINQ会直接赋值给数据对象,这个数据对象的类型是固定的,这里需要一种更彻底的解决方案,直接在LINQ Select上解决掉。
那么还有一种解决方案就是不使用async Lambda,就不存在嵌套Task的问题,直接在Select中返回异步方法的Task的Result属性:
//int数据
var ints = Enumerable.Range(1, 10);
//Select调用异步方法
IEnumerable<string> strs = ints.Select(i => Int2StringAsync(i).Result);
如果一定要使用async Lambda,则必须将嵌套的Task进行Unwrap。(当然这里更多的是为了讨论技术本身,实际工作中没必要这么钻牛角尖呵呵。)
结合上面讲到的知识,使用Task.Factory.StartNew需要进行一个Unwrap,然后返回Task<T>的结果作为Select方法的最终返回值,代码:
//int数据
var ints = Enumerable.Range(1, 10);
//Select调用异步方法
IEnumerable<string> strs = ints.Select(i =>
Task.Factory.StartNew(async () => await Int2StringAsync(i)).Unwrap().Result);
而Task.Run的话,不需要Unwrap:
IEnumerable<string> strs = ints.Select(i =>
Task.Run(async () => await Int2StringAsync(i)).Result);
TAG: