在C# 5.0功能之Async一瞥中,简单的介绍了Async CTP的使用,我们一起领略了下一版本的C#可能给我们带来的强大而简单的编写异步执行的代码的方法。
文中提到一个异步方法的返回值有三个选项:
- void
- Task
- Task<T>
什么时候使用哪一种返回类型,是有讲究的。一不小心使用不当,会让代码产生意想不到的结果。为了避免在将同步代码改成异步代码时出现返回类型选择不恰当的情况,给大家介绍ASync选择返回类型的三法则。
(图片来自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中。
界面如下:
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的方法改写成异步方法。
(图片来自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: }
-
法则二、当一个方法属于触发后不用理会什么时候完成的方法,可以直接使用void,例如事件处理函数(Event Handler);
-
法则三、当虽然不需要返回结果,但却需要知道是否执行完成的方法时,返回一个Task,例如示例中的UpdateNameUnknownStudnetAsync方法。
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: }
资源下载:
* Sync 版源代码
* Async 版源代码
Little knowledge is dangerous.