关于返回值,实际这里有两个层面(本文说的是第二个层面):
一是函数的返回值:
void 表示函数无返回值。
Task 表示函数返回值是 Task,但是异步无返回值。
二是这个异步的返回值。
Task 表示函数返回值是 Task,但是异步无返回值。
Task<T> 表示函数返回值是 Task<T>,异步返回值是 T。
为什么要返回 Task,不直接 void?为什么要返回 Task<T>,不直接返回 T?
其实大多数场景确实是可以这样做的,但是有些开发中,比如 handler,返回 Task、Task<T>,目的是让你实现这个 handler 时,必须使用异步去实现。请参见:C# 中 Action 和 Func。
若觉得本文复杂、头晕,直接看最后结论就是了。
先看 Task(无返回值)
private static async Task Do1Async() { await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine($"Do1Async {DateTime.Now}"); }); // 此处不能 return await,见后解释。 } private static Task Do1() { return Task.Run(() => { Thread.Sleep(3000); Console.WriteLine($"Do1 {DateTime.Now}"); }); }
如上,Do1Async 中的 Async 是微软推荐 async 修饰时加上的,但并非必须。
内部 await 了,方法修饰符必须是 async。
Task.Run await 了,可以不用 return,此时它认为返回值类型是 Task<TResult> 中的 TResult,而上述代码并没有指定 TResult,所以不需要返回值(所以上述代码中说:不能 return await)。
Task.Run 没有 await,则需要 return,此时它认为返回值类型是 Task。
由于用 await 没有用 return,所以 await 后面还可以写语句。
除了上述最后一条外,实际上,上述代码并没有太多区别:
private static void Do() { Console.WriteLine($"a {DateTime.Now}"); Do1Async(); // 此处 Visual Studio 会警告,说这里会异步,建议用 await(仅仅是建议)。 Console.WriteLine($"b {DateTime.Now}"); Do1(); // 此处 Visual Studio 不会警告。 Console.WriteLine($"end {DateTime.Now}"); } static void Main(string[] args) { Do(); Console.WriteLine($"out {DateTime.Now}"); Console.ReadKey(); }
结果如下:
a 2022-05-04 19:48:26 b 2022-05-04 19:48:26 end 2022-05-04 19:48:26 out 2022-05-04 19:48:26 Do1 2022-05-04 19:48:29 Do1Async 2022-05-04 19:48:29
如果看不惯警告,那么我们加上 await,则 Do 方法也要标记 async,且返回值改为 Task,同时 Visual Studio 建议我们改方法名为 DoAsync。
同时 Main 方法调用 DoAsync 也会得到警告。
所以指定了 await,就形成了一个传递链,各层级的调用方,都必须是 await + async Task。
private static async Task DoAsync() { Console.WriteLine($"a {DateTime.Now}"); await Do1Async(); // 此处 Visual Studio 不会警告。 Console.WriteLine($"b {DateTime.Now}"); Do1(); // 此处 Visual Studio 会警告,说这里会异步,建议用 await(仅仅是建议)。我们可以加上,不会报错。 Console.WriteLine($"end {DateTime.Now}"); }
所以:这里加上 await 是什么意思呢?
如上 Main 调用 DoAsync,输出 a,然后看到 await,于是说:好的,你忙,我先退出去执行其他的了。于是继续执行 Main 中的其他代码(输出 out),再等一会儿,await Do1Async 延时到了,然后输出 Do1Async,然后输出 b,然后输出 end,然后再是输出 Do1(因为 Do1 没有用 await 调用,所以 end 没被阻塞到)。
其实根据我们业务需要吧,如果我就要异步,后面代码我不关心顺序,那完全可以用 return。
再看 Task<TResult>(有返回值)
前面提到了,有无 await 对返回值的解释不同:
有 await:对于 Task<TResult>,必须 return TResult;对于 Task,不能 return。
无 await:对于 Task<TResult>,必须 return Task<TResult>;对于 Task,必须 return Task。
private static async Task<int> Do1Async() { int m = await Task.Run(() => { Thread.Sleep(1000); return 1; }); return m; // 也可以不要 int m,直接 return await,我们换种写法,以表明 = 与 await 的位置。 } private static Task<int> Do1() { return Task.Run(() => { Thread.Sleep(1000); return 2; }); } private static async Task DoAsync() { Console.WriteLine(Do1Async()); Console.WriteLine(Do1()); }
如上,DoAsync 会得到一个警告,建议 await。
private static async Task DoAsync() { Console.WriteLine(Do1Async()); Console.WriteLine(await Do1()); }
我们给 Do1() 加上 await,就没有警告了,也就是说在这个调用链条上,要用一个 await,才不会得到警告。
但是,你以为没警告,就正确吗?
private static async Task DoAsync() { Console.WriteLine(Do1Async()); Console.WriteLine(await Do1()); }
得到的结果是:
System.Threading.Tasks.Task`1[System.Int32] 2
也就是说:在取值的时候,要使用 await,得到的才是 Task<TResult> 中的 TResult,否则它所得到的就是 Task。
于是我们认为 await 有两个作用
1、阻塞我后面的代码(但不阻塞调用者外面的)。
2、使返回值由 Task 变为 Task<TResult> 中的 TResult。
针对第二条,我们要注意 await 应该放在调用的地方,放错地方白搭,比如上面 Do1Async,明明返回的是 int m,可是输出则是 System.Threading.Tasks.Task`1[System.Int32],即 Task<int>。
简简单单
本文前面,其实为了去探究 await、async 及 Task、Task<T> 的返回值,其实是故意把事情弄复杂了。
我们在写返回值类型为 Task、Task<T> 的函数时,就直接 return 就是了,也就是文中的 Do1 写法。
至于 await、async 则在调用它的函数中使用就是了。