选好 Async 函数的返回类型

简介:

C# 5.0功能之Async一瞥中,简单的介绍了Async CTP的使用,我们一起领略了下一版本的C#可能给我们带来的强大而简单的编写异步执行的代码的方法。

文中提到一个异步方法的返回值有三个选项:

  • void
  • Task
  • Task<T>

什么时候使用哪一种返回类型,是有讲究的。一不小心使用不当,会让代码产生意想不到的结果。为了避免在将同步代码改成异步代码时出现返回类型选择不恰当的情况,给大家介绍ASync选择返回类型的三法则

image

(图片来自Bing搜索)

 

(还是申明一下:本文的例子基于Async CTP SP1 Refresh完成。由于 Async还处于CTP阶段,很多东西还在讨论,因此,也许到正式发布的时候,细节还会变动。)

 

假设有一个学生类,包括学号ID姓名Name属性:

   1:      public class Student
   2:      {
   3:          public int ID { get; set; }
   4:          public string Name { get; set; }
   5:   
   6:          public override string ToString()
   7:          {
   8:              return string.Format("ID: {0}, Name: {1}.", ID, Name);
   9:          }
  10:      }

然后,有一组学生记录的StudentRepository:

   1:      public class StudentRepository
   2:      {
   3:          ICollection<Student> storage;
   4:   
   5:          public ICollection<Student> GetStudents()
   6:          {
   7:              var studentCollection = CreateStudents();
   8:              UpdateNameUnknownStudent(studentCollection);
   9:              return studentCollection;
  10:          }
  11:   
  12:          private ICollection<Student> CreateStudents()
  13:          {
  14:              Thread.Sleep(2000);
  15:              storage = new Collection<Student>()
  16:              {
  17:                  new Student(){ ID=1 },
  18:                  new Student(){ ID=2 }
  19:              };
  20:              return storage;
  21:          }
  22:   
  23:          private void UpdateNameUnknownStudent(ICollection<Student> studentCollection)
  24:          {
  25:              foreach (var student in studentCollection)
  26:              {
  27:                  Thread.Sleep(1000); // Someoperation time like db access or so.
  28:                  student.Name = student.Name ?? "Unknown";
  29:              }
  30:          }
  31:      }

其中,12行的CreateStudents方法模拟了学生对象的创建,它返回一个学生集合;

第23行UpdateNameUnkownStudent方法遍历学生集合,并且,如果学生的姓名为空,将学生姓名设置成“Unknown”

第5行GetStudents()作为公开的接口,先后调用CreateStudnets和UpdateNameUnknownStudent方法,并将结果返回。

为了模拟现实代码中例如数据库等操作的处理时间,在第14行和第27行分别加了延时。

 

然后,创建一个WPF UI,调用上面的StudentRepository类,把结果存放到一个Label中。

界面如下:

image

Demo按钮的事件处理代码如下:

   1:          private void btnDemo_Click(object sender, RoutedEventArgs e)
   2:          {
   3:              StudentRepository repository = new StudentRepository();
   4:              var studentCollection = repository.GetStudents();
   5:              StringBuilder builder = new StringBuilder(studentCollection.Count);
   6:              foreach (var student in studentCollection)
   7:              {
   8:                  builder.AppendLine(student.ToString());
   9:              }
  10:              lblResult.Content = builder.ToString();
  11:          }

执行代码,虽然代码能够得到正确结果,但是,在点击Demo按钮后,界面出现了几秒钟的死锁。为了提供更好的用户体验,我们决定把学生创建和为空名学生记录添加Unknown的方法改写成异步方法。

image

(图片来自Bing搜索)

 

首先看CreateStudents方法,它返回一个ICollection<Student>的集合。

  • 法则一:对于需要返回对象的方法,我们添加async关键字后,改为返回Task<T>,也即,改为:
   1:          private async Task<ICollection<Student>> CreateStudentsAsync()
   2:          {
   3:              await TaskEx.Delay(2000);
   4:              storage = new Collection<Student>()
   5:              {
   6:                  new Student(){ ID=1 },
   7:                  new Student(){ ID=2 }
   8:              };
   9:              return storage;
  10:          }

 

接下来,我们看UpdateNameUnknownStudent方法,由于它不返回结果,我们直接加上async:

 
   1:          private async void UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }

相应的GetStudent方法和Demo按钮也加上async关键字和await等待结果后,重新编译运行。点击Demo按钮。我们发现,界面是不死锁了,但是结果出错了(就说会产生意想不到的结果吧):

ID: 1, Name: . 
ID: 2, Name: .

 

显然,UpdateNameUnknownStudentAsync没有运行。让我们来仔细的看一下执行流

首先,Demo按钮Click事件调用者调用GetStudentAsync,为理清调用层次,我们记btnDemo_Click方法为L1方法

   1:          private async void btnDemo_Click(object sender, RoutedEventArgs e)
   2:          {
   3:              StudentRepository repository = new StudentRepository();
   4:              var studentCollection = await repository.GetStudentsAsync();
   5:              StringBuilder builder = new StringBuilder(studentCollection.Count);
   6:              foreach (var student in studentCollection)
   7:              {
   8:                  builder.AppendLine(student.ToString());
   9:              }
  10:              lblResult.Content = builder.ToString();
  11:          }

当执行到第4行时,遇到await关键字,调用GetStudentsAsync方法(记为L2方法),并且进入等待状态,等待L2结果

GetStudentAsync(L2)此时代码如下:

   1:          public async Task<ICollection<Student>> GetStudentsAsync()
   2:          {
   3:              var studentCollection = await CreateStudentsAsync();
   4:              UpdateNameUnknownStudentAsync(studentCollection); // We have a problem here …
   5:              return studentCollection;
   6:          }
当它执行第3行代码,调用CreateStudnetsAsync以后,遇到await关键字,等待CreateStudentAsync的完成。
CretaeStudent方法完成后,L2方法继续执行到调用UpdateNameUnknownStudentAsync方法(记为L3方法):
   1:          private async void UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }
 
L3执行到第5行时,遇到了await,当前线程开始等待TaskEx.Delay完成; 同时,它会检查它的调用者(L2)是否有代码可以在当前线程上执行。由于在L2中,并 没有await L3方法,因此,当L3中await一个结果时,L2所在进程会执行下一个语句:return studentCollection。
L1中的await等到了L2的return,进而进行下一语句……然而,此时此刻,添加Unknown的M3其实还没有完成。前面看到的残缺的结果就这样被显示了出来。
 
引入这个问题的 原因是,由于在有async关键字的情况下,void或者返回Task都不需要在代码中显式的使用return,我们在改写UpdateNameStudentAsync方法时, 没有仔细考虑应当使用void还是Task作为返回类型。因此,到底是返回Task还是保持void是一个在刚开始使用Async时经常遇到的问题。
明白了代码执行流以后,判断方法就不难了:
  • 法则二、当一个方法属于 触发后不用理会什么时候完成的方法,可以直接使用void例如事件处理函数(Event Handler);
  • 法则三、当虽然不需要返回结果,但却需要知道是否执行完成的方法时,返回一个Task,例如示例中的UpdateNameUnknownStudnetAsync方法。
 
在把示例中UpdateNameUnknownStudentAsync方法改成如下形式,并且在GetStudentAsync方法中await它以后,我们便可以得到预期的结果了:
   1:          private async Task UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }
 
image
 
写在最后
Async/await的引入虽然为提供了我们书写异步执行代码的捷径,但是,要用好这把双刃剑,得在 理解Async代码的执行流程的基础上 不断总结多练多用啊。
 
 

资源下载:

* Sync 版源代码

* Async 版源代码

Async CTP SP1 Refresh.



本文转自Work Hard Work Smart博客园博客,原文链接:http://www.cnblogs.com/linlf03/archive/2012/03/16/2400822.html,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
缓存 Python
深度解密为什么实例在调用方法时会将自身传给 self 参数(一)
深度解密为什么实例在调用方法时会将自身传给 self 参数
45 0
|
2月前
|
设计模式 Python
深度解密为什么实例在调用方法时会将自身传给 self 参数(二)
深度解密为什么实例在调用方法时会将自身传给 self 参数(二)
36 1
|
5月前
|
存储 固态存储 Serverless
函数计算操作报错合集之创建云函数并设置代码从Bucket获取时,返回403错误,该如何解决
Serverless 应用引擎(SAE)是阿里云提供的Serverless PaaS平台,支持Spring Cloud、Dubbo、HSF等主流微服务框架,简化应用的部署、运维和弹性伸缩。在使用SAE过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
5月前
|
监控 Serverless 异构计算
函数计算操作报错合集之GPU服务请求返回了404错误是什么原因
在使用函数计算服务(如阿里云函数计算)时,用户可能会遇到多种错误场景。以下是一些常见的操作报错及其可能的原因和解决方法,包括但不限于:1. 函数部署失败、2. 函数执行超时、3. 资源不足错误、4. 权限与访问错误、5. 依赖问题、6. 网络配置错误、7. 触发器配置错误、8. 日志与监控问题。
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
|
6月前
|
JavaScript
js -- 函数总结篇,函数提升、动态参数、剩余参数、箭头函数、this指向......
js -- 函数总结篇,函数提升、动态参数、剩余参数、箭头函数、this指向......
|
JSON 小程序 JavaScript
小程序根据返回值的int类型渲染不同的状态
小程序根据返回值的int类型渲染不同的状态
134 0
|
7月前
|
Java C++ Python
函数的参数列表
函数的参数列表
146 2
|
存储 PHP
PHPlstat函数的使用方法与实例解析
PHP是一种广泛应用于Web开发的编程语言,它的开放性、通用性和易用性使其成为了Web领域中的主流语言。在PHP编程中,我们经常需要使用到一些函数来完成任务,其中非常重要的一个函数就是“PHPlstat”。这个函数可以用来获取文件的相关信息,本文将介绍PHPlstat函数的使用方法以及一些实例解析。
88 0
|
前端开发
如何获取promise对象的值
如何获取promise对象的值
1538 0