C--设计模式-七-

78 阅读28分钟

C# 设计模式(七)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

二十五、空对象模式

本章介绍了空对象模式。

定义

空对象模式不是 GoF 设计模式。我从维基百科上得到这个定义,它是这样说的。

在面向对象的计算机编程中,空对象是指没有引用值或具有已定义的中立(“空”)行为的对象。空对象设计模式描述了此类对象的用途及其行为(或缺乏行为)。它最初发表在程序设计的模式语言系列丛书中。

概念

该模式可以实现“什么都不做”的关系,或者当应用遇到空对象而不是真实对象时,它可以提供默认行为。使用这种模式,我们的核心目标是通过if块避免“空对象检查”或“空协作检查”,并通过提供不做任何事情的默认行为来封装对象的缺失,从而制定一个更好的解决方案。该模式的基本结构如图 25-1 所示。

img/463942_2_En_25_Fig1_HTML.jpg

图 25-1

空对象模式的基本结构

本章从一个看似没问题的程序开始,但它有一个严重的潜在 bug。当您使用潜在的解决方案分析 bug 时,您会理解对空对象模式的需求。那么,让我们跳到下一节。

错误的程序

让我们假设您有两种不同类型的交通工具:BusTrain,,并且一个客户端可以传递不同的输入(例如,ab)来创建一个Bus对象或一个Train对象。下面的程序演示了这一点。当输入有效时,这个程序可以顺利运行,但是当您提供一个无效的输入时,一个潜在的错误就暴露出来了。这是有问题的程序。

using System;

namespace ProgramWithOnePotentialBug
{
    interface IVehicle
    {
        void Travel();
    }
    class Bus : IVehicle
    {
        public static int busCount = 0;
        public Bus()
        {
            busCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Bus");
        }
    }
    class Train : IVehicle
    {
        public static int trainCount = 0;
        public Train()
        {
            trainCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Train");
        }
    }

    class Program
    {
        static void Main(string[] args)

        {
            Console.WriteLine("***This program demonstrates the need of null object pattern.***\n");
            string input = String.Empty;
            int totalObjects = 0;

            while (input != "exit")
            {
                Console.WriteLine("Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.");
                input = Console.ReadLine();
                IVehicle vehicle = null;
                switch (input)
                {
                    case "a":
                        vehicle = new Bus();
                        break;
                    case "b":
                        vehicle = new Train();
                        break;
                    case "exit":
                        Console.WriteLine("Creating one more bus and closing the application");
                        vehicle = new Bus();
                        break;
                }
                totalObjects = Bus.busCount + Train.trainCount;
                vehicle.Travel();
                Console.WriteLine($"Total objects created in the system ={totalObjects}");
            }
        }
    }
}

具有有效输入的输出

你可能有一个眼前的问题;当你输入exit时,你创建了一个不必要的对象。这是真的。我们以后再处理。现在,让我们关注另一个对我们来说更危险的 bug。下面是一些有效输入的输出。

***This program demonstrates the need of null object pattern.***

Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =1
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
b
Let us travel with Train
Total objects created in the system =2
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =3
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.

不需要输入的分析

让我们假设用户错误地提供了一个不同的字符,比如 e ,如下所示。

Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
e

这一次,你得到了一个名为System.NullReferenceException的运行时异常,如图 25-2 所示。

img/463942_2_En_25_Fig2_HTML.jpg

图 25-2

当用户提供无效输入时,会发生运行时异常

潜在的解决办法

您可能想到的直接补救方法是在调用操作之前进行空检查,如下所示。

if (vehicle != null)
{
  vehicle.Travel();
}

分析

先前的解决方案在这种情况下有效。但是请考虑一个企业应用。当您对每个场景进行空检查时,如果您在每个场景中都像这样放置if条件,那么您的代码就会变脏。同时,你可能会注意到维护困难的副作用。空对象模式的概念在类似的情况下很有用。

Point to Remember

在前面的例子中,当用户键入 exit 时,我可以避免创建不必要的对象,如果我使用如下的空条件操作符,也可以避免空检查:

vehicle?.Travel();

该运算符仅在 C# 6 和更高版本中可用。不过,研究一下空对象模式的实现细节对您还是有好处的。例如,当您使用空对象模式时,您可以为这些空对象提供默认行为(最适合您的应用),而不是什么都不做。

真实世界的例子

当有水供应而没有任何内部泄漏时,洗衣机就能正常工作。但是假设有一次,你忘记在开始洗衣服之前供水,但是你按下了开始洗衣服的按钮。在这种情况下,洗衣机不应损坏自身;所以,它可以发出警报声来引起你的注意,并指示此刻没有供水。

计算机世界的例子

假设在客户端-服务器架构中,服务器基于客户端输入进行计算。服务器需要足够智能,不会启动任何不必要的计算。在处理输入之前,它可能希望进行交叉验证,以确保是否需要开始计算,或者应该忽略无效的输入。在这种情况下,您可能会注意到带有空对象模式的命令模式。

基本上,在企业应用中,使用这种设计模式可以避免大量的 null 检查和 if/else 阻塞。下面的实现给出了这种模式的概述。

履行

让我们修改之前讨论过的有问题的程序。这次您通过一个NullVehicle对象处理无效输入。因此,如果用户错误地提供了任何无效数据(换句话说,除了本例中的 ab 之外的任何输入),应用什么都不做;也就是说,它可以通过一个NullVehicle对象忽略那些无效输入,这个对象什么也不做。该类定义如下。

/// <summary>
/// NullVehicle class
/// </summary>
class NullVehicle : IVehicle
{
 private static readonly NullVehicle instance = new NullVehicle();
 private NullVehicle()
 {
  nullVehicleCount++;
  }
 public static int nullVehicleCount;
 public static NullVehicle Instance
 {
  get
  {
    return instance;
  }
 }
 public void Travel()
{
   // Do Nothing
}
}

您可以看到,当我创建一个NullVehicle对象时,我应用了单体设计模式的概念。因为可能有无限多的无效输入,所以在下面的例子中,我不想重复创建NullVehicle对象。一旦有了一个NullVehicle对象,我想重用那个对象。

Note

对于空对象方法,您需要返回任何看起来合理的默认值。在我们的例子中,你不能乘坐一辆不存在的车辆。因此,对于NullVehicle类来说,Travel()方法什么也不做是有道理的。

类图

图 25-3 为类图。

img/463942_2_En_25_Fig3_HTML.jpg

图 25-3

类图

解决方案资源管理器视图

图 25-4 显示了程序的高层结构。

img/463942_2_En_25_Fig4_HTML.jpg

图 25-4

解决方案资源管理器视图

示范

下面是完整的实现。

using System;
namespace NullObjectPattern
{
    interface IVehicle
    {
        void Travel();
    }
    /// <summary>
    /// Bus class
    /// </summary>
    class Bus : IVehicle
    {
        public static int busCount = 0;
        public Bus()
        {
            busCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Bus.");
        }
    }
    /// <summary>
    /// Train class
    /// </summary>
    class Train : IVehicle
    {
        public static int trainCount = 0;
        public Train()
        {
            trainCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Train.");
        }
    }
    /// <summary>
    /// NullVehicle class
    /// </summary>
    class NullVehicle : IVehicle
    {
        private static readonly NullVehicle instance = new NullVehicle();
        private NullVehicle()
        {
            nullVehicleCount++;
        }

        public static int nullVehicleCount;
        public static NullVehicle Instance
        {
            get
            {
                return instance;
            }
        }
        public void Travel()
        {
            // Do Nothing
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Null Object Pattern Demonstration.***\n");
            string input = String.Empty;
            int totalObjects = 0;

            while (input != "exit")
            {
                Console.WriteLine("Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit) ");
                input = Console.ReadLine();
                IVehicle vehicle = null;
                switch (input)
                {
                    case "a":
                        vehicle = new Bus();
                        break;
                    case "b":
                        vehicle = new Train();
                        break;
                    case "exit":
                        Console.WriteLine("Closing the application.");
                        vehicle = NullVehicle.Instance;
                        break;
                    default:
                        Console.WriteLine("Please supply the correct input(a/b/exit)");
                        vehicle = NullVehicle.Instance;
                        break;
                }
                totalObjects = Bus.busCount + Train.trainCount + NullVehicle.nullVehicleCount;
                // No need to do null check now.
                //if (vehicle != null)
                vehicle.Travel();
                //}
                Console.WriteLine("Total objects created in the system ={0}",
                totalObjects);

            }
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Null Object Pattern Demonstration.***

Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
a
Let us travel with Bus.
Total objects created in the system =2
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
c
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
d
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =4
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
exit
Closing the application.
Total objects created in the system =4

分析

我提请你注意以下几点。

  • 无效输入及其影响以粗体显示。

  • 由于空车辆对象/无效输入,对象计数没有增加。

  • 您没有执行任何空值检查。尽管如此,程序执行不会因为无效的用户输入而中断。

问答环节

25.1 在实现的开始,我看到创建了一个额外的对象。这是故意的吗?

为了节省一些计算机内存/存储,我在构造NullVehicle类时遵循了支持早期初始化的单例设计模式。您不希望为每个无效输入创建一个NullVehicle对象,因为您的应用可能会收到大量无效输入。如果您不防范这种情况,大量的NullVehicle对象可能会驻留在系统中(这是无用的),它们会占用大量的计算机内存,这反过来会导致一些不必要的副作用。(例如,系统可能会变慢,应用响应时间可能会增加,等等。)

25.2 什么时候应该使用这种模式?

这种模式在下列情况下很有用。

  • 您不希望遇到NullReferenceException(例如,如果您错误地试图调用一个空对象的方法)。

  • 您喜欢忽略代码中的大量空检查。

  • 你想让你的代码更干净,更容易维护。

Note

在这一章的最后,你会学到这种模式的另一种用法。

25.3 与空对象模式相关的 挑战 有哪些?

您需要注意以下情况。

  • 大多数情况下,您可能希望找到并修复失败的根本原因。所以,如果你扔一个NullReferenceException,那对你来说会更好。您总是可以在try / catch块或try / catch / finally块中处理这些异常,并相应地更新日志信息。

  • 当您无意中想要处理一个根本不存在的对象时,空对象模式可以帮助您实现一个默认行为。但是试图提供这样的默认行为可能并不总是合适的。

  • 空对象模式的不正确实现会抑制程序执行中可能正常出现的真正错误。

25.4。看起来好像空对象像代理一样工作。这是正确的吗?

不会。一般来说,代理在某个时间点作用于真实对象,它们也可能提供一些行为。但是空对象不应该做这样的事情。

25.5。空对象模式总是与 NullReferenceException 相关联。这是正确的吗?

概念是相同的,但是异常名可以不同或特定于语言。例如,在 Java 中,您可以使用此模式来防范 java.lang.NullPointerException,但在 C# 这样的语言中,您使用它来防范 System.NullReferenceException。

最后,我想提请大家注意另一个有趣的点。空对象模式在另一个上下文中很有用。例如,考虑下面的代码段。

            //A case study in another context.
            List<IVehicle> vehicleList = new List<IVehicle>();
            vehicleList.Add(new Bus());
            vehicleList.Add(new Train());
            vehicleList.Add(null);
            foreach (IVehicle vehicle in vehicleList)
            {
                vehicle.Travel();
            }

当你使用前面的代码段时,你再次得到System.NullReferenceException。但是如果你用vehicleList.Add(NullVehicle.Instance);代替vehicleList.Add(null);,就没有运行时异常。因此,您可以轻松地循环,这是该模式的另一个重要用途。

二十六、MVC 模式

本章涵盖了 MVC 模式。

定义

MVC(模型-视图-控制器)是一种架构模式。这种模式通常用于 web 应用和开发强大的用户界面。Trygve Reenskaug 于 1979 年在一篇题为“Smalltalk-80TM 中的应用编程:如何使用模型-视图-控制器”的论文中首次描述了 MVC,这篇论文是在万维网存在之前写的。所以,那时候还没有 web 应用的概念。但是现代的应用是最初概念的改编。一些开发人员宁愿称之为“MVC 架构”,而不是真正的设计模式

维基百科是这样定义的。

模型-视图-控制器(Model-view-controller,MVC)是一种通常用于开发用户界面的架构模式,它将应用分成三个相互连接的部分。这样做是为了将信息的内部表示与信息呈现给用户并被用户接受的方式分开。MVC 设计模式将这些主要组件解耦,允许高效的代码重用和并行开发。 ( https://en.wikipedia.org/wiki/Model-view-controller )

我最喜欢的关于 MVC 的描述来自 Connelly Barnes,他说,

理解 MVC 的一个简单方法:模型是数据,视图是屏幕上的窗口,控制器是两者之间的粘合剂。 ( http://wiki.c2.com/?ModelViewController )

概念

使用这种模式,您可以将用户界面逻辑与业务逻辑分离开来,并以一种可以有效重用的方式分离主要组件。这种方法促进了并行开发。

从定义中可以明显看出,模式由这些主要组件组成:模型、视图和控制器。控制器放置在视图和模型之间,使得它们只能通过控制器相互通信。该模型将数据显示机制与数据操作机制分开。图 26-1 显示了 MVC 模式。

img/463942_2_En_26_Fig1_HTML.jpg

图 26-1

典型的 MVC 架构

需要记住的要点

这些是对该模式中关键组件的简要描述。

  • 视图表示最终输出。它还可以接受用户输入。它是表示层,你可以把它想象成一个图形用户界面(GUI)。你可以用各种技术来设计它。例如,在. NET 应用中,您可以使用 HTML、CSS、WPF 等等,而对于 Java 应用,您可以使用 AWT、Swing、JSF、JavaFX 等等。

  • 模型管理数据和业务逻辑,它充当应用的实际大脑。它管理数据和业务逻辑。它知道如何存储、管理或操作数据,并处理来自控制器的请求。但是这个组件与视图组件是分离的。一个典型的例子是数据库、文件系统或类似的存储。它可以用 Oracle、SQL Server、DB2、Hadoop、MySQL 等等来设计。

  • 控制器是中介。它接受来自视图组件的用户输入,并将请求传递给模型。当它从模型得到响应时,它将数据传递给视图。可以用 C# 设计。NET、ASP.NET、VB.NET、核心 Java、JSP、Servlets、PHP、Ruby、Python 等等。

您可能会注意到不同应用中的不同实现。这里有一些例子。

  • 您可以有多个视图。

  • 视图可以将运行时值(例如,使用 JavaScript)传递给控制器。

  • 您的控制器可以验证用户的输入。

  • 您的控制器可以通过多种方式接收输入。例如,它可以通过 URL 从 web 请求中获取输入,或者通过单击表单上的 Submit 按钮传递输入。

  • 在某些应用中,您可能会注意到模型可以更新视图组件。

简而言之,您需要使用这个模式来支持您自己的需求。图 26-2 、 26-3 和 26-4 显示了 MVC 架构的已知变体。

变体 1

图 26-2 为变型 1。

img/463942_2_En_26_Fig2_HTML.jpg

图 26-2

典型的 MVC 框架

变体 2

图 26-3 为变型 2。

img/463942_2_En_26_Fig3_HTML.jpg

图 26-3

一个多视图的 MVC 框架

变体 3

图 26-4 为变型 3。

img/463942_2_En_26_Fig4_HTML.jpg

图 26-4

用观察者模式/基于事件的机制实现的 MVC 模式

对 MVC 最好的描述之一来自于 wiki。c2。com ( http://wiki.c2.com/?ModelViewController ),上面写着,“我们需要智能模型、轻薄控制器、视图。”

真实世界的例子

考虑我们的模板方法模式的真实例子。但这一次,让我们换个角度来解读。我说在餐厅里,根据顾客的输入,一个厨师调整口味,做出最后一道菜。但是你知道顾客不会直接向厨师下订单。顾客看到菜单卡(视图)后,可能会咨询服务员,然后下订单。服务员将订单交给厨师,厨师从餐厅的厨房(类似于仓库或计算机数据库)收集所需的材料。准备好后,服务员把盘子端到顾客的桌子上。所以,你可以考虑一个服务员作为控制者,厨房里的厨师作为模型,食物准备材料作为数据。

计算机世界的例子

许多 web 编程框架使用 MVC 框架的概念。典型的例子包括 Django、Ruby on Rails、ASP.NET 等等。一个典型的 ASP.NET MVC 项目可以有如下图所示的视图 26-5 。

img/463942_2_En_26_Fig5_HTML.jpg

图 26-5

一个典型的 ASP.NET MVC 项目的解决方案浏览器视图

Points to Note

不同的技术遵循不同的结构,所以你不需要如图 26-5 所示的严格命名约定的文件夹结构。

履行

为了简单和符合我们的理论,我还将即将到来的实现分成三个主要部分:模型、视图和控制器。一旦注意到解决方案资源管理器视图,您就可以确定为完成此任务而创建的独立文件夹。以下是一些要点。

  • IModel, IView,IController是三个接口,分别由具体的类EmployeeModel, ConsoleView,EmployeeController,实现。看到这些名称,您可以假设它们是我们 MVC 架构的模型、视图和控制器层的代表。

  • 在这个应用中,要求非常简单。一些员工需要在申请表上注册。最初,该应用有三个不同的注册员工:Amit、Jon 和 Sam。这些员工的 ID 是 E1、E2 和 E3。所以,你看到下面这个构造函数:

    public EmployeeModel()
    {
        // Adding 3 employees at the beginning.
        enrolledEmployees = new List<Employee>();
        enrolledEmployees.Add(new Employee("Amit", "E1"));
        enrolledEmployees.Add(new Employee("John", "E2"));
        enrolledEmployees.Add(new Employee("Sam", "E3"));
    }
    
    
  • 在任何时间点,您都应该能够在系统中看到注册的员工。在客户端代码中,您调用控制器对象上的DisplayEnrolledEmployees(),如下所示:

controller.DisplayEnrolledEmployees();

然后,控制器将调用传递给视图层,如下所示:

view.ShowEnrolledEmployees(enrolledEmployees);

您会看到视图接口的具体实现者(ConsoleView.cs)对该方法的描述如下:

  • 您可以在注册员工列表中添加新员工或删除员工。为此使用了AddEmployeeToModel(Employee employee)RemoveEmployeeFromModel(string employeeIdToRemove)方法。让我们看看RemoveEmployeeFromModel(...)的方法签名。要删除一个雇员,您需要提供雇员 ID(它只不过是一个字符串)。但是如果没有找到雇员 ID,应用将忽略这个删除请求。

  • 在 Employee 类中添加了一个简单的检查,以确保不会在应用中重复添加具有相同 ID 的雇员。

public void ShowEnrolledEmployees (List<Employee> enrolledEmployees)
{
        Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
        foreach (Employee emp in enrolledEmployees)
        {
                Console.WriteLine(emp);
        }
        Console.WriteLine("---------------------");
}

现在来看一下实现。是的,它很大,但是当你在前面的要点和支持图的帮助下一部分一部分地分析它时,你应该不会在理解代码上遇到任何困难。也可以考虑一下评论,供自己即时参考。

Points to Note

通常,您希望将 MVC 与提供内置支持并执行大量基础工作的技术结合使用。例如,当你使用 ASP.NET(或类似的技术)来实现 MVC 模式时,因为你有很多内置的支持。在这些情况下,你需要学习新的术语。

在本书中,我使用控制台应用来实现设计模式。让我们在即将到来的实现中继续使用同样的方法,因为我们的重点只放在 MVC 架构上。

类图

图 26-6 为类图。

img/463942_2_En_26_Fig6_HTML.jpg

图 26-6

类图

解决方案资源管理器视图

图 26-7 显示了程序的高层结构。

img/463942_2_En_26_Fig7_HTML.jpg

图 26-7

解决方案资源管理器视图

演示 1

这是完整的演示。

模型文件夹中的内容
// Employee.cs

namespace MVCPattern.Model
{
    // The key "data" in this application
    public class Employee
    {
        private string empName;
        private string empId;
        public string GetEmpName()
        {
            return empName;
        }
        public string GetEmpId()
        {
            return empId;
        }
        public Employee(string empName, string empId)
        {
            this.empName = empName;
            this.empId = empId;
        }

        public override string ToString()
        {
            return  $"{empName} is enrolled with id : {empId}.";
        }
    }
}

// Model.cs
using System.Collections.Generic;

namespace MVCPattern.Model
{
    public interface IModel
    {

        List<Employee> GetEnrolledEmployeeDetailsFromModel();
        void AddEmployeeToModel(Employee employeee);
        void RemoveEmployeeFromModel(string employeeId);
    }
}

// EmployeeModel.cs
using System;
using System.Collections.Generic;

namespace MVCPattern.Model
{
    public class EmployeeModel : IModel
    {
        List<Employee> enrolledEmployees;

        public EmployeeModel()
        {
            // Adding 3 employees at the beginning.
            enrolledEmployees = new List<Employee>();
            enrolledEmployees.Add(new Employee("Amit", "E1"));
            enrolledEmployees.Add(new Employee("John", "E2"));
            enrolledEmployees.Add(new Employee("Sam", "E3"));
        }

        public List<Employee> GetEnrolledEmployeeDetailsFromModel()
        {
            return enrolledEmployees;
        }

        // Adding an employee to the model(registered employee list)
        public void AddEmployeeToModel(Employee employee)
        {
            Console.WriteLine($"\nTrying to add an employee to the registered list.The employee name is {employee.GetEmpName()} and id is {employee.GetEmpId()}.");

            if (!enrolledEmployees.Contains(employee))
            {
                enrolledEmployees.Add(employee);
                Console.WriteLine(employee + " [added recently.]");
            }
            else
            {
                Console.WriteLine("This employee is already added in the registered list.So, ignoring the request of addition.");
            }
        }
        // Removing an employee from model(registered employee list)

        public void RemoveEmployeeFromModel(string employeeIdToRemove)
        {
            Console.WriteLine($"\nTrying to remove an employee from the registered list.The employee id is {employeeIdToRemove}.");
            Employee emp = FindEmployeeWithId(employeeIdToRemove);
            if (emp != null)
            {
                Console.WriteLine("Removing this employee.");
                enrolledEmployees.Remove(emp);
            }
            else
            {
                Console.WriteLine($"At present, there is no employee with id {employeeIdToRemove}.Ignoring this request.");
            }
        }
        Employee FindEmployeeWithId(string employeeIdToRemove)
        {
            Employee removeEmp = null;
            foreach (Employee emp in enrolledEmployees)
            {
                if (emp.GetEmpId().Equals(employeeIdToRemove))
                {

                    Console.WriteLine($" Employee Found.{emp.GetEmpName()} has id: { employeeIdToRemove}.");
                    removeEmp = emp;
                }
            }
            return removeEmp;
        }
    }
}

视图文件夹中的内容
// View.cs
using MVCPattern.Model;
using System.Collections.Generic;

namespace MVCPattern.View
{
    public interface IView
    {
        void ShowEnrolledEmployees(List<Employee> enrolledEmployees);
    }
}

// ConsoleView.cs
using System;
using System.Collections.Generic;
using MVCPattern.Model;

namespace MVCPattern.View
{
    public class ConsoleView : IView
    {
        public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
        {
            Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
            foreach (Employee emp in enrolledEmployees)
            {
                Console.WriteLine(emp);
            }
            Console.WriteLine("---------------------");
        }
    }
}

控制器文件夹中的内容
// Controller.cs
using MVCPattern.Model;

namespace MVCPattern.Controller
{
    interface IController
    {
        void DisplayEnrolledEmployees();
        void AddEmployee(Employee employee);
        void RemoveEmployee(string employeeId);
    }

}

// EmployeeController.cs
using System.Collections.Generic;
using MVCPattern.Model;
using MVCPattern.View;

namespace MVCPattern.Controller
{
    public class EmployeeController : IController
    {
        IModel model;
        IView view;

        public EmployeeController(IModel model, IView view)
        {
            this.model = model;
            this.view = view;
        }

        public void DisplayEnrolledEmployees()
        {
            // Get data from Model
            List<Employee> enrolledEmployees = model.GetEnrolledEmployeeDetailsFromModel();
            // Connect to View
            view.ShowEnrolledEmployees(enrolledEmployees);
        }

        // Sending a request to model to add an employee to the list.
        public void AddEmployee(Employee employee)
        {
            model.AddEmployeeToModel(employee);
        }
        // Sending a request to model to remove an employee from the list.
        public void RemoveEmployee(string employeeId)
        {
            model.RemoveEmployeeFromModel(employeeId);

        }
    }
}

客户代码
// Program.cs
using MVCPattern.Controller;
using MVCPattern.Model;
using MVCPattern.View;
using System;

namespace MVCPattern
{
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***MVC architecture Demo***\n");
            // Model
            IModel model = new EmployeeModel();

            // View
            IView view = new ConsoleView();

            // Controller
            IController controller = new EmployeeController(model, view);
            controller.DisplayEnrolledEmployees();

            // Add an employee
            Employee empToAdd = new Employee("Kevin", "E4");
            controller.AddEmployee(empToAdd);
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            // Remove an existing employee using the employee id.
            controller.RemoveEmployee("E2");
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            /* Cannot remove an  employee who does not belong to the list.*/
            controller.RemoveEmployee("E5");
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            // Avoiding a duplicate entry
            controller.AddEmployee(empToAdd);
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            /* This segment is added to discuss a question in "Q&A Session" and initially commented out. */
           // view = new MobileDeviceView();
           // controller = new EmployeeController(model, view);
           // controller.DisplayEnrolledEmployees();
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***MVC architecture Demo***

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E2.
 Employee Found.John has id: E2.
Removing this employee.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

问答环节

假设你有一名程序员、一名数据库管理员和一名图形设计师。你能预测他们在 MVC 架构中的角色吗?

图形设计师设计视图层,DBA 创建模型,程序员制作智能控制器。

使用 MVC 设计模式的主要优势是什么?

一些重要的优点如下。

  • 高内聚和低耦合是 MVC 的好处。您可能已经注意到,在这种模式中,模型和视图之间的紧密耦合很容易消除。因此,应用可以很容易地扩展和重用。

  • 该模式支持并行开发。

  • 您还可以容纳多个运行时视图。

26.3 与 MVC 模式相关的挑战是什么?

这里有一些挑战。

  • 它需要高度熟练的人员。

  • 对于微小的应用来说,可能不太适合。

  • 开发人员可能需要熟悉多种语言、平台和技术。

  • 多工件一致性是一个大问题,因为您将整个项目分成三个主要部分。

26.4 你能在这个实现中提供多个视图吗?

当然可以。让我们在应用中添加一个名为 MobileDeviceView 的新的更短的视图。让我们将这个类添加到视图文件夹中,如下所示。

using System;
using System.Collections.Generic;
using MVCPattern.Model;
namespace MVCPattern.View
{
    public class MobileDeviceView:IView
    {

        public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
        {
            Console.WriteLine("\n +++This is a mobile device view of currently enrolled employees.+++ ");
            foreach (Employee emp in enrolledEmployees)
            {
                Console.WriteLine(emp.GetEmpId() + "\t" + emp.GetEmpName());
            }
            Console.WriteLine("+++++++++++++++++++++");
        }
    }
}

一旦添加了这个类,修改后的解决方案资源管理器视图应该类似于图 26-8 。

img/463942_2_En_26_Fig8_HTML.jpg

图 26-8

修改的解决方案资源管理器视图

现在,在客户端代码的末尾添加以下代码段(请参考注释以供参考)。

/* This segment is added to discuss a question in "Q&A Session and was
   initially commented out.Now I’m uncommenting the following three lines of code."
*/
view = new MobileDeviceView();
controller = new EmployeeController(model, view);
controller.DisplayEnrolledEmployees();

现在,如果您运行应用,您会看到修改后的输出。

修改输出

下面是修改后的输出。输出的最后一部分显示了新变化的效果。更改以粗体显示。

***MVC architecture Demo***

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E2.
 Employee Found.John has id: E2.
Removing this employee.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
 +++This is a mobile device view of currently enrolled employees

.+++
E1      Amit
E3      Sam
E4      Kevin
+++++++++++++++++++++

二十七、异步编程中的模式

你会在异步编程中看到许多有趣的模式,这很艰难,很有挑战性,但也很有趣。它通常被称为异步。整体概念不是一天进化出来的,这需要时间,而在 C# 5.0 中,你得到了asyncawait关键词让它变得更简单。在此之前,程序员用各种技术实现了这个概念。每种技术都有其优点和缺点。本章的目标是向你介绍不同的异步编程模式。

概观

首先,让我们讨论异步编程。简单地说,你在你的应用中取一个代码段,并在一个单独的线程上运行它。关键优势是什么?简单的答案是,您可以释放原始线程,让它继续执行剩余的任务,而在一个单独的线程中,您可以执行不同的任务。这种机制帮助您开发现代应用;例如,当您实现一个高度响应的用户界面时,这些概念非常有用。

Points to Remember

大体上,您会注意到异步编程中的三种不同模式,如下所示:

  • IAsyncResult 模式 : 或者,它被称为异步编程模型(APM)。在这个模式中,在核心处,您可以看到支持异步行为的IAsyncResult接口。在同步模型中,如果您有一个名为 XXX()的同步方法,在异步版本中,您会看到对应同步方法的BeginXXX()EndXXX()方法。比如在同步版本中,如果你有Read()方法支持读操作;在异步编程中,通常有BeginRead()EndRead()方法来异步支持相应的读操作。使用这个概念,从演示 5 到演示 7,您会看到BeginInvokeEndInvoke方法。但是对于即将到来的和新的开发,不推荐使用这种模式。

  • **:基于事件的异步模式。NET 框架 2.0。它基于事件机制。这里您可以看到带有Async后缀的方法名,一个或多个事件,以及EventArg派生类型。这种模式仍在使用,但不推荐用于新的开发。

**** 基于任务的异步模式(TAP) : 它最早出现在 in.NET 框架 4 中;这是异步编程的推荐做法。在 C# 中,你经常会看到这种模式中的asyncawait关键字。***

***为了使这一章简短,我可以省略关于 APM 和 EAP 的讨论,但是我在这一章中讨论它们,以便你理解遗留代码。同时,您发现了异步编程持续发展的途径。

为了更好地理解异步编程,让我们从它的对应物开始讨论:同步编程。同步方法很简单,代码路径也很容易理解,但是在这种编程中,您需要等待从特定的代码段获得结果,直到您不能做任何有成效的事情。例如,当一段代码试图打开一个需要时间加载的网页时,或者当一段代码正在运行一个长时间运行的算法时,等等。在这些情况下,如果您遵循同步方法,您需要无所事事。因此,即使你的计算机速度超快,计算能力更强,你也没有充分发挥它的潜力,这不是一个好主意。因此,为了支持现代需求和构建高响应性的应用,对异步编程的需求与日俱增。因此,当您了解这一类别中不同的实现模式时,您会受益匪浅。

使用同步方法

在演示 1 中,我执行了一个简单的程序,从同步方法开始。这里有两个简单的方法叫做ExecuteMethodOne()ExecuteMethodTwo()。在Main()方法内部,我同步调用这些方法(即,我先调用ExecuteMethodOne(),然后调用ExecuteMethodTwo())。为了专注于关键的讨论,我把这些方法做得非常简单。我将简单的 sleep 语句放入其中,以确保这些方法执行的工作需要一定的时间来完成。一旦您运行应用并注意到输出,您会看到只有在ExecuteMethodOne()完成执行后,ExecuteMethodTwo()才能开始执行。在这种情况下,Main()方法不能完成,直到方法完成它们的执行。

Note

在本章中,你会看到这些方法略有不同。我试图维护类似的方法,以便您可以轻松地比较异步编程的不同技术。出于简单演示的目的,在这些例子中,我假设ExecuteMethodOne()需要更多的时间来完成,因为它将执行一些冗长的操作。所以,我强迫自己在里面睡了一个相对长的时间。相反,我假设ExecuteMethodTwo()执行一个小任务,所以我在里面放置了一个相对较短的睡眠。

演示 1

这是完整的演示。

using System;
using System.Threading;

namespace SynchronousProgrammingExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***A Synchronous Program Demonstration.***");
            Console.WriteLine("ExecuteMethodTwo() needs to wait for ExecuteMethodOne() to finish first.");
            ExecuteMethodOne();
            ExecuteMethodTwo();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne()
        {
            Console.WriteLine("MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("MethodOne has finished.");
        }
        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

这是输出。

***A Synchronous Program Demonstration.***
ExecuteMethodTwo() needs to wait for ExecuteMethodOne() to finish first.
MethodOne has started.
MethodOne has finished.
MethodTwo has started.
MethodTwo has finished.
End Main().

使用线程类

如果您仔细观察演示 1 中的方法,您会发现这些方法并不相互依赖。如果您可以并行执行它们,您的应用的响应时间将会得到改善,并且您可以减少总的执行时间。所以,让我们找到一些更好的方法。

在这种情况下,您可以实现多线程的概念。演示 2 是一个使用线程的简单解决方案。它展示了在一个新线程中替换ExecuteMethodOne()方法。

演示 2

using System;
using System.Threading;

namespace UsingThreadClass
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming using Thread class.***");
            //ExecuteMethodOne();
            //Old approach.Creating a separate thread for the following //task(i.e. ExecuteMethodOne.)
            Thread newThread = new Thread(() =>
            {
                Console.WriteLine("MethodOne has started on a separate thread.");
                // Some big task
                Thread.Sleep(1000);
                Console.WriteLine("MethodOne has finished.");
            }
            );
            newThread.Start();
            /*
               Taking a small sleep to increase the probability of executing ExecuteMethodOne() before ExecuteMethodTwo().
             */
            Thread.Sleep(20);
            ExecuteMethodTwo();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming using Thread class.***
MethodOne has started on a separate thread.
MethodTwo has started.
MethodTwo has finished.
End Main().
MethodOne has finished.

分析

注意,尽管ExecuteMethodOne()开始得很早,但是ExecuteMethodTwo()并没有等待ExecuteMethodOne()完成它的执行。此外,由于ExecuteMethodTwo()做得很少(睡眠时间为 100 毫秒),它能够在ExecuteMethodOne()完成执行之前完成。不仅如此,由于主线程没有被阻塞,它能够继续执行。

问答环节

27.1 为什么在 Main 里面的 Method2() 执行之前放一个睡眠语句?

接得好。这不是必须的,但是在某些情况下,您可能会注意到,即使您试图在当前线程中的ExecuteMethodTwo()之前启动ExecuteMethodOne()在一个单独的线程上执行,也不会发生这种情况。因此,您可能会注意到以下输出。

***Asynchronous Programming using Thread class.***
MethodTwo has started.
MethodOne has started in a separate thread.
MethodTwo has finished.
End Main().
MethodOne has finished.

在这个例子中,这个简单的 sleep 语句帮助你增加在ExecuteMethodTwo()之前开始ExecuteMethodOne()的概率。

使用线程池类

通常不鼓励在现实世界的应用中直接创建线程。这背后的一些关键原因如下。

  • 维护太多的线程会导致困难和高成本的操作。

  • 由于上下文切换,浪费了大量时间,而不是做真正的工作。

为了避免直接创建线程,C# 为您提供了使用内置ThreadPool类的便利。有了这个类,您可以使用现有的线程,这些线程可以重用以满足您的需要。ThreadPool类在维护应用中的最佳线程数量方面非常有效。因此,如果需要,您可以使用这个工具异步执行一些任务。

ThreadPool是包含一些static方法的静态类;他们中的一些人也有超载的版本。为了您的快速参考,图 27-1 是来自 Visual Studio IDE 的部分截图,显示了ThreadPool类中的方法。

img/463942_2_En_27_Fig1_HTML.jpg

图 27-1

Visual Studio 2019 IDE 中 ThreadPool 类的截图

在本节中,我们的重点是QueueUserWorkItem方法。图 27-1 显示该方法有两个重载版本。现在要了解这个方法的细节,让我们展开 Visual Studio 中的方法描述。例如,一旦展开此方法的第一个重载版本,您会注意到以下情况。

//
// Summary:
//     Queues a method for execution. The method executes when a thread //     pool thread becomes available.
//
// Parameters:
//   callBack:
//     A System.Threading.WaitCallback that represents the method to be //     executed.
//
// Returns:
//     true if the method is successfully queued; System.NotSupportedException
//     is thrown if the work item could not be queued.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     callBack is null.
//
//   T:System.NotSupportedException:
//     The common language runtime (CLR) is hosted, and the host does not //     support this action.
[SecuritySafeCritical]
public static bool QueueUserWorkItem(WaitCallback callBack);

如果您进一步研究方法参数,您会发现WaitCallBack是一个具有以下描述的委托。

//
// Summary:
//     Represents a callback method to be executed by a thread pool thread.
//
// Parameters:
//   state:
//     An object containing information to be used by the callback method.
[ComVisible(true)]
public delegate void WaitCallback(object state);

第二个重载版本的QueueUserWorkItem可以接受一个名为state的额外的object参数。内容如下。

public static bool QueueUserWorkItem(WaitCallback callBack, object state);

它告诉我们,使用这个重载版本,您可以通过这个参数向您的方法传递一些有价值的数据。在接下来的演示中,我使用了两个重载版本,这就是为什么在接下来的例子中,除了ExecuteMethodOne()ExecuteMethodTwo()(您在前面的演示中已经看到了)之外,我还引入了另一个名为ExecuteMethodThree()的方法,在这个方法中我传递了一个对象参数。

人们经常互换使用变量和形参这两个词。但是一个专业的程序员通常对此很挑剔。方法定义中使用的变量称为方法的参数。例如,如果您在一个类中看到如下所示的方法定义:

public void Sum(int firstNumber,int secondNumber)

你说 firstNumber 和 secondNumber 是方法 Sum 的参数。现在假设你有一个类的对象,比如 ob。因此,当您使用以下代码行调用该方法时:

ob.Sum(1,2)

你说 1 和 2 是传递给 Sum 方法的参数。

简而言之,你可以说我们将参数传递给一个方法,这些值被赋给方法参数。根据这些定义,我应该在我的注释中说,我已经将 10 作为参数传递给了ExecuteMethodThree。但是为了简单起见,程序员通常不会过多强调这些术语,而且你可能会看到这些术语可以互换使用。

演示 3

为了有效地使用QueueUserWorkItem方法,您需要使用一个匹配WaitCallBack委托签名的方法。在下面的演示中,我将两个方法放入一个ThreadPool中。在演示 1 和演示 2 中,ExecuteMethodTwo()没有接受任何参数。所以,如果你想按原样使用这个方法并把它传递给QueueUserWorkItem,你会得到下面的编译错误。

No overload for 'ExecuteMethodTwo' matches delegate 'WaitCallback'

因此,让我们用一个虚拟的object参数来修改ExecuteMethodTwo()方法,如下所示。(我保留了评论,供大家参考。)

/*
The following method's signature should match
the delegate WaitCallback.It is as follows:
public delegate void WaitCallback(object state)
*/
private static void ExecuteMethodTwo(object state)
{
  Console.WriteLine("--MethodTwo has started.");
  // Some small task
  Thread.Sleep(100);
  Console.WriteLine("--MethodTwo has finished.");
}

现在让我们介绍另一个名为ExecuteMethodThree(...)的方法,它真正使用了参数。该方法描述如下。

private static void ExecuteMethodThree(object number)
{
 Console.WriteLine("---MethodThree has started.");
 int upperLimit = (int)number;
 for (int i = 0; i < upperLimit; i++)
 {
  Console.WriteLine("---MethodThree prints 3.0{0}", i);
 }
 Thread.Sleep(100);
 Console.WriteLine("---MethodThree has finished.");
}

现在通过下面的演示和相应的输出。

using System;
using System.Threading;

namespace UsingThreadPool
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming using ThreadPool class.***");

            // Using Threadpool
            // Not passing any argument to ExecuteMethodTwo
            ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodTwo));
            /*
             Passing 10 as the argument to
             ExecuteMethodThree.
            */
            ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodThree), 10);
            ExecuteMethodOne();

            Console.WriteLine("End Main().");
            Console.ReadKey();
        }

        private static void ExecuteMethodOne()
        {
            Console.WriteLine("-MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("-MethodOne has finished.");
        }

        /*
        The following method's signature should match
        the delegate WaitCallback.It is as follows:
        public delegate void WaitCallback(object state)
        */
        private static void ExecuteMethodTwo(object state)
        {
            Console.WriteLine("--MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("--MethodTwo has finished.");
        }
        /*
        The following method has a parameter.
        This method's signature also matches the WaitCallBack
        delegate signature.
        */
        private static void ExecuteMethodThree(object number)
        {
            Console.WriteLine("---MethodThree has started.");
            int upperLimit = (int)number;
            for (int i = 0; i < upperLimit; i++)
            {
                Console.WriteLine($"---MethodThree prints 3.0{i}");
            }
            Thread.Sleep(100);
            Console.WriteLine("---MethodThree has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming using ThreadPool class.***
-MethodOne has started.
--MethodTwo has started.
---MethodThree has started.
---MethodThree prints 3.00
---MethodThree prints 3.01
---MethodThree prints 3.02
---MethodThree prints 3.03
---MethodThree prints 3.04
---MethodThree prints 3.05
---MethodThree prints 3.06
---MethodThree prints 3.07
---MethodThree prints 3.08
---MethodThree prints 3.09
--MethodTwo has finished.
---MethodThree has finished.
-MethodOne has finished.
End Main().

问答环节

27.2 使用简单的委托实例化技术,如果我使用下面的第一行而不是第二行,应用会编译并运行吗?

ThreadPool.QueueUserWorkItem(ExecuteMethodTwo);

thread pool . queue user work item(new waiting callback(executemethod two));

是的,但是既然你现在正在学习使用WaitCallback委托,我使用了实例化的详细方法来引起你对它的特别注意。

将 Lambda 表达式与 ThreadPool 类一起使用

如果您喜欢 lambda 表达式,您可以在类似的上下文中使用它。例如,在前面的演示中,您可以使用 lambda 表达式替换ExecuteMethodThree(...),如下所示。

// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as an argument to ExecuteMethodThree
ThreadPool.QueueUserWorkItem((number) =>
{
  Console.WriteLine("--MethodThree has started.");
  int upperLimit = (int)number;
  for (int i = 0; i < upperLimit; i++)
  {
   Console.WriteLine("---MethodThree prints 3.0{0}", i);
  }
  Thread.Sleep(100);
  Console.WriteLine("--MethodThree has finished.");
  }, 10

);

因此,在前面的演示中,您可以注释掉下面的行,并用前面介绍的 lambda 表达式替换ExecuteMethodThree(...)

ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodThree), 10);

如果您再次执行该程序,您会得到类似的输出。为了便于参考,我在演示 4 中展示了完整的实现。

演示 4

using System;
using System.Threading;

namespace UsingThreadPoolWithLambdaExpression
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming Demonstration.***");
            Console.WriteLine("***Using ThreadPool with Lambda Expression.***");

            // Using Threadpool
            // Not passing any parameter for ExecuteMethodTwo
            ThreadPool.QueueUserWorkItem(ExecuteMethodTwo);
            // Using lambda Expression
            // Here the method needs a parameter(input).
            // Passing 10 as an argument to ExecuteMethodThree
            ThreadPool.QueueUserWorkItem((number) =>
            {
                Console.WriteLine("--MethodThree has started.");
                int upperLimit = (int)number;
                for (int i = 0; i < upperLimit; i++)
                {
                    Console.WriteLine("---MethodThree prints 3.0{0}", i);
                }
                Thread.Sleep(100);
                Console.WriteLine("--MethodThree has finished.");
            }, 10

          );

            ExecuteMethodOne();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }
        /// <summary>
        /// ExecuteMethodOne()
        /// </summary>
        private static void ExecuteMethodOne()
        {
            Console.WriteLine("-MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("-MethodOne has finished.");
        }

        /*
        The following method's signature should match
        the delegate WaitCallback.It is as follows:
        public delegate void WaitCallback(object state)
        */

        private static void ExecuteMethodTwo(Object state)
        {
            Console.WriteLine("--MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("--MethodTwo has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming Demonstration.***
***Using ThreadPool with Lambda Expression.***
--MethodTwo has started.
-MethodOne has started.
--MethodThree has started.
---MethodThree prints 3.00
---MethodThree prints 3.01
---MethodThree prints 3.02
---MethodThree prints 3.03
---MethodThree prints 3.04
---MethodThree prints 3.05
---MethodThree prints 3.06
---MethodThree prints 3.07
---MethodThree prints 3.08
---MethodThree prints 3.09
--MethodTwo has finished.
--MethodThree has finished.
-MethodOne has finished.
End Main().

Note

这次,您看到了带有ThreadPool类的 lambda 表达式。在演示 2 中,您看到了带有Thread类的 lambda 表达式。

使用 IAsyncResult 模式

我提到过IAsyncResult接口帮助你实现异步行为。我还告诉你,在同步模型中,如果你有一个名为XXX的同步方法,在异步版本中,你会看到对应同步方法的BeginXXXEndXXX方法。现在你可以看到这些细节了。

使用异步委托进行轮询

在演示 3 和演示 4 中,您看到了一个内置的WaitCallBack委托。通常,委托有许多不同的用途。在本节中,您将看到另一个重要的用法。让我们考虑一下轮询,这是一种重复检查条件的机制。在我们接下来的例子中,让我们检查一个委托实例是否完成了它的任务。

演示 5

这一次,我稍微修改了一下ExecuteMethodOne(...)ExecuteMethodTwo()方法。这些方法可以打印线程 id。这次,我允许ExecuteMethodOne(...)接受一个提供睡眠时间的int参数,而不是盲目地睡眠 1000 毫秒。

和以前的情况一样,ExecuteMethodTwo()只休眠了 100 毫秒,但是与ExecuteMethodTwo()相比,ExecuteMethodOne(...)需要更多的时间来完成它的任务。为了实现这一点,在本例中,我在ExecuteMethodOne(...)中传递了 3000 毫秒作为方法参数。

让我们看看代码的重要部分。现在我的ExecuteMethodOne如下:

// First Method
private static void ExecuteMethodOne(int sleepTimeInMilliSec)
{
  Console.WriteLine("MethodOne has started.");
  Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
  // Some big task
  Thread.Sleep(sleepTimeInMilliSec);
  Console.WriteLine("\nMethodOne has finished.");
}

为了匹配签名,我如下声明委托Method1Delegate

public delegate void Method1Delegate(int sleepTimeinMilliSec);

稍后我将它实例化如下。

Method1Delegate method1Del = ExecuteMethodOne;

到目前为止,一切都很简单。现在来看代码中最重要的一行,如下所示。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);

你还记得在委托的上下文中,你可以使用Invoke()方法吗?但是那一次你的代码遵循同步路径。现在你正在探索异步编程,所以你看到了BeginInvokeEndInvoke方法。当 C# 编译器看到 delegate 关键字时,它为动态生成的类提供这些方法。

BeginInvoke方法的返回类型是IAsyncResult。如果您将鼠标悬停在BeginInvoke上或者注意它的结构,您会看到虽然ExecuteMethodOne只接受一个参数,但是BeginInvoke方法总是接受两个额外的参数:一个类型为AsyncCallback和一个类型为object。你很快就会看到对它们的讨论。在这个例子中,我只使用了第一个参数,并将 3000 毫秒作为ExecuteMethodOne的参数。但是对于BeginInvoke的后两个参数,我传了null值。

BeginInvoke返回的结果很重要,我将结果保存在一个IAsyncResult对象中。IAsyncResult具有以下只读属性。

public interface IAsyncResult
{
 bool IsCompleted { get; }
 WaitHandle AsyncWaitHandle { get; }
 object AsyncState { get; }
 bool CompletedSynchronously { get; }
}

目前,我关注的是isCompleted属性。如果您进一步扩展这些定义,您会看到isCompleted的定义如下。

//
// Summary:
//     Gets a value that indicates whether the asynchronous  operation has //     completed.
//
// Returns:
//     true if the operation is complete; otherwise, false.
bool IsCompleted { get; }

因此,很明显,您可以使用这个属性来验证代理是否已经完成了它的工作。

在下面的例子中,我检查其他线程中的委托是否完成了它的工作。如果工作没有完成,我会在控制台窗口中打印星号(*),并强制主线程短暂休眠,这就是为什么您会在本演示中看到下面这段代码。

while (!asyncResult.IsCompleted)
{
    // Keep working in main thread
    Console.Write("*");
    Thread.Sleep(5);
}

最后,EndInvoke方法接受一个类型为IAsyncResult的参数。所以,我通过asyncResult作为这个方法中的一个参数。现在进行完整的演示。

using System;
using System.Threading;

namespace PollingDemoInDotNetFramework
{
    //WILL NOT WORK ON .NET CORE.
    //RUN THIS PROGRAM ON .NET FRAMEWORK.
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Polling Demo.Run it in .NET Framework.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Synchronous call
            //ExecuteMethodOne(3000);

            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
            ExecuteMethodTwo();
            while (!asyncResult.IsCompleted)
            {
                // Keep working in main thread
                Console.Write("*");
                Thread.Sleep(5);
            }

            method1Del.EndInvoke(asyncResult);
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }
        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }

    }
}

输出

下面是一个可能的输出。

***Polling Demo.Run it in .NET Framework.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

问答环节

27.3 上一个案例中, ExecuteMethodOne(...) 只带一个参数, BeginInvoke 带三个参数。那么,我是不是可以简单的说,如果 ExecuteMethodOne(...) 接受 n 数量的参数,那么 BeginInvoke 就有 n+2 参数?

是的,初始的参数集是基于您的方法的,但是对于最后两个参数,一个是类型AsyncCallback,的,最后一个是类型object.

Points to Remember

  • 这种类型的例子在。NET 框架 4.7.2。如果你在。NET Core 3.0,你会得到这个异常:System。PlatformNotSupportedException:“此平台上不支持操作。其中一个主要原因是异步委托实现依赖于中不存在的远程处理功能。NET 核心。关于这一点的详细论述可以在 https://github.com/dotnet/runtime/issues/16312 中找到。

  • 如果您不想在控制台窗口中检查和打印星号(*),您可以在主线程完成执行后简单地调用委托类型的EndInvoke()方法。EndInvoke()本身等待直到代理完成它的工作。

  • 如果你没有明确地检查代理是否完成了它的执行,或者你只是忘记调用EndInvoke(),代理的线程在主线程死亡后停止。例如,如果您注释掉前面示例中的以下代码段。

    //while (!asyncResult.IsCompleted)
    //{
    //    Keep working in main thread
    //    Console.Write("*");
    //    Thread.Sleep(5);
    //}
    //method1Del.EndInvoke(asyncResult);
    //Console.ReadKey();
    
    
  • BeginInvoke通过使用EndInvoke.帮助调用线程稍后获得异步方法调用的结果

And run the application again, you may NOT see the statement "MethodOne has finished."

使用 IAsyncResult 的 AsyncWaitHandle

你注意到了吗WaitHandle``AsyncWaitHandle``IAsyncResult?里面的{ get; }很重要,这一次,我向你展示了使用这个属性的另一种方法。如果你仔细观察,你会发现AsyncWaitHandle返回一个WaitHandle,,它有如下描述。

//
// Summary:
//     Gets a System.Threading.WaitHandle that is used to wait for an //     asynchronous operation to complete.
//
// Returns:
//     A System.Threading.WaitHandle that is used to wait for an //     asynchronous operation to complete.
WaitHandle AsyncWaitHandle { get; }

Visual Studio IDE 确认WaitHandle是一个等待对共享资源进行独占访问的抽象类。在WaitHandle中,你会看到有五个不同重载版本的WaitOne()方法,如下所示。

public virtual bool WaitOne(int millisecondsTimeout);
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
public virtual bool WaitOne();

在接下来的演示中,我使用了第一个重载版本,并提供了一个可选的超时值,单位为毫秒。如果您展开该方法,您会看到以下与之相关联的摘要。

// Summary:
// Blocks the current thread until the current System.Threading.WaitHandle // receives a signal, using a 32-bit signed integer to specify the time // interval in milliseconds.
//(Some other details omitted)
public virtual bool WaitOne(int millisecondsTimeout);

因此,很明显,通过使用WaitHandle,,你可以等待一个委托线程完成它的工作。在下面的程序中,如果等待成功,控制将从while循环中退出。但是如果发生超时,WaitOne()返回 false,并且while循环继续并在控制台中打印星号(*)。

演示 6

using System;
using System.Threading;
//RUN THIS PROGRAM ON .NET FRAMEWORK.

namespace UsingWaitHandleInDotNetFramework
{
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Polling and WaitHandle Demo.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Synchronous call
            //ExecuteMethodOne(3000);
            // Asynchrous call using a delegate
            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
            ExecuteMethodTwo();
            while (true)
            {
                // Keep working in main thread
                Console.Write("*");
                /*
                 There are 5 different overload method for WaitOne().Following method blocks the current thread until the
                 current System.Threading.WaitHandle receives a signal,using a 32-bit signed integer to specify the time interval in milliseconds.
                */
                if (asyncResult.AsyncWaitHandle.WaitOne(10))
                {
                    Console.Write("\nResult is available now.");
                    break;
                }
            }
            method1Del.EndInvoke(asyncResult);
            Console.WriteLine("\nExiting Main().");
            Console.ReadKey();
        }

        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            // It will have a different thread id
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

这是一个可能的输出。

***Polling and WaitHandle Demo.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
*******************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.
***
Result is available now.
Exiting Main().

分析

如果您将这个演示与上一个进行比较,您将会看到异步操作以不同的方式完成。这次,你没有使用IsCompleted属性,而是使用了我向你展示的IAsyncResult.AsyncWaitHandle属性,这两种属性可以在不同的应用中看到。

使用异步回调

回顾一下前面两个演示中使用的BeginInvoke方法。让我们回顾一下我是如何使用它的。

// Asynchrous call using a delegate
Method1Delegate method1Del = ExecuteMethodOne;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);

这段代码显示,在BeginInvoke方法中,我为最后两个方法参数传递了两个null参数。如果您将鼠标悬停在这些先前演示的行上,您会注意到BeginInvoke期望一个IAsyncCallback委托作为第二个参数,在本例中期望一个object作为第三个参数。

让我们调查一下IAsyncCallback代表。Visual Studio IDE 告诉我们这个委托是在System命名空间中定义的,它有如下描述。

//
// Summary:
//     References a method to be called when a corresponding asynchronous //     operation completes.
//
// Parameters:
//   ar:
//     The result of the asynchronous operation.
  [ComVisible(true)]
  public delegate void AsyncCallback(IAsyncResult ar);

你可以使用一个callback方法来执行一些有用的东西(比如一些内务工作)。AsyncCallback委托有一个void返回类型,它接受一个IAsyncResult参数。所以,让我们定义一个可以匹配这个委托签名的方法,并在Method1Del实例完成执行后调用这个方法。下面是一个示例方法(姑且称之为ExecuteCallbackMethod),它将在接下来的演示中使用。

/*
It's a callback method.This method will be invoked
when Method1Delegate completes its work.
*/
private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
{
 //if null you can throw some exception

    if (asyncResult != null)
    {
     Console.WriteLine("\nCallbackMethod has started.");
     Console.WriteLine($"Inside ExecuteCallbackMethod(...), Thread id {Thread.CurrentThread.ManagedThreadId} .");
     // Do some housekeeping work/ clean-up operation
     Thread.Sleep(100);
     Console.WriteLine("CallbackMethod has finished.");
    }
   }

演示 7

现在查看完整的实现。

using System;
using System.Threading;

namespace UsingAsynchronousCallback
{
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Asynchronous Callback.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);

            // Asynchrous call using a delegate
            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, null);

            ExecuteMethodTwo();
            while (!asyncResult.IsCompleted)
            {
                // Keep working in main thread
                Console.Write("*");
                Thread.Sleep(5);
            }

            method1Del.EndInvoke(asyncResult);
            Console.WriteLine("Exit Main().");
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }

        /*
         It's a callback method.This method will be invoked
         when Method1Delegate instance completes its work.
         */
        private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
        {
            if (asyncResult != null)//if null you can throw some exception
            {
                Console.WriteLine("\nCallbackMethod has started.");
                Console.WriteLine($"Inside ExecuteCallbackMethod(...),Thread id {Thread.CurrentThread.ManagedThreadId} .");
                // Do some housekeeping work/ clean-up operation
                Thread.Sleep(100);
                Console.WriteLine("CallbackMethod has finished.");
            }
        }
    }
}

输出

下面是一个可能的输出。

***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

CallbackMethod has started.
Inside ExecuteCallbackMethod(...),Thread id 3 .
Exit Main().
CallbackMethod has finished.

分析

回调方法仅在ExecuteMethodOne完成执行后才开始工作。另外,注意ExecuteMethodOneExecuteCallbackMethod线程 id 是相同的。这是因为回调方法是从运行ExecuteMethodOne的线程中调用的。

问答环节

27.4 什么是 回调方法

通常,它是在特定操作完成后调用的方法。在异步编程中,当您不知道某个操作的确切完成时间,但希望在某个任务完成后开始一个新任务时,您经常会看到这种方法。例如,在前面的例子中,如果ExecuteMethodOne在其执行期间分配了一些资源,ExecuteCallbackMethod可以执行一些清理工作。

我发现回调方法不是从主线程调用的。是否在意料之中?

是的。在这个例子中,ExecuteCallbackMethod是回调方法,它只有在ExecuteMethodOne完成工作后才能开始执行。因此,从运行ExecuteMethodOne的同一个线程中调用ExecuteCallbackMethod是有意义的。

我可以在这个例子中使用 lambda 表达式吗?

接得好。为了获得类似的输出,在前面的演示中,没有创建一个新的ExecuteCallbackMethod方法并使用下面的代码行,

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, null);

您可以使用 lambda 表达式替换它,如下所示。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000,
 (result) =>
{
    if (result != null)//if null you can throw some exception
    {
        Console.WriteLine("\nCallbackMethod has started.");
        Console.WriteLine($"Inside ExecuteCallbackMethod(),Thread id { Thread.CurrentThread.ManagedThreadId }.");
        // Do some housekeeping work/ clean-up operation
        Thread.Sleep(100);
        Console.WriteLine("CallbackMethod has finished.");
    }
 },
null);

27.7 我看到你在 BeginInvoke 方法内部使用回调方法的时候,没有传递一个对象作为最终参数,而是传递了一个空值。这有什么具体原因吗?

不,我没有在这些演示中使用该参数。因为它是一个对象参数,所以可以传递任何对你有意义的东西。使用回调方法时,可以传递委托实例本身。它可以帮助您的回调方法分析异步方法的结果。

但是为了简单起见,让我们修改前面的演示并传递一个字符串消息作为BeginInvoke中的最后一个参数。让我们假设现在您正在修改现有的代码行

IAsyncResult asyncResult = method1Del.BeginInvoke(3000,ExecuteCallbackMethod, null);

有了下面这个。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, "Method1Delegate, Thank you for using me." );

To accommodate this change, lets modify the ExecuteCallbackMethod() method too.The newly added lines are shown in bold.
private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
{
   if (asyncResult != null)//if null you can throw some exception
    {
     Console.WriteLine("\nCallbackMethod has started.");
     Console.WriteLine($"Inside ExecuteCallbackMethod(...),Thread id { Thread.CurrentThread.ManagedThreadId} .");
     // Do some housekeeping work/ clean-up operation
     Thread.Sleep(100);
     // For Q&A 27.7
     string msg = (string)asyncResult.AsyncState;
     Console.WriteLine($"Callback method says : ‘{msg}’");
     Console.WriteLine("CallbackMethod has finished.");
     }
  }
If you run the program again, this time you can see the following output which conforms the new string message:

***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

CallbackMethod has started.
Exit Main().
Inside ExecuteCallbackMethod(...),Thread id 3 .
Callback method says : `Method1Delegate, Thank you for using me.'
CallbackMethod has finished.

Points to Remember

您已经看到了使用委托实现轮询、等待句柄和异步回调。这种编程模型可以在。NET 框架也有,比如HttpWebRequest类的BeginGetResponseBeginGetRequestStream或者SqlCommand类的BeginExecuteNonQuery(), BeginExecuteReader()BeginExecuteXmlReader()。这些方法也有重载版本。

使用基于事件的异步模式

在本节中,您将看到基于事件的异步模式的使用,这种模式最初很难理解。根据应用的复杂性,这种模式可以有多种形式。以下是这种模式的一些关键特征。

  • 一般来说,异步方法可以是其同步版本的副本,但是当您调用它时,它在一个单独的线程上启动,然后立即返回。这种机制允许您调用一个线程来继续,而预期的操作在后台运行。这些操作的例子可以是长时间运行的过程,例如加载大图像、下载大文件、连接、建立到数据库的连接等等。基于事件的异步模式在这些情况下很有帮助。例如,一旦长时间运行的下载操作完成,就可以引发一个事件来通知信息。事件的订阅者可以根据该通知立即采取行动。

  • 您可以同时执行多个方法,并在每个方法完成时收到通知。

  • 使用这种模式,您可以利用多线程,但同时也隐藏了整体的复杂性。

  • 在最简单的情况下,您的方法名有一个Async后缀,告诉其他人您正在使用该方法的异步版本。同时,您有一个带有Completed后缀的相应事件。在理想情况下,您应该有一个相应的 cancel 方法,并且它应该支持显示进度条/报告。支持取消操作的方法也可以命名为MethodNameAsyncCancel(或者简称为CancelAsync)。

  • SoundPlayer、PictureBox、WebClient 和 BackgroundWorker 等组件通常是这种模式的代表。

我使用 WebClient 制作了一个简单的应用。我们来看看。

演示 8

在程序的开始,您会看到我需要包含一些特定的名称空间。我用注释告诉你它们在这个演示中的重要性。

在本例中,我想将一个文件下载到我的本地系统中。但是我没有使用来自互联网的真实 URL,而是将源文件存储在本地系统中。这有两个主要好处。

  • 运行此应用不需要互联网连接。

  • 由于您没有使用互联网连接,下载操作相对较快。

现在,在看到完整的示例之前,请看下面的代码块。

WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
// Target location for download
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

到目前为止,事情简单明了。但是我提请你注意下面几行代码。

webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

你可以看到在第一行中,我使用了一个在WebClient中定义的叫做DownloadFileAsync的方法。在 Visual Studio 中,方法描述告诉我们以下内容。

// Summary:
//     Downloads, to a local file, the resource with the specified URI. This method does not block the calling thread.
//
// Parameters:
//   address:
//     The URI of the resource to download.
//
//   fileName:
//     The name of the file to be placed on the local computer.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     The address parameter is null. -or- The fileName parameter is null.
//
//   T:System.Net.WebException:
//     The URI formed by combining System.Net.WebClient.BaseAddress and address is invalid.
//     -or- An error occurred while downloading the resource.
//
//   T:System.InvalidOperationException:
//     The local file specified by fileName is in use by another thread.
public void DownloadFileAsync(Uri address, string fileName);

使用此方法时,调用线程不会被阻塞。(实际上,DownloadFileAsyncDownloadFile方法的异步版本,也是在 WebClient .中定义的)

现在我们来看下一行代码。

webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

Visual Studio 对DownloadFileCompleted事件描述如下。

/ Summary:
//     Occurs when an asynchronous file download operation completes.
public event AsyncCompletedEventHandler DownloadFileCompleted;

它对AsyncCompletedEventHandler进一步描述如下。

// Summary:
//     Represents the method that will handle the MethodNameCompleted event
//     of an asynchronous operation.
//
// Parameters:
//   sender:
//     The source of the event.
//
//   e:
//     An System.ComponentModel.AsyncCompletedEventArgs that contains the //     event data.
public delegate void AsyncCompletedEventHandler(object sender, AsyncCompletedEventArgs e);

您可以订阅DownloadFileCompleted事件来显示下载操作完成的通知。为此,我使用了以下方法。

private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
    Console.WriteLine("Successfully downloaded the file now.");
}

Note

DownloadCompleted方法匹配AsyncCompletedEventHandler委托的签名。

我假设您在运行这个应用之前已经掌握了委托和事件的概念。你知道我可以替换这一行代码。

webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

使用下面的代码行。

webClient.DownloadFileCompleted += DownloadCompleted;

但是为了更好的可读性,我喜欢保留长版本。现在查看完整的示例和输出。

using System;
// For AsyncCompletedEventHandler delegate
using System.ComponentModel;
using System.Net; // For WebClient
using System.Threading; // For Thread.Sleep() method

namespace UsingWebClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Event Based Asynchronous Program Demo.***");
            // Method1();
            #region The lenghty operation(download)
            Console.WriteLine("Starting a download operation.");
            WebClient webClient = new WebClient();
            // File location
            Uri myLocation = new Uri(@"C:\TestData\OriginalFile.txt");
            // Target location for download
            string targetLocation = @"C:\TestData\DownloadedFile.txt";
            webClient.DownloadFileAsync(myLocation, targetLocation);
            webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
            #endregion
            ExecuteMethodTwo();
            Console.WriteLine("End Main()...");
            Console.ReadKey();
        }
        // ExecuteMethodTwo
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some very small task
            Thread.Sleep(10);
            Console.WriteLine("MethodTwo has finished.");
        }

        private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            Console.WriteLine("Successfully downloaded the file now.");
        }
    }
}

输出

下面是一个可能的输出。

***Event Based Asynchronous Program Demo.***
Starting a download operation.
MethodTwo has started.
MethodTwo has finished.
End Main()...
Successfully downloaded the file now.

分析

您可以看到下载操作是在ExecuteMethodTwo()开始执行之前开始的。然而,ExecuteMethodTwo()在下载操作完成之前完成了它的任务。如果你对Original.txt的内容感兴趣,这里有。

Dear Reader,
This is my test file.It is originally stored at C:\TestData in my system.

您可以使用类似的文件和内容进行测试,以便在您的终端进行快速验证。

附加说明

当你引入一个进度条时,你可以使这个例子更好。在这种情况下,您可以使用 Windows 窗体应用来获得对进度条的内置支持。我们暂且忽略ExecuteMethodTwo(),单独关注异步下载操作。你可以做一个基本的表单,如图 27-2 所示,包含三个简单的按钮和一个进度条。(你需要先将这些控件拖放到你的表单上,并将其命名为如图 27-2 所示。我假设你知道这些简单的活动。)

img/463942_2_En_27_Fig2_HTML.jpg

图 27-2

一个简单的 UI 应用,演示基于事件的异步

下面这段代码是不言自明的。

using System;
using System.ComponentModel;
using System.Net;
using System.Windows.Forms;

namespace UsingWebClentWithWinForm
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void StartDownload_Click(object sender, EventArgs e)
        {
         WebClient webClient = new WebClient();
         Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
         string targetLocation = @"C:\TestData\downloaded_file.txt";
         webClient.DownloadFileAsync(myLocation, targetLocation);
         webClient.DownloadFileCompleted += new      AsyncCompletedEventHandler(DownloadCompleted);
         webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
         MessageBox.Show("Executed download operation.");
    }
    private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
    {
         MessageBox.Show("Successfully downloaded the file now.");
    }
    private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
    {
         progressBar.Value = e.ProgressPercentage;
    }

    private void ResetButton_Click(object sender, EventArgs e)
    {
         progressBar.Value = 0;
    }

    private void ExitButton_Click(object sender, EventArgs e)
    {
        this.Close();
    }
    }
}

Note

您可以从 Apress 网站下载该应用的完整代码。

输出

一旦你点击StartDownloadButton,你会得到如图 27-3 和图 27-4 所示的输出。

img/463942_2_En_27_Fig3_HTML.jpg

图 27-3

UI 应用的运行时屏幕截图

点击 OK 按钮后,你会看到如图 27-4 所示的消息框。

img/463942_2_En_27_Fig4_HTML.jpg

图 27-4

当您单击“确定”按钮时,会弹出另一个消息框

问答环节

27.8 与基于事件的异步程序相关的有哪些优点和缺点?

**以下是与这种方法相关的一些常见的优点和缺点。

赞成的意见

  • 您可以调用一个长时间运行的方法并立即返回。当方法完成时,您可以得到一个通知,您可以有效地使用它。

骗局

  • 因为你有分离的代码,所以理解、调试和维护通常很困难。

  • 当您订阅了一个事件,但后来忘记取消订阅时,就会出现一个大问题。这个错误会导致应用中的内存泄漏,影响可能非常严重;例如,您的系统挂起或没有响应,您需要经常重新启动它。

了解任务

要理解基于任务的异步模式(TAP),首先,你必须知道什么是任务。任务只是您想要执行的一个工作单元。您可以在同一个线程或不同的线程中完成这项工作。使用任务,您可以更好地控制线程;例如,您可以在特定任务完成后执行后续工作。父任务可以创建子任务,因此您可以组织层次结构。当你级联你的消息时,这种层次结构是很重要的。考虑一个例子。在您的应用中,一旦父任务被取消,子任务也应该被取消。

您可以用不同的方式创建任务。在下面的演示中,我用三种不同的方式创建了三个任务。以下代码段有支持注释。

#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion

你可以看到所有三个任务(taskOne, taskTwo, taskThree)都试图做一个相似的操作:它们只是执行MyMethod(),描述如下。

private static void MyMethod()
{
    Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
    // Some task
    Thread.Sleep(100);
    Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId,  Thread.CurrentThread.ManagedThreadId);
    }

你可以看到在MyMethod()里面,为了区分任务和线程,我在控制台里打印了它们对应的 id。除此之外,我将方法名作为参数传递给了StartNew()方法。这个方法有 16 个重载版本(在撰写本文时),我使用的是如下定义的那个版本。

//
// Summary:
//     Creates and starts a task.
//
// Parameters:
//   action:
//     The action delegate to execute asynchronously.
//
// Returns:
//     The started task.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     The action argument is null.
public Task StartNew(Action action);

因为在这种情况下,MyMethod()匹配Action委托的签名,所以我对StartNew使用这个方法没有问题。

Points to Remember

让我们回忆一下行动代表背后的理论,供你参考。下面代码的方法总结:

    public delegate void Action();

它封装了一个没有参数也不返回值的方法。

在接下来的例子(演示 9)中,你看到MyMethod()不接受任何参数,它的返回类型是 void 这就是为什么我可以在StartNew()中使用方法名。

但是需要注意的是,在高级编程中,您经常会看到通用版本的动作委托。我从我的书高级 C# 中选择了以下几行(2020 年出版):

动作委托可以接受 1 到 16 个输入参数,但没有返回类型。重载版本如下:

Action<in T>
Action<in T1,in T2>
Action<in T1,in T2, in T3>
....
Action<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>

例如,如果您有一个名为CalculateSumOfThreeInts的方法,它将三个 int 作为输入参数,其返回类型为 void,如下所示:

private static void CalculateSumOfThreeInts(int i1, int i2, int i3)
{
    int sum = i1 + i2 + i3;
    Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
}

您可以使用动作委托来获取三个整数的和,如下所示:

Action<int, int, int> sum = new Action<int, int, int>( CalculateSumOfThreeInts);
sum(10, 3, 7);

否则,您可以使用如下简称:

Action<int, int, int> sum = CalculateSumOfThreeInts;
sum(10, 3, 7);

演示 9

现在进行完整的演示和输出。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DifferentWaysToCreateTask
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using different ways to create tasks.****");
            Console.WriteLine($"Inside Main().Thread ID:{Thread.CurrentThread.ManagedThreadId}");

            #region Different ways to create and execute task
            // Using constructor.
            Task taskOne = new Task(MyMethod);
            taskOne.Start();
            // Using task factory.
            TaskFactory taskFactory = new TaskFactory();
            // StartNew Method creates and starts a task.
            // It has different overloaded versions.
            Task taskTwo = taskFactory.StartNew(MyMethod);
            // Using task factory via a task.
            Task taskThree = Task.Factory.StartNew(MyMethod);
            #endregion
            Console.ReadKey();
        }

        private static void MyMethod()
        {
            Console.WriteLine($"Task.id={Task.CurrentId} with Thread id {Thread.CurrentThread.ManagedThreadId} has started.");
            Thread.Sleep(100);
            Console.WriteLine($"MyMethod for Task.id={Task.CurrentId} and Thread id {Thread.CurrentThread.ManagedThreadId} is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using different ways to create tasks.****
Inside Main().Thread ID:1
Task.id=3 with Thread id 6 has started.
Task.id=2 with Thread id 4 has started.
Task.id=1 with Thread id 5 has started.
MyMethod for Task.id=3 and Thread id 6 is completed.
MyMethod for Task.id=1 and Thread id 5 is completed.
MyMethod for Task.id=2 and Thread id 4 is completed.

Note

ManagedThreadId获取一个特定托管线程的唯一标识符 only 。在您的机器上运行应用时,您可能会注意到一个不同的值。所以,你不应该觉得既然你已经创建了 n 个线程,你应该只看到 1 到 n 之间的线程 id。可能有其他线程在后台运行。

问答环节

27.9 StartNew() 可用于匹配动作委托签名的方法。这是正确的吗?

一点也不。我在一个接受参数的StartNew重载中使用了它,参数是匹配动作委托签名的方法的名称。但是,还有其他过载版本的StartNew;例如,考虑下面的例子,你可以看到Func代表。

public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, TaskCreationOptions creationOptions);

或者,

public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, CancellationToken cancellationToken);

27.10 在之前的一个 Q & A 中,我看到了 TaskCreationOptions 。这是什么意思?

这是一个enum。您可以使用它来设定任务的行为。这是它的细节。

public enum TaskCreationOptions
{
        None = 0,
        PreferFairness = 1,
        LongRunning = 2,
        AttachedToParent = 4,
        DenyChildAttach = 8,
        HideScheduler = 16,
        RunContinuationsAsynchronously = 64,
}

在接下来的演示中,您会看到另一个重要的叫做TaskContinuationOptionsenum,它也可以帮助您设置任务行为。

使用基于任务的异步模式(TAP)

基于任务的异步模式(TAP)来自 C# 4.0。它是 C# 5.0 中的async/await的基础。TAP 引入了Task类,当异步代码块的返回值不是大问题时,使用它的通用变体Task<TResult>. Task。但是当你真的关心这个返回值时,你应该使用通用版本,Task<TResult>.你已经对Task有了一个大概的了解。让我们使用这个概念,使用ExecuteMethodOne()ExecuteMethodTwo() .实现一个基于任务的异步模式

演示 10

这是一个完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace UsingTAP
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Task-based Asynchronous Pattern.****");
            Console.WriteLine($"Inside Main().The thread ID:{Thread.CurrentThread.ManagedThreadId}");
            Task taskForMethod1 = new Task(ExecuteMethodOne);
            taskForMethod1.Start();
            ExecuteMethodTwo();
            Console.ReadKey();
        }

        private static void ExecuteMethodOne()
        {
            Console.WriteLine("Method1 has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("Method1 has completed its job now.");
        }

        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("Method2 has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            Thread.Sleep(100);
            Console.WriteLine("Method2 is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using Task-based Asynchronous Pattern.****
Inside Main().The thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside ExecuteMethodOne(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.

您刚刚看到了一个基于任务的异步模式的示例演示。我不关心ExecuteMethodOne().的返回值,但是假设你对ExecuteMethodOne()是否成功执行感兴趣。为了简单起见,在接下来的例子中,我使用了一个string消息来表示成功完成。这次,你会看到Task,的一个通用变体,在这个例子中是Task<string>。对于 lambda 表达式爱好者,我在本例中用 lambda 表达式修改了ExecuteMethodOne(),为了满足关键需求,我调整了返回类型。

在这个例子中,我添加了另一个名为ExecuteMethodThree()的方法。为了比较,这个方法最初被注释掉;执行程序,并分析输出。稍后,我取消了对它的注释,并使用方法创建了一个任务层次结构。一旦完成,程序再次执行,你会注意到当ExecuteMethodOne()完成它的任务时ExecuteMethodThree()开始运行。我保留了这些评论来帮助你理解。

现在进行演示 11。

演示 11

这是一个完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TAPDemonstration2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Task-based Asynchronous Pattern.Using lambda expression into it.****");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            // Task taskForMethod1 = new Task(Method1);
            // taskForMethod1.Start();
            Task<string> taskForMethod1 = ExecuteMethodOne();
            /*
             Wait for task to complete.
             If you use Wait() method as follows, you'll not see the  asynchonous behavior.
             */
            // taskForMethod1.Wait();
            // Continue the task
            // The taskForMethod3 will continue once taskForMethod1 is // finished
            // Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);
            ExecuteMethodTwo();
            Console.WriteLine($"Task for Method1 was a : {taskForMethod1.Result}");
            Console.ReadKey();
        }
        // Using lambda expression
        private static Task<string> ExecuteMethodOne()
        {
            return Task.Run(() =>
            {
                string result = "Failure";
                try
                {
                    Console.WriteLine("Method1 has started.");
                    Console.WriteLine($"Inside Method1(),Task.id={Task.CurrentId}");
                    Console.WriteLine($"Inside Method1(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
                    //Some big task
                    Thread.Sleep(1000);
                    Console.WriteLine("Method1 has completed its job now.");
                    result = "Success";
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Exception caught:{0}", ex.Message);
                }
                return result;
            }
            );
        }

        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("Method2 has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            Thread.Sleep(100);
            Console.WriteLine("Method2 is completed.");
        }
        private static void ExecuteMethodThree(Task task)
        {
            Console.WriteLine("Method3 starts now.");
            Console.WriteLine($"Task.id is:{Task.CurrentId} with Thread id is:{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(20);
            Console.WriteLine($"Method3 with Task.id {Task.CurrentId} and Thread id {Thread.CurrentThread.ManagedThreadId} is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside Method1(),Task.id=1
Inside Method1(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.
Task for Method1 was a : Success

分析

你有没有注意到,这一次,我没有对taskForMethod1使用Start()方法?相反,我使用了Task类中的Run()方法来执行Method1().,我为什么要这么做呢?嗯,在Task类里面,Run是一个静态方法。Visual Studio 中的方法总结对这个Run方法做了如下陈述:"Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task1 object that represents that work."`在编写的时候,这个方法有八个重载版本,如下。

public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<TResult> function);
public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task Run(Func<Task> function);
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);

现在检查这个例子中的另一个要点。如果取消对下面一行的注释,

// Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);

并再次运行该应用,您会得到类似如下的输出。

***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside Method1(),Task.id=1
Inside Method1(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.
Task for Method1 was a : Success
Method3 starts now.
Task.id is:2 with Thread id is:5
Method3 with Task.id 2 and Thread id 5 is completed.

你可以看到ContinueWith()方法有助于继续一项任务。您可能还会注意到以下内容。

TaskContinuationOptions.OnlyOnRanToCompletion

它只是声明当taskForMethod1完成它的工作时,任务将继续。同样,您可以使用enum TaskContinuationOptions选择其他选项,其描述如下。

public enum TaskContinuationOptions
{
    None = 0,
    PreferFairness = 1,
    LongRunning = 2,
    AttachedToParent = 4,
    DenyChildAttach = 8,
    HideScheduler = 16,
    LazyCancellation = 32,
    RunContinuationsAsynchronously = 64,
    NotOnRanToCompletion = 65536,
    NotOnFaulted = 131072,
    OnlyOnCanceled = 196608,
    NotOnCanceled = 262144,
    OnlyOnFaulted = 327680,
    OnlyOnRanToCompletion = 393216,
    ExecuteSynchronously = 524288
}

问答环节

27.11 我可以一次分配多项任务吗?

是的,你可以。例如,在前面修改的示例中,如果您有另一个名为 Execute MethodFour的方法,描述如下。

private static void ExecuteMethodFour(Task task)
{
    Console.WriteLine("Method4 starts now.");
    Console.WriteLine($"Task.id is:{ Task.CurrentId } with Thread id is :{ Thread.CurrentThread.ManagedThreadId } ");
            Thread.Sleep(10);
    Console.WriteLine($"Method4 with Task.id { Task.CurrentId } and Thread id { Thread.CurrentThread.ManagedThreadId } is completed."); ,
}

你可以写下面几行。

Task<string> taskForMethod1 = Method1();
Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);
 taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodFour, TaskContinuationOptions.OnlyOnRanToCompletion);

这意味着一旦taskForMethod1完成任务,你会看到taskForMethod3,的继续工作,它执行ExecuteMethodThreeExecuteMethodFour

还需要注意的是,一个延续作品可以有另一个延续作品。举个例子,如果你想要下面这样的东西。

  • 一旦 taskForMethod1 完成,则继续 taskForMethod3 和

  • 一旦 taskForMethod3 完成,就只能继续 taskForMethod4

你可以写类似下面的东西。

// Method1 starts
Task<string> taskForMethod1 = Method1();
// Task taskForMethod3 starts after taskForMethod1
Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree,
TaskContinuationOptions.OnlyOnRanToCompletion);
// Task taskForMethod4 starts after taskForMethod3
Task taskForMethod4 = taskForMethod3.ContinueWith(ExecuteMethodFour, TaskContinuationOptions.OnlyOnRanToCompletion);

使用 async 和 await 关键字

asyncawait关键字使点击模式非常灵活。从本章开始,我使用了两种方法。第一种方法是长时间运行的方法,比第二种方法需要更多的时间来完成。在接下来的例子中,我将继续使用类似的方法进行案例研究。为简单起见,我们分别称它们为Method1()Method2(),

最初,我使用 d 一个非 lambda 版本*,,但是在分析部分,我使用了代码的 lambda 表达式变体。*首先,我们再来看看Method1()

private static void Method1()
{
    Console.WriteLine("Method1 has started.");
    Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
    // Some big task
    Thread.Sleep(1000);
    Console.WriteLine("Method1 has completed its job now.");
}

当您使用 lambda 表达式并使用async/await对时,您的代码可能如下所示。

// Using lambda expression
private static async Task Method1()
{
    await Task.Run(() =>
    {
        Console.WriteLine("Method1 has started.");
        Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
        // Some big task
        Thread.Sleep(1000);
        Console.WriteLine("Method1 has completed its job now.");
    }
    );
}

你注意到一个有趣的事实了吗?同步版本和异步版本的方法体非常相似。但是许多早期实现异步编程的解决方案并不是这样的。(它们也很复杂。)

那么,await是做什么的呢?当你分析代码时,你会发现一旦你得到一个await,调用线程就会跳出这个方法,继续做别的事情。

在接下来的演示中,我使用了Task.Run,,它导致异步调用在一个单独的线程上继续。这并不意味着延续工作应该总是在一个新的线程上完成,因为有时你并不担心不同的线程;例如,当您的呼叫等待通过网络建立连接以下载某些内容时。

最后,在 nonlambda 版本(演示 12)中,我使用了下面的代码块。

private static async Task ExecuteTaskOne()
{
    await Task.Run(Method1);
}

而在Main()内部,我没有调用Method1(),而是用ExecuteTaskOne()异步执行Method1()。你可以看到我在Run方法中传递了方法名Method1。您可以看出我在这里使用了最短的重载版本的Run方法。因为Method1匹配Action委托的签名,所以您可以在Task类的Run方法中传递这个方法名作为参数。

演示 12

这是完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace UsingAsyncAwait
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            /*
             This call is not awaited.So,the current method
             continues before the call is completed.
             i.e., following async call is not awaited.
             */
            ExecuteTaskOne();
            Method2();
            Console.ReadKey();
        }

        private static async Task ExecuteTaskOne()
        {
            await Task.Run(Method1);
        }
        private static void Method1()
        {
            Console.WriteLine("Method1() has started.");
            Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("Method1() has completed its job now.");
        }

        private static void Method2()
        {
            Console.WriteLine("Method2() has started.");
            Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            //Some small task
            Thread.Sleep(100);
            Console.WriteLine("Method2() is completed.");
        }
    }
}

Note

我建议您在 Visual Studio 2019 的最新版本中执行基于任务的异步程序,以避免一些错误行为,这些行为在 Visual Studio 的旧版本中出现过。

输出

下面是一个可能的输出。

***Exploring task-based asynchronous pattern(TAP) using async and await.****
Inside Main().Thread ID:1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.

分析

在前面的输出中,您可以看到Method1()被提前调用,但是Method2()的执行并没有因此而被阻塞。请注意,此输出可能会有所不同。所以,在某些情况下,你可能还会看到Method2()Method1()之前开始。所以,如果你想让Method1()先开始,你可以在Method2()执行之前放一个小的Sleep()。您可以看到Method2()在主线程中运行,而Method1()在不同的线程中执行。

如果你喜欢使用 lambda 表达式,你可以替换下面的代码段

private static async Task ExecuteTaskOne()
{
        await Task.Run(Method1);
}

private static void Method1()
{
        Console.WriteLine("Method1() has started.");
        Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
        // Some big task
        Thread.Sleep(1000);
        Console.WriteLine("Method1() has completed its job now.");
}

用这个。

// Using lambda expression
private static async Task ExecuteMethod1()
{
    await Task.Run(() =>
    {
           Console.WriteLine("Method1() has started.");
           Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
           // Some big task
           Thread.Sleep(1000);
           Console.WriteLine("Method1() has completed its job now.");
        }
    );
}

现在在前面的演示中,您可以直接调用ExecuteMethod1()方法来获得类似的输出,而不是调用ExecuteTaskOne()

Note

在前面的示例中,您会看到下面一行的警告消息:ExecuteMethod1();,它告诉您以下内容:

Warning CS4014 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

如果您将鼠标悬停在此处,您会得到两个建议:其中一个建议告诉您应用丢弃,如下所示。

_ = ExecuteMethod1(); // applying discard

Note

从 C# 7.0 开始就支持丢弃。这些是应用中临时的、虚拟的和未使用的变量。因为这些变量可能不在已分配的存储上,所以它们可以减少内存分配。这些变量可以增强可读性和可维护性。使用下划线(_)来表示应用中的丢弃变量。

但是如果您遵循第二个建议,在该行之前插入await,如下所示。

await ExecuteMethod1();

编译器会引发另一个错误,内容如下。

Error CS4033 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

为了消除这个错误,你需要使包含async的方法(也就是说,现在你从下面一行开始。

static async Task Main(string[] args)

在应用了async/await对之后,Main()方法可能如下所示。

class Program
{
    // static void Main(string[] args)
    static async Task Main(string[] args)
    {
        Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
        Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
        await ExecuteMethod1();
        // remaining code

这个整体的讨论是为了提醒你,要把async/await一起应用,并适当放置。

我用最后一个演示结束了这一章,这一次,我稍微修改了应用的调用顺序。现在我介绍另一种叫做Method3(),的方法,类似于Method2()。这个新添加的方法可以从ExecuteTaskOne(),中调用,其结构如下。

private static async Task ExecuteTaskOne()
{
        Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
        int value=await Task.Run(Method1);
        Console.WriteLine("ExecuteTaskOne(), after await() call.");
        // Method3 will be called if Method1 executes successfully
        if (value = = 0)
        {
             Method3();
        }
}

看一下前面的代码段。它只是说我想从Method1(),获取返回值,并基于该值,我决定是否调用Method3()。所以,这一次,Method1()的返回类型不是void;相反,它返回一个int ( 0表示成功完成,否则为-1),这个方法用如下的try-catch块重新构造。

private static int Method1()
{
    int flag = 0;
    try
    {
           Console.WriteLine("Method1() has started.");
           Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
           // Some big task
           Thread.Sleep(1000);
           Console.WriteLine("Method1() has completed its job now.");
 }
 catch (Exception e)
 {
        Console.WriteLine("Caught Exception {0}", e);
        flag = -1;
 }
 return flag;
}

现在来看看下面的例子。

演示 13

这是完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncAwaitAlternateDemonstration
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
            Console.WriteLine("***This is a modified example with three methods.***");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            /*
             This call is not awaited.So,the current method
             continues before the call is completed.
             i.e., following async call is not awaited.
             */
            _ = ExecuteTaskOne();
            Method2();
            Console.ReadKey();
        }

        private static async Task ExecuteTaskOne()
        {
            Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
            int value = await Task.Run(Method1);
            Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
            /*
            Method3() will be called if Method1()
            executes successfully(i.e. if it returns 0)
            */
            if (value == 0)
            {
                Method3();
            }
        }

        private static int Method1()
        {
            int flag = 0;
            try
            {
                Console.WriteLine("Method1() has started.");
                Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
                //Some big task
                Thread.Sleep(3000);
                Console.WriteLine("Method1() has completed its job now.");
            }
            catch (Exception e)
            {
                Console.WriteLine("Caught Exception {0}", e);
                flag = -1;
            }
            return flag;
        }
        private static void Method2()
        {
            Console.WriteLine("Method2() has started.");
            Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            Console.WriteLine("Method2() is completed.");
        }
        private static void Method3()
        {
            Console.WriteLine("Method3() has started.");
            Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            Console.WriteLine("Method3() is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Exploring task-based asynchronous pattern(TAP) using async and await.****
***This is a modified example with three methods.***
Inside Main().Thread ID:1
Inside ExecuteTaskOne(), prior to await() call.
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Inside ExecuteTaskOne(), after await() call.
Method3() has started.
Inside Method3(),Thread id 4 .
Method3() is completed.

分析

仔细看看输出。你可以看到Method3()需要等待Method1()的完成,但是Method2()可以在Method1()结束执行之前完成它的执行。这里,只有当Method1()返回的值为 0 时Method3()才能继续(如果Method1()内部出现任何异常,我将标志值设置为–1)。因此,这个场景类似于演示 11 中的ContinueWith()方法。

Point to Note

在演示 13 中,请注意ExecuteTaskOne()中的以下代码行。

int value=await Task.Run(Method1);

它简单地将代码段分为两部分:前调用等待和*后调用等待。*这个语法类似于任何同步调用,但是通过使用await(在一个async方法中),您应用了一个暂停点并使用了异步编程的力量。

我用微软的一些有趣的笔记来结束这一章。当您进一步探索 async/await 关键字时,它们会很方便。记住以下几点。

  • await运算符不能出现在 lock 语句的正文中。

  • 您可能会在一个async方法的主体中看到多个await操作符。但是如果它不存在,这不会引发任何编译时错误。相反,您会得到一个警告,并且该方法会同步执行。因此,您可能会在类似的上下文中注意到下面的警告:Warning CS1998 This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread

一大章!希望我能够揭开异步编程中不同模式的神秘面纱。尽管 IAsyncResult 模式和基于事件的异步在接下来的章节中并不推荐,但我在本章中讨论了它们,因为它们有助于您理解遗留代码,它们向您展示了异步编程的发展。你可能会发现它们在将来很有用。

我对模式的讨论到此结束。我希望你喜欢学习这些模式。现在,您已经准备好使用各种模式跳入编程的汪洋大海。下面就来探讨一下剩下的边角案例,没有实践是无法掌握的。所以,继续编码吧。*****