C-7-和--NET-Core-秘籍-全-

C#7 和 .NET Core 秘籍(全)

原文:zh.annas-archive.org/md5/FFE2E66D9C939D110BF0079B0B5B3BA8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Visual Studio 2017 允许开发人员利用最新技术,在各种平台上创建世界一流的应用程序。C# 7.0 中的新语言功能是强大的工具,将使您能够编写更好的软件并更快地交付复杂的业务需求。

本书介绍了 C# 7 中所有令人惊叹的新功能。它还探讨了开发人员如何编写诊断分析器以生成更好的代码并遵守特定的代码标准。它探索了.NET Core 1.1,并介绍了如何使用 MVC 框架创建 ASP.NET Core 应用程序。

如果您对创建移动应用程序感兴趣,本书将向您展示如何使用 Cordova 进行操作。如果您想创建原生 iOS 应用程序,本书将向您展示如何使用 Mac 版 Visual Studio 进行操作。

如果无服务器计算是您想要了解更多的内容,那就不要再看了。在本书中,我们将看看无服务器计算这个术语的含义。我们还将看看如何创建 Azure 函数以及如何使用 AWS 和 S3。最后,我们将看看如何使用 C# lambda 函数与 AWS。

本书将向您展示 C#的美丽之处,结合 Visual Studio 的强大功能,使您成为一个非常强大的开发人员,能够应对各种编程挑战。

无论您在使用 C#进行编程方面的技能水平如何,本书都为每个人提供了一些内容,并将使您成为更好的开发人员。

本书内容

第一章,C# 7.0 的新功能,展示了 C# 7.0 为 C#语言带来了许多新功能。其中最重要的功能是元组和模式匹配。了解 C# 7.0 中的新功能对开发人员来说至关重要,以保持竞争力。

第二章,类和泛型,构成了软件开发的基本构件,并且在构建良好的代码中至关重要。类的强大之处在于描述我们周围的世界,并将其转化为编译器可以理解的编程语言,使开发人员能够创建出色的软件。

第三章,C#中的面向对象编程,是 C#和面向对象编程(OOP)的基础。理解这个概念对于.NET 开发人员至关重要。

第四章,Visual Studio 中的代码分析器,讨论了代码分析器如何帮助开发人员编写更好的代码。确保您或您的团队发布的代码符合您特定的代码质量标准。

第五章,正则表达式,介绍了正则表达式如何利用描述字符串的模式,通过使用特殊字符来匹配特定的文本片段。适当应用正则表达式可以成为解决某些编程问题的非常强大的方法。

第六章,处理文件、流和序列化,教导读者处理文件、流和序列化是作为开发人员您将多次进行的工作。能够正确地做到这一点肯定会让您作为开发人员具有优势。

第七章,使用异步编程使应用程序响应,讨论了异步编程是 C#中一项令人兴奋的功能。它允许您在主线程上继续程序执行,同时长时间运行的任务完成其执行。这使您的应用程序保持响应。

第八章,使用 C#进行并行和多线程高性能编程,可以提高代码的性能。使用多线程和并行编程可能意味着用户体验应用程序是工作正常还是出现故障的区别。

第九章,使用响应式扩展组合基于事件的程序,教导读者 Rx 是一项令人兴奋的技术。如果您需要在应用程序中创建搜索即时功能,让数据更改通知您的应用程序而不必一直轮询数据(比如股票价格),或者通常使您的应用程序更具响应性,那么您很可能需要考虑使用 Rx。

第十章,探索.NET Core 1.1,是关于.NET Core 目前的热点。它允许您创建在 Windows、Linux 和 macOS 上运行的跨平台应用程序。了解如何使用它对所有.NET 开发人员都是必不可少的。

第十一章,ASP.NET Core on the MVC Framework,讲述了 MVC 框架,它根据其遵循的 MVC 设计模式命名。它允许开发人员通过让框架的每个组件专注于一个特定的事物来分离逻辑。正是这种关注点的分离使得 MVC 如此强大,特别是与.NET Core 结合使用时。

第十二章,选择和使用源代码控制策略,是关于源代码控制如何成为每个开发人员工具包的重要组成部分。无论您是业余爱好者还是专业程序员,当您离开办公桌回家时,最好确保您的代码是安全的。

第十三章,在 Visual Studio 中创建移动应用程序,允许开发人员轻松创建移动应用程序。它还为开发人员提供了更多关于如何去做的选择。从使用 Xamarin 创建移动应用程序,到使用 Cordova,再到使用 Visual Studio for Mac 编写原生 iOS 应用程序,.NET 开发人员可以使用他们熟悉和喜爱的 IDE 扩展他们的服务提供。

第十四章,在 Visual Studio 中编写安全代码和调试,讲述了调试是开发人员经常做的事情。能够高效地进行调试是另一回事。同样,能够编写安全代码对任何开发人员来说都是必不可少的。创建安全代码并能够高效地调试该代码将产生更好的最终产品。

第十五章,在 Azure Service Fabric 上创建微服务,讲述了传统上,开发人员以单片方式编写应用程序,这是一个单一的可执行文件,通过类等组件进行分解。微服务是一种旨在解决传统单片应用程序问题的技术。它允许开发人员创建可以独立运行而不依赖于任何其他服务的较小部分(服务)。

第十六章,Azure 和无服务器计算,讲述了无服务器并不意味着没有服务器,而是您(或应用程序)不知道使用哪个服务器来为应用程序提供某些功能。将应用程序的一些逻辑拆分为无服务器类型的架构可以在负载增加(或减少)时实现极端的可扩展性,提高性能,并减少编写和调试的代码量。

您需要为这本书做好准备

您将需要最新版本的 Visual Studio 2017。对于一些关于移动开发的章节,您将需要一个活跃的 Apple iTunes 账户。关于最后一章,您需要创建一个免费的 AWS 账户。最后,一些配方可能还需要访问 Azure 门户。

这本书是为谁准备的

这本书将吸引对 C#和.NET 有基本了解的开发人员,以及对 Visual Studio 2015 环境有基本熟悉的开发人员。

部分

在这本书中,您会经常看到几个标题(准备就绪,如何做,它是如何工作的,还有更多,以及参见)。

为了清晰地说明如何完成一个配方,我们使用以下各节:

准备工作

本节告诉您在配方中可以期待什么,并描述了为配方设置任何软件或所需的任何初步设置。

操作方法…

本节包含所需的跟随配方的步骤。

工作原理…

本节通常包括对前一节发生的事情的详细解释。

还有更多…

本节包括有关配方的其他信息,以使读者对配方更加了解。

另请参阅

本节提供了有用的链接,指向配方的其他有用信息。

约定

在本书中,您会发现一些区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"您将在附带源代码的_ 数据库脚本文件夹中找到该脚本。"

代码块设置如下:

public override void Initialize(AnalysisContext context)
{
  context.RegisterSymbolAction(AnalyzeSymbol,
  SymbolKind.NamedType);
} 

新术语重要单词以粗体显示。例如,您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:"从工具菜单中,选择 NuGet Package Manager,然后选择 Manage NuGet Packages for Solution...."

警告或重要说明会以这样的方式出现在一个框中。

提示和技巧会以这样的方式出现。

第一章:C# 7.0 中的新功能

在本章中,我们将通过以下配方来查看 C# 7.0 的功能:

  • 使用元组-入门

  • 使用元组-深入了解

  • 模式匹配

  • 输出变量

  • 解构

  • 本地函数

  • 文字的改进

  • 引用返回和本地变量

  • 通用异步返回类型

  • 访问器、构造函数和终结器的表达式主体

  • 抛出表达式

介绍

C# 7.0 为 C#语言带来了许多新功能。如果在 C# 6.0 发布后仍感到不满意,那么 C# 7.0 绝对不会让您失望。它专注于消耗数据,简化代码和提高性能。C#程序经理 Mads Torgersen 指出,C# 7.0 最大的功能是元组。另一个是模式匹配。这两个功能(以及其他功能)受到了全球 C#开发人员的热情欢迎。因此,毫无疑问,开发人员将立即开始实施 C# 7.0 引入的这些新功能。因此,尽快了解 C# 7.0 提供的内容并在开发项目中实施新的语言功能将非常有益。

在本书中,我将使用 Visual Studio 2017 的发行候选版。在撰写和最终发布 Visual Studio 2017 之间,某些功能和方法可能会发生变化。

使用元组-入门

我遇到了许多情况,我想从一个方法中返回多个值。正如 Mads Torgersen 指出的,开发人员现有的选项并不理想。因此,C# 7.0 引入了元组类型元组文字,以便让开发人员轻松地从方法中返回多个值。开发人员在创建元组时也可以放心。元组是结构体,是值类型。这意味着它们是在本地创建的,并且通过复制内容传递。元组也是可变的,元组元素是公共可变字段。我个人对使用元组感到非常兴奋。让我们在下一个配方中更详细地探讨元组。

做好准备

首先,在 Visual Studio 2017 中创建一个常规控制台应用程序。只需将您创建的项目命名为烹饪书。在我开始使用 C# 7.0 中的元组之前,我需要添加一个 NuGet 包。请记住,我正在使用 Visual Studio 的发行候选版。这个过程可能会在产品最终发布之前发生变化。

  1. 要做到这一点,请转到工具,NuGet 包管理器,然后单击“解决方案的 NuGet 包管理器...”。

  1. 选择浏览选项卡,然后在搜索框中键入 ValueTuple。应显示 Microsoft NuGet 包中的 System.ValueTuple。在“解决方案的管理包”下选择烹饪书项目,然后单击“安装”按钮。

请注意,我在撰写本书的部分内容时使用的是 Visual Studio 2017 RC。在最终版本发布后,您可能不需要从 NuGet 添加System.ValueTuple。然而,从 NuGet 添加System.ValueTuple可能仍然是一个要求。只有时间会告诉我们。

  1. Visual Studio 现在会显示一个提示,让您审查即将对项目进行的更改。只需单击“确定”按钮。最后,您需要提供 Microsoft 要求的许可协议。只需单击“我接受”按钮。Visual Studio 现在将开始安装 NuGet 包。它将在输出窗口中显示其进度。

完成所有这些后,我的 Visual Studio 解决方案如下:

现在,您将准备好创建与元组一起使用的第一个方法。让我们看看如何做到这一点。

如何做...

  1. 首先,在 Visual Studio 控制台应用程序的Program.cs文件中创建一个新类。你可以随意命名你的类,但出于本书的目的,我将简单地称我的类为Chapter1。你的代码现在应该如下所示:
        namespace cookbook
        {
          class Program
          {
            static void Main(string[] args)
            {

            }
          }

          public class Chapter1
          {

          }
        }

  1. 这是我们将在本章中使用的格式。假设我们想要编写一个方法,需要计算变量数量的学生的平均分数。每个班级的学生人数都不相同。因此,我们希望我们的方法返回用于计算平均分数的班级学生人数。更改static void main方法以包含分数列表。我们还创建了Chapter1类的新实例,并调用GetAverageAndCount()方法,该方法将用于返回我们需要的两个值。

我将为了说明目的而硬编码这些值;但实际上,这些分数可以是任意数量的学生。确保按照我在代码清单中的方式添加值,因为我将在本教程的最后说明一个问题。

        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
          Chapter1 ch1 = new Chapter1();
          var s = ch1.GetAverageAndCount(scores);
        }

  1. 在这里,我们可以利用元组的强大功能来声明Chapter1类中的GetAverageAndCount()方法。它接受一个整数分数数组,并如下所示:
        public (int, int) GetAverageAndCount(int[] scores)
        {

        }

  1. 注意返回的元组类型(int, int)。我们只从GetAverageAndCount()方法返回两个值,但实际上,如果需要,可以返回多个值。为了运行代码示例,我们将创建此方法的虚拟实现。只需包含一个返回两个零的元组文字即可。
        public (int, int) GetAverageAndCount(int[] scores)
        {
          var returnTuple = (0, 0);
          return returnTuple;
        }

  1. 回到调用元组返回方法的static void Main方法,并编写代码来使用返回值。你创建的每个元组都将公开名为Item1Item2Item3等的成员。这些用于获取从元组返回方法返回的值。
        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
          Chapter1 ch1 = new Chapter1();
          var s = ch1.GetAverageAndCount(scores);
          WriteLine($"Average was {s.Item1} across {s.Item2} students");
          ReadLine();
        }

  1. 在命名空间之前添加以下using指令。
        using static System.Console;

  1. 你会注意到我们使用s.Item1s.Item2来引用从GetAverageAndCount()方法返回的返回值。虽然这是完全合法的,但它并不是很描述性,使得难以推断变量的使用方式。这基本上意味着你必须记住Item1是平均值,Item2是计数值。也许,情况正好相反?Item1是计数,Item2是平均值?这实际上取决于你在GetAverageAndCount()方法中所做的事情(这可能随时间而改变)。因此,我们的元组返回方法可以进行如下增强:
        public (int average, int studentCount) 
          GetAverageAndCount(int[] scores)
        {
          var returnTuple = (0, 0);
          return returnTuple;
        }

  1. 现在,元组返回类型可以为其元素声明变量名。这使得调用GetAverageAndCount()方法的调用者可以轻松知道哪个值是哪个。你仍然可以继续使用s.Item1s.Item2,但现在更容易相应地更改static void Main方法中的调用代码:
        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
          Chapter1 ch1 = new Chapter1();
          var s = ch1.GetAverageAndCount(scores);
          WriteLine($"Average was {s.average} across {
            s.studentCount} students");
          ReadLine();
        }

  1. 更改WriteLine中的插值字符串,我们可以看到元组返回的值的使用方式更加清晰。现在你知道第一个值是平均值,第二个值是用于计算平均值的学生数量。然而,元组允许开发人员更灵活地操作。记得GetAverageAndCount()方法中的元组文字吗?我们只需在虚拟实现中添加如下内容:
        var returnTuple = (0, 0);

  1. C# 7.0 还允许开发人员向元组文字添加名称。在GetAverageAndCount()方法中,将元组文字更改如下:
        var returnTuple = (ave:0, sCount:0);

  1. 我刚刚给第一个值命名为ave(表示平均值),第二个值命名为sCount(表示学生人数)。这真是令人兴奋的事情!在修改了元组文字之后,GetAverageAndCount()方法的虚拟实现应如下所示:
        public (int average, int studentCount) 
          GetAverageAndCount(int[] scores)
        {
          var returnTuple = (ave:0, sCount:0);
          return returnTuple;
        }

元组之间的配合非常好。只要元组类型匹配,你就不必担心元组文字中的avesCount名称与返回类型的averagestudentCount名称不匹配。

工作原理...

到目前为止,在本示例中,我们已经看到元组在需要从方法返回多个值时为开发人员提供了很大的灵活性。虽然GetAverageAndCount()的虚拟实现只是返回了值为零的元组文字,但它让您对元组是如何连接有了一些想法。这个示例是下一个示例的基础。我鼓励您彻底阅读这两个示例,以充分理解元组及其用法。

使用元组-深入研究

现在我将开始为我们在上一个示例中创建的GetAverageAndCount()方法的虚拟实现添加更多内容。如果您对元组不熟悉,并且还没有完成上一个示例,请先完成上一个示例,然后再开始本示例的工作。

准备工作

您需要完成上一个示例使用元组-入门中的代码步骤,才能继续进行本示例的工作。确保您已添加了上一个示例中指定的所需 NuGet 软件包。

如何做...

  1. 让我们再次看一下调用代码。通过摆脱var s,我们可以进一步简化static void Main方法中的代码。当我们调用GetAverageAndCount()方法时,我们将元组返回到var s中。
        var s = ch1.GetAverageAndCount(scores);

  1. 我们不必这样做。C# 7.0 允许我们立即将元组分割为其各自的部分,如下所示:
        var (average, studentCount) = ch1.GetAverageAndCount(scores);

  1. 现在我们可以直接使用元组返回的值:
        WriteLine($"Average was {average} across {studentCount} students");

  1. 在实现GetAverageAndCount()方法之前,请确保您的static void Main方法如下所示:
        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
          Chapter1 ch1 = new Chapter1();
          var (average, studentCount) = ch1.GetAverageAndCount(scores);
          WriteLine($"Average was {average} across {
            studentCount} students");
          ReadLine();
        }

  1. 其次,确保GetAverageAndCount()方法的虚拟实现如下所示:
        public (int average, int studentCount) 
          GetAverageAndCount(int[] scores)
        {
          var returnTuple = (ave:0, sCount:0);
          return returnTuple;
        }

  1. 继续运行控制台应用程序。您将看到averagestudentCount两个值从我们的GetAverageAndCount()虚拟实现中返回。

  1. 数值显然仍然为零,因为我们还没有在方法内定义任何逻辑。我们接下来会这样做。在编写实现之前,请确保已添加以下using语句:
        using System.Linq;

  1. 因为我们在变量scores上使用了整数数组,所以我们可以轻松地返回所需的结果。通过编写scores.Sum(),LINQ 允许我们获得scores数组中包含的学生成绩的总和。我们还可以通过编写scores.Count()轻松地获得scores数组中学生成绩的计数。因此,平均值逻辑上应该是分数之和除以学生成绩的计数(scores.Sum()/scores.Count())。然后,我们将值放入我们的returnTuple文字中,如下所示:
        public (int average, int studentCount) 
          GetAverageAndCount(int[] scores)
        {
          var returnTuple = (ave:0, sCount:0);
          returnTuple = (returnTuple.ave = scores.Sum()/scores.Count(),
                         returnTuple.sCount = scores.Count());
          return returnTuple;
        }

  1. 运行控制台应用程序以查看以下显示的结果:

  1. 我们可以看到班级平均分并不太好,但这对我们的代码来说并不重要。另一行代码也不太好的是这一行:
        returnTuple = (returnTuple.ave = scores.Sum()/scores.Count(), 
                       returnTuple.sCount = scores.Count());

  1. 这有点笨拙,读起来不太顺畅。让我们简化一下。记住我之前提到过,只要它们的类型匹配,元组就可以很好地配合使用?这意味着我们可以这样做:
        public (int average, int studentCount)
          GetAverageAndCount(int[] scores)
        {
          var returnTuple = (ave:0, sCount:0);
          returnTuple = (scores.Sum()/scores.Count(), scores.Count());
          return returnTuple;
        }

  1. 再次运行控制台应用程序,注意结果保持不变:

  1. 那么为什么一开始要给元组文字名称呢?好吧,这样可以让您在GetAverageAndCount()方法中轻松引用它们。在方法中使用foreach循环时,这也非常有用。考虑以下情况。除了返回学生成绩的计数和平均值之外,我们还需要在班级平均分低于某个阈值时返回一个额外的布尔值。在本示例中,我们将使用一个名为CheckIfBelowAverage()的扩展方法,并将一个整数参数作为threshold值。首先创建一个名为ExtensionMethods的新静态类。
        public static class ExtensionMethods
        {

        }

  1. static类中,创建一个名为CheckIfBelowAverage()的新方法,并传递一个名为threshold的整数值。这个扩展方法的实现非常简单,所以我不会在这里详细介绍。
        public static bool CheckIfBelowAverage(
          this int classAverage, int threshold)
        {
          if (classAverage < threshold)
          {
            // Notify head of department
            return true;
          }
          else
            return false;
        }

  1. Chapter1类中,通过更改其签名并传递需要应用的阈值的值,重载GetAverageAndCount()方法。您会记得我提到过元组返回类型的方法可以返回多个值,不仅仅是两个。在这个例子中,我们返回了一个名为belowAverage的第三个值,它将指示计算出的班级平均值是否低于我们传递给它的阈值值。
        public (int average, int studentCount, bool belowAverage) 
          GetAverageAndCount(int[] scores, int threshold)
        {

        }

  1. 修改元组文字,将其添加到subAve,并将其默认为true,因为零的班级平均值在逻辑上低于我们传递给它的任何阈值值。
        var returnTuple = (ave: 0, sCount: 0, subAve: true);

  1. 现在我们可以在我们的元组文字值上调用扩展方法CheckIfBelowAverage(),并通过threshold变量传递它。当我们用它来调用扩展方法时,给元组文字起逻辑名称变得非常有用。
        returnTuple = (scores.Sum() / scores.Count(), scores.Count(), 
                       returnTuple.ave.CheckIfBelowAverage(threshold));

  1. 您的完成的GetAverageAndCount()方法现在应该如下所示:
        public (int average, int studentCount, bool belowAverage) 
          GetAverageAndCount(int[] scores, int threshold)
        {
          var returnTuple = (ave: 0, sCount: 0, subAve: true);
          returnTuple = (scores.Sum() / scores.Count(), scores.Count(), 
          returnTuple.ave.CheckIfBelowAverage(threshold)); 
          return returnTuple;
        }

  1. 修改您的调用代码,以使用重载的GetAverageAndCount()方法如下所示:
        int threshold = 51;
        var (average, studentCount, belowAverage) = ch1.GetAverageAndCount(
                                                   scores, threshold);

  1. 最后,修改插值字符串如下所示:
        WriteLine($"Average was {average} across {studentCount}
                  students. {(average < threshold ? 
                  " Class score below average." : 
                  " Class score above average.")}");

  1. 您的static void Main方法中的完成代码现在应该如下所示:
        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
          Chapter1 ch1 = new Chapter1();
          int threshold = 51;
          var (average, studentCount, belowAverage) = 
               ch1.GetAverageAndCount(scores, threshold);
          WriteLine($"Average was {average} across {studentCount} 
                    students. {(average < threshold ? 
                    " Class score below average." : 
                    " Class score above average.")}");
          ReadLine();
        }

  1. 运行您的控制台应用程序以查看结果。

  1. 测试三元运算符?在插值字符串中是否正确工作,将您的阈值值修改为低于返回的平均值。
        int threshold = 40;

  1. 再次运行您的控制台应用程序将得到一个通过的平均班级分数。

  1. 最后,我需要强调这个食谱中存在一个明显的问题。我相信你已经注意到了。如果没有,不要担心。这有点狡猾。这是我在这个食谱开始时提到的陷阱,我故意想要包括它来说明代码中的错误。我们的学生成绩数组定义如下:
        int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };

  1. 这些总和等于 400,因为只有 8 个分数,所以值将正确计算,因为它分成一个整数 (400 / 8 = 50)。但是如果我们在其中加入另一个学生的分数会发生什么呢?让我们来看看。修改您的分数数组如下:
        int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24, 49 };

  1. 再次运行您的控制台应用程序并查看结果。

  1. 问题在于平均值是不正确的。它应该是 49.89。我们知道我们想要一个 double(除非您的应用程序意图返回一个整数)。因此,我们需要注意在返回类型和元组文字中正确地转换值。我们还需要在扩展方法CheckIfBelowAverage()中处理这个问题。首先,通过以下方式更改扩展方法签名以作用于 double。
        public static bool CheckIfBelowAverage(
          this double classAverage, int threshold)
        {

        }

  1. 然后,我们需要将元组方法返回类型中的average变量的数据类型更改为如下:
        public (double average, int studentCount, bool belowAverage) 
               GetAverageAndCount(int[] scores, int threshold)
        {

        }

  1. 然后,通过使用ave: 0D,修改元组文字,使ave成为一个 double。
        var returnTuple = (ave: 0D, sCount: 0, subAve: true);

  1. 将平均值计算转换为double
        returnTuple = ((double)scores.Sum() / scores.Count(),
          scores.Count(), 
        returnTuple.ave.CheckIfBelowAverage(threshold));

  1. 向您的应用程序添加以下using语句:
        using static System.Math;

  1. 最后,在插值字符串中使用Round方法将average变量格式化为两位小数。
        WriteLine($"Average was {Round(average,2)} across {studentCount}
                  students. {(average < threshold ? 
                             " Class score below average." : 
                             " Class score above average.")}");

  1. 如果一切都做得正确,您的GetAverageAndCount()方法应该如下所示:
        public (double average, int studentCount, bool belowAverage) 
               GetAverageAndCount(int[] scores, int threshold)
        {
          var returnTuple = (ave: 0D, sCount: 0, subAve: true);
          returnTuple = ((double)scores.Sum() / scores.Count(), 
                          scores.Count(),   
                          returnTuple.ave.CheckIfBelowAverage(
                          threshold));
          return returnTuple;
        }

  1. 您的调用代码也应该如下所示:
        static void Main(string[] args)
        {
          int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24, 49 }; 
          Chapter1 ch1 = new Chapter1();
          int threshold = 40;
          var (average, studentCount, belowAverage) = 
               ch1.GetAverageAndCount(scores, threshold);
          WriteLine($"Average was {Round(average,2)} across 
                    {studentCount} students. {(average < threshold ? 
                    " Class score below average." : 
                    " Class score above average.")}");
          ReadLine();
        }

  1. 运行控制台应用程序,以查看学生成绩的正确平均值。

它是如何工作的...

元组是结构体,因此是在本地创建的值类型。因此,您不必担心在使用和分配元组时产生大量分配。它们的内容在传递时仅仅是复制。元组是可变的,元素是公开范围的可变字段。使用本配方中的代码示例,因此我可以做以下事情:

returnTuple = (returnTuple.ave + 15, returnTuple.sCount - 1);

C# 7.0 允许我首先更新平均值(将平均值上移),然后递减计数字段。元组是 C# 7.0 的一个非常强大的特性,当正确实现时,对许多开发人员将大有裨益。

模式匹配

C# 7.0 引入了一种与函数式编程语言常见的方面相同的模式匹配。这种新类型的结构可以以不同的方式测试值。为了实现这一点,C# 7.0 中的两种语言构造已经得到增强,以利用模式。这些如下:

  • is表达式

  • switch语句中的case子句

关于is表达式,开发人员现在可以在右侧使用模式,而不仅仅是类型。在switch语句中,case子句现在可以匹配模式。switch语句不再局限于原始类型,可以在任何东西上进行切换。让我们首先看一下is表达式。

准备工作

为了说明模式匹配的概念,假设以下情景。我们有两种对象类型,称为StudentProfessor。我们想要最小化代码,所以我们想要创建一个单一的方法来输出传递给它的对象的数据。这个对象可以是StudentProfessor对象。该方法需要弄清楚它正在处理哪个对象,并相应地采取行动。但首先,我们需要在控制台应用程序中做一些事情来设置好一切:

  1. 确保已添加以下using语句。
        using System.Collections.Generic;

  1. 现在,您需要创建两个名为StudentProfessor的新类。Student类的代码需要如下所示:
        public class Student
        {
          public string Name { get; set; }
          public string LastName { get; set; } 
          public List<int> CourseCodes { get; set; }
        }

  1. 接下来,Professor类的代码需要如下所示:
        public class Professor
        {
          public string Name { get; set; }
          public string LastName { get; set; }
          public List<string> TeachesSubjects { get; set; }
        }

要理解我们使用模式匹配的目的,我们首先需要了解我们来自何处。我将在下一节开始时向您展示开发人员在 C# 7.0 之前可能如何编写此代码。

如何做...

  1. Chapter1类中,创建一个名为OutputInformation()的新方法,该方法以一个人对象作为参数。
        public void OutputInformation(object person)
        {

        }

  1. 在这个方法中,我们需要检查传递给它的对象的类型。传统上,我们需要做以下事情:
        if (person is Student)
        {
          Student student = (Student)person;
          WriteLine($"Student {student.Name} {student.LastName}
                    is enrolled for courses {String.Join<int>(
                    ", ", student.CourseCodes)}");
        }

        if (person is Professor)
        {
          Professor prof = (Professor)person;
          WriteLine($"Professor {prof.Name} {prof.LastName} 
                    teaches {String.Join<string>(",", prof.TeachesSubjects)}");
        }

  1. 我们有两个if语句。我们期望的是Student对象或Professor对象。完整的OutputInformation()方法应如下所示:
        public void OutputInformation(object person)
        {
          if (person is Student)
          {
            Student student = (Student)person;
            WriteLine($"Student {student.Name} {student.LastName}
                      is enrolled for courses {String.Join<int>
                      (", ", student.CourseCodes)}");
          }
          if (person is Professor)
          {
            Professor prof = (Professor)person;
            WriteLine($"Professor {prof.Name} {prof.LastName}
                      teaches {String.Join<string>
                      (",", prof.TeachesSubjects)}");
            }
          }

  1. static void Main中调用这个方法非常容易。这两个对象是相似的,但它们包含的列表不同。Student对象公开了一个课程代码列表,而Professor公开了一个教给学生的科目列表。
        static void Main(string[] args)
        {
          Chapter1 ch1 = new Chapter1();

          Student student = new Student();
          student.Name = "Dirk";
          student.LastName = "Strauss";
          student.CourseCodes = new List<int> { 203, 202, 101 };

          ch1.OutputInformation(student);

          Professor prof = new Professor();
          prof.Name = "Reinhardt";
          prof.LastName = "Botha";
          prof.TeachesSubjects = new List<string> {
               "Mobile Development", "Cryptography" };

          ch1.OutputInformation(prof);
        }

  1. 运行控制台应用程序,看看OutputInformation()方法的运行情况。

  1. 虽然我们在控制台应用程序中看到的信息是我们所期望的,但我们可以通过模式匹配更简化OutputInformation()方法中的代码。为此,请修改代码如下:
        if (person is Student student)
        {

        }
        if (person is Professor prof)
        {

        }

  1. 第一个if表达式检查对象person是否是Student类型。如果是,它将该值存储在student变量中。对于第二个if表达式也是如此。如果为真,则将person的值存储在prof变量中。为了使代码执行到每个if表达式的大括号之间的代码,条件必须评估为真。因此,我们可以省去将person对象转换为StudentProfessor类型的转换,直接使用studentprof变量,如下所示:
        if (person is Student student)
        {
          WriteLine($"Student {student.Name} {student.LastName}
                    is enrolled for courses {String.Join<int>
                    (", ", student.CourseCodes)}");
        }
        if (person is Professor prof)
        {
          WriteLine($"Professor {prof.Name} {prof.LastName}
                    teaches {String.Join<string>
                    (",", prof.TeachesSubjects)}");
        }

  1. 再次运行控制台应用程序,您将看到输出与以前完全相同。但是,我们编写了更好的代码,使用类型模式匹配来确定要显示的正确输出。

  1. 然而,模式并不止于此。您还可以在常量模式中使用它们,这是最简单的模式类型。让我们看看对常量null的检查。通过模式匹配,我们可以改进我们的OutputInformation()方法如下:
        public void OutputInformation(object person)
        {
          if (person is null)
          {
            WriteLine($"Object {nameof(person)} is null");
          }
        }

  1. 更改调用OutputInformation()方法的代码并将其设置为null
        Student student = null;

  1. 运行您的控制台应用程序并查看显示的消息。

在这里使用nameof关键字是一个好习惯。如果变量名person需要更改,相应的输出也将被更改。

  1. 最后,C# 7.0 中的switch语句已经改进,以利用模式匹配。C# 7.0 允许我们切换到任何内容,而不仅仅是基本类型和字符串。case子句现在使用模式,这真的很令人兴奋。让我们看看如何在以下代码示例中实现这一点。我们将继续使用StudentProfessor类型来说明switch语句中模式匹配的概念。修改OutputInformation()方法并包括如下的样板switch语句。switch语句仍然具有默认值,但现在可以做更多事情。
        public void OutputInformation(object person)
        {
          switch (person)
          {
            default:
              WriteLine("Unknown object detected");
            break;
          }
        }

  1. 我们可以扩展case语句以检查Professor类型。如果它将对象匹配到Professor类型,它可以在case语句的主体中对该对象进行操作并将其用作Professor类型。这意味着我们可以调用Professor特定的TeachesSubjects属性。我们可以这样做:
        switch (person)
        {
          case Professor prof:
            WriteLine($"Professor {prof.Name} {prof.LastName}
                      teaches {String.Join<string>
                      (",", prof.TeachesSubjects)}");
          break;
          default:
            WriteLine("Unknown object detected");
          break;
        }

  1. 我们也可以对Student类型执行相同的操作。更改switch的代码如下:
        switch (person)
        {
          case Student student:
            WriteLine($"Student {student.Name} {student.LastName}
                      is enrolled for courses {String.Join<int>
                      (", ", student.CourseCodes)}");
          break;
          case Professor prof:
            WriteLine($"Professor {prof.Name} {prof.LastName}
                      teaches {String.Join<string>
                      (",", prof.TeachesSubjects)}");
          break;
          default:
            WriteLine("Unknown object detected");
          break;
        }

  1. case语句的最后一个(也是很棒的)特性尚待说明。我们还可以实现一个when条件,类似于我们在 C# 6.0 中看到的异常过滤器。when条件只是评估为布尔值,并进一步过滤它触发的输入。要看到这一点的效果,请相应地更改switch
        switch (person)
        {
          case Student student when (student.CourseCodes.Contains(203)):
          WriteLine($"Student {student.Name} {student.LastName}
                    is enrolled for course 203.");
          break;
          case Student student:
          WriteLine($"Student {student.Name} {student.LastName}
                    is enrolled for courses {String.Join<int>
                    (", ", student.CourseCodes)}");
          break;
          case Professor prof:
          WriteLine($"Professor {prof.Name} {prof.LastName}
                    teaches {String.Join<string>(",",
                    prof.TeachesSubjects)}");
          break;
          default:
            WriteLine("Unknown object detected");
          break;
        }

  1. 最后,为了全面检查空值,我们可以修改我们的switch语句以适应这些情况。因此,完成的switch语句如下所示:
        switch (person)
       {
          case Student student when (student.CourseCodes.Contains(203)):
            WriteLine($"Student {student.Name} {student.LastName} 
                      is enrolled for course 203.");
          break;
          case Student student:
          WriteLine($"Student {student.Name} {student.LastName} 
                    is enrolled for courses {String.Join<int>
                    (", ", student.CourseCodes)}");
          break;
          case Professor prof:
          WriteLine($"Professor {prof.Name} {prof.LastName}
                    teaches {String.Join<string>
                    (",", prof.TeachesSubjects)}");
          break;
          case null:
            WriteLine($"Object {nameof(person)} is null");
          break;
          default:
            WriteLine("Unknown object detected");
          break;
        }

  1. 再次运行控制台应用程序,您将看到第一个包含when条件的case语句对Student类型触发。

它是如何工作的...

通过模式匹配,我们看到模式用于测试值是否属于某种类型。

您还会听到一些开发人员说他们测试值是否具有特定的形状

当我们找到匹配时,我们可以获取特定于该类型(或形状)的信息。我们在访问特定于Student类型的CourseCodes属性的代码中看到了这一点,以及特定于Professor类型的TeachesSubjects属性。

最后,您现在需要仔细注意您的case语句的顺序,这很重要。使用when子句的case语句比仅检查Student类型的语句更具体。这意味着when情况需要在Student情况之前发生,因为这两种情况都是Student类型。如果Student情况发生在when子句之前,它将永远不会触发具有课程代码 203 的Studentsswitch

另一个重要的事情要记住的是,default子句将始终最后进行评估,无论它出现在switch语句的何处。因此,在switch语句中将其写为最后一个子句是一个很好的做法。

输出变量

C# 7.0 对out变量进行了重新审视。这是一个小改变,但确实改善了代码的可读性和流畅性。以前,我们首先必须声明一个变量作为方法中的 out 参数。在 C# 7.0 中,我们不再需要这样做。

准备工作

我们将使用一个经常使用的方法来测试值是否为特定类型。是的,你猜对了,我们将使用TryParse。我已经能听到一些人抱怨了(还是只有我?)。对我来说,使用TryParse是一件苦乐参半的事情。能够尝试解析一些东西以测试其是否有效是很好的,但是out变量的使用从来没有像我想象的那样整洁。如果您不熟悉TryParse方法,它是一个测试值是否解析为特定类型的方法。如果是,TryParse将返回一个布尔值true;否则,它将返回false

如何做...

  1. 以下代码示例将说明我们以前如何使用TryParse来检查字符串值是否为有效整数。您会注意到,我们不得不声明整数变量intVal,它被用作out变量。intVal变量通常悬空在那里,通常没有初始化,等待在TryParse中使用。
        string sValue = "500";

        int intVal;
        if (int.TryParse(sValue, out intVal))
        {
          WriteLine($"{intVal} is a valid integer");
          // Do something with intVal
        }

  1. 在 C# 7.0 中,这已经简化了,如下面的代码示例所示。我们现在可以在将其作为 out 参数传递的地方声明out变量,就像这样:
        if (int.TryParse(sValue, out int intVal))
        {
          WriteLine($"{intVal} is a valid integer");
          // Do something with intVal
        }

  1. 这是一个小改变,但非常好。运行控制台应用程序并检查显示的输出。

  1. 当我们将out变量声明为out参数的参数时,编译器将能够推断出类型应该是什么。这意味着我们也可以使用var关键字,就像这样:
        if (int.TryParse(sValue, out var intVal))
        {
          WriteLine($"{intVal} is a valid integer");
          // Do something with intVal
        }

它是如何工作的...

C# 7.0 对out变量所做的更改并不重大。然而,对于经常使用它的开发人员来说,这是一个很大的便利。到目前为止,在本章中,我们已经看到了元组的使用,模式匹配和out变量。我们可以轻松地将我们学到的一些内容结合起来,创造出一些真正独特的东西。考虑使用扩展方法,元组和out变量。我们可以轻松地创建一个名为ToInt()的扩展方法,其实现如下:

public static (string originalValue, int integerValue, bool isInteger) ToInt(this string stringValue)
{
  var t = (original: stringValue, toIntegerValue: 0, isInt: false);
  if (int.TryParse(stringValue, out var iValue)) 
  {
    t.toIntegerValue = iValue; t.isInt = true;
  }
  return t;
}

我们创建了一个 Tuple 文字,如果TryParse返回 false,它将被返回。如果TryParsetrue,我设置了t.toIntegerValuet.isInt值。调用扩展方法的代码如下:

var (original, intVal, isInteger) = sValue.ToInt();
if (isInteger)
{
  WriteLine($"{original} is a valid integer");
  // Do something with intVal
}

当您运行控制台应用程序时,您会发现输出与以前完全相同。这只是说明了 C# 7.0 中新功能与彼此结合的强大力量。再加上一些模式匹配,我们将拥有一个非常有效的扩展方法。我会让你们继续玩耍。有很多东西等待你们去发现。

解构

元组可以使用解构声明进行消耗。这只是将元组拆分为其各个部分,并将这些部分分配给新变量。这称为解构,不仅适用于元组。

准备工作

还记得我们在本章开头使用元组吗?嗯,我们使用类似以下代码来获取元组文字返回的值。

var (average, studentCount) = ch1.GetAverageAndCount(scores);

这是将元组的部分解构为新变量averagestudentCount。然而,我不想再看一下元组。我想做的是展示如何在任何类型上实现解构声明。为此,我们需要确保该类型具有解构方法。我们将修改现有的Student类以添加解构方法。

如何做...

  1. 如果您之前创建了Student类,您的代码中应该有类似于以下内容:
        public class Student
        {
          public string Name { get; set; }
          public string LastName { get; set; }
          public List<int> CourseCodes { get; set; }
        }

  1. 要创建一个析构函数,需要在Student类中添加一个Deconstruct方法。您会注意到这是一个void方法,它带有两个out参数(在这种情况下)。然后我们只需将NameLastName的值分配给out参数。

如果我们想在Student类中解构更多的值,我们将传入更多的out参数,每个值都要解构一个参数。

        public void Deconstruct(out string name, out string lastName)
        {
          name = Name;
          lastName = LastName;
        }

  1. 您修改后的Student类现在应该如下所示:
        public class Student
        {
          public string Name { get; set; }
          public string LastName { get; set; }
          public List<int> CourseCodes { get; set; }

          public void Deconstruct(out string name, out string lastName)
          {
            name = Name;
            lastName = LastName;
          }
        }

  1. 现在可以像使用元组一样使用我们的Student类了:
        Student student = new Student();
        student.Name = "Dirk";
        student.LastName = "Strauss";

        var (FirstName, Surname) = student;
        WriteLine($"The student name is {FirstName} {Surname}");

  1. 运行控制台应用程序将显示从Student类返回的解构值。

  1. 析构函数同样可以轻松地用于扩展方法中。这是扩展现有类型以包括析构声明的一种不错的方式。要实现这一点,我们需要从Student类中删除析构函数。您现在可以将其注释掉,但本质上这就是我们要做的:
        public class Student
        {
          public string Name { get; set; }
          public string LastName { get; set; }
          public List<int> CourseCodes { get; set; }
        }

  1. Student类现在不包含析构函数。转到扩展方法类并添加以下扩展方法:
        public static void Deconstruct(this Student student, 
                 out string firstItem, out string secondItem)
        {
          firstItem = student.Name;
          secondItem = student.LastName;
        }

  1. 扩展方法仅对Student类型起作用。它遵循了先前在Student类本身中创建的析构函数的基本实现。再次运行控制台应用程序,您将看到与以前相同的结果。唯一的区别是现在代码使用扩展方法来解构Student类中的值。

工作原理...

在代码示例中,我们将学生名和姓氏设置为特定值。这只是为了说明解构的使用。更可能的情况是将学生编号传递给Student类(可能是在构造函数中),如下所示:

Student student = new Student(studentNumber);

Student类中的实现将使用通过构造函数传递的学生编号进行数据库查找。然后将返回学生详细信息。Student类的更可能的实现可能如下所示:

public class Student
{
  public Student(string studentNumber)
  {
    (Name, LastName) = GetStudentDetails(studentNumber);
  }
  public string Name { get; private set; }
  public string LastName { get; private set; }
  public List<int> CourseCodes { get; private set; }

  public void Deconstruct(out string name, out string lastName)
  {
    name = Name;
    lastName = LastName;
  }

  private (string name, string surname) GetStudentDetails(string studentNumber)
  {
    var detail = (n: "Dirk", s: "Strauss");
    // Do something with student number to return the student details
    return detail;
  }
}

您会注意到GetStudentDetails()方法只是一个虚拟实现。这是数据库查找将开始并且值将从这里返回的地方。现在调用Student类的代码更有意义。我们调用Student类,传递给它一个学生编号,并对其进行解构以找到学生的名字和姓氏。

Student student = new Student("S20323742");
var (FirstName, Surname) = student;
WriteLine($"The student name is {FirstName} {Surname}");

本地函数

一开始使用本地函数可能会有点奇怪。实际上,在大多数函数式语言中经常使用它们。C# 7.0 现在允许我们做同样的事情。那么什么是本地函数呢?嗯,把它想象成一个特定方法的辅助方法。这个辅助方法只有在从特定方法中使用时才真正有意义,并且对于应用程序中的其他方法来说并不有用。因此,在现有方法内部使用它是有意义的。有些人可能认为扩展方法可能同样适用,但扩展方法实际上应该用于扩展许多其他方法的功能。本地函数的用处将在以下代码示例中变得明显。

准备工作

您不需要特别准备或预先设置任何内容来使用本地函数。为了说明本地函数的使用,我将创建一个方法,该方法在从总楼层面积中减去公共区域空间后计算建筑的楼层面积。

如何操作...

  1. 创建一个名为GetShopfloorSpace()的方法,它接受三个参数:公共区域空间,建筑宽度和建筑长度。
        public Building GetShopfloorSpace(int floorCommonArea,
                         int buildingWidth, int buildingLength)
        {

        }

  1. 我们正在返回一个Building类型,因此创建一个名为Building的类,它有一个名为TotalShopFloorSpace的属性。
        public class Building
        { 
          public int TotalShopFloorSpace { get; set; } 
        }

  1. 我们的本地函数将简单地获取建筑物的宽度长度来计算总楼层面积,然后从中减去公共区域,以获得商店可用的楼层空间。本地函数将如下所示:
        int CalculateShopFloorSpace(int common, int width, int length)
        {
          return (width * length) - common;
        }

  1. 这就是有趣的地方。在GetShopfloorSpace()方法内添加本地函数,并在以下代码示例中添加其余代码:
        public Building GetShopfloorSpace(int floorCommonArea,
                         int buildingWidth, int buildingLength)
        {
          Building building = new Building();

          building.TotalShopFloorSpace = CalculateShopFloorSpace(
                   floorCommonArea, buildingWidth, buildingLength);

          int CalculateShopFloorSpace(int common, int width, int length)
          {
            return (width * length) - common;
          }

          return building;
        }

  1. 在调用代码中,在static void Main方法内,调用方法如下:
        Chapter1 ch1 = new Chapter1();
        Building bldng = ch1.GetShopfloorSpace(200, 35, 100);
        WriteLine($"The total space for shops is 
                  {bldng.TotalShopFloorSpace} square meters");

  1. 运行控制台应用程序并查看输出如下显示:

它是如何工作的...

本地函数的美妙之处在于您可以从方法的任何地方调用它们。为了说明这一点,在GetShopfloorSpace()方法的return语句之前添加以下代码行。这实质上覆盖了我们最初传递给方法的任何内容。

building.TotalShopFloorSpace = CalculateShopFloorSpace(10, 9, 17);

修改后的方法现在看起来是这样的:

public Building GetShopfloorSpace(int floorCommonArea, int buildingWidth, int buildingLength)
{
  Building building = new Building();

  building.TotalShopFloorSpace = CalculateShopFloorSpace(
           floorCommonArea, buildingWidth, buildingLength);

  int CalculateShopFloorSpace(int common, int width, int length)
  {
    return (width * length) - common;
  }

  building.TotalShopFloorSpace = CalculateShopFloorSpace(10, 9, 17);

  return building;
}

再次运行控制台应用程序。这次您将看到值完全不同。对本地函数的第二次调用覆盖了第一次调用,并说明本地函数可以在包含它的方法中随时调用。

我可以想到一些以前可能可以使用这个的情况。我不认为我会经常使用它。但是这确实是 C#语言的一个非常好的补充,并且对开发人员可用。

文字的改进

这是 C#语言的另一个小改进,但我相信开发人员经常会使用它。我年轻时的第一份工作是在一家物流公司工作。这些人过去常常向大众供应零部件,而最关键的零部件是通过空运从德国或其他地方运来的。我永远不会忘记物流人员在随意交谈中提到的 9 位和 12 位的运输编号。我想知道他们是如何在一年中记住成百上千个不同的运输编号的。听了一会儿后,我注意到他们在每三个数字后稍作停顿。即使只是看着 12 位数 395024102833 也是一种视觉负担。想象一天要做这样几次,包括记住下一批货物的快速移动者(我甚至不想谈论印刷的货物清单,那简直是一场噩梦)。因此,更容易将数字视为 395-024-102-833,这样更容易发现模式。这基本上正是 C# 7.0 现在允许开发人员使用文字的方式。

准备工作

数字文字有时可能很难阅读。这就是为什么 C# 7.0 引入了下划线(_)作为数字文字中的数字分隔符。C# 7.0 还引入了二进制文字,允许您直接指定位模式,而无需知道十六进制。

如何做...

  1. 将以下代码添加到您的项目中。很明显,newNum文字更容易阅读,特别是如果您以三个一组阅读它。
        var oldNum = 342057239127493;
        var newNum = 342_057_239_127_493;
        WriteLine($"oldNum = {oldNum} and newNum = {newNum}");

  1. 如果运行控制台应用程序,您将看到两个数字文字的值完全相同:

  1. 对于二进制文字也是如此。您现在可以将它们表示如下:
        var binLit = 0b1010_1100_0011_0010_0001_0000;

它是如何工作的...

这只是文字的语法糖。我相信背后还有更多的东西,但是在您的代码中实现这一点确实非常简单。

引用返回和本地变量

在 C#中通过引用传递对象并不新鲜。这是使用ref关键字完成的。然而,在 C# 7.0 中,您现在可以通过引用返回对象,并将这些对象存储在本地变量中。

准备工作

重要的是要理解ref关键字的概念。当你传递一个ref参数时,你是在处理变量本身,而不仅仅是变量的值。这意味着,如果值被改变,原始的内存位置会被更新,而不仅仅是参数的副本。这在下面的例子中变得更清楚。

如何做...

  1. Chapter1类中,创建一个名为GetLargest()的新方法。该方法并不特别。它只是获取两个值中的最大值并将其返回给调用代码。
        public int GetLargest(int valueA, int valueB)
        {
          if (valueA > valueB)
            return valueA;
          else
            return valueB;
        }

  1. 创建一个同名的第二个方法。只是这一次,添加ref关键字。
        public ref int GetLargest(ref int valueA, ref int valueB)
        {
          if (valueA > valueB)
            return ref valueA;
          else
            return ref valueB;
        }

  1. static void Main方法中,创建一个Chapter1类的实例并调用GetLargest()方法。增加变量val并将变量值写入控制台窗口。
        int a = 10;
        int b = 20;
        Chapter1 ch1 = new Chapter1();
        int val = ch1.GetLargest(a, b);
        val += 25;

        WriteLine($"val = {val} a = {a} b = {b} ");

  1. 然后,在前面的调用代码之后写入以下代码,但调用ref ch1.GetLargest()方法。增加refVal变量并将变量值写入控制台窗口。
        ref int refVal = ref ch1.GetLargest(ref a, ref b);
        refVal += 25;

        WriteLine($"refVal = {refVal} a = {a} b = {b} ");

  1. 运行控制台应用程序并考虑显示的输出。

工作原理...

在控制台窗口中,你会看到两个非常不同的结果。简单地说,在第一行中,变量a是变量a,变量b是变量b,变量val是变量val

在第二行中,变量a是变量a,变量b是变量b,变量refVal是变量b。这就是ref关键字的全部关键所在。在第一个GetLargest()方法中,我们将最大值返回到变量val中。这个值是 20。变量val和变量b之间没有关系,因为它们在内存中分配了不同的空间。

在第二个GetLargest()方法中,我们将最大的变量本身(即b)返回到变量refVal中。因此,变量refVal成为变量b的别名,因为它们都指向内存中分配的相同空间。为了更清楚地说明这一点,让我们看一下变量的内存地址。

从项目菜单中,转到当前项目的属性。在生成选项卡中,选中允许不安全代码的选项并保存属性。

将以下代码添加到你的控制台应用程序中:

unsafe
{
  IntPtr a_var_memoryAddress = (IntPtr)(&a);
  IntPtr b_var_memoryAddress = (IntPtr)(&b);
  IntPtr val_var_memoryAddress = (IntPtr)(&val);

  fixed (int* refVal_var = &refVal)
  {
    IntPtr refVal_var_memoryAddress = (IntPtr)(refVal_var);
    WriteLine($"The memory address of a is {a_var_memoryAddress}");
    WriteLine($"The memory address of b is {b_var_memoryAddress}");
    WriteLine($"The memory address of val is {val_var_memoryAddress}");
    WriteLine($"The memory address of refVal is
              {refVal_var_memoryAddress}");
  }
}

这段代码与ref返回和本地变量的配方没有真正关系,所以我甚至不会详细介绍它。如果你想了解更多关于 C#中指针的知识,请从 MSDN 上的指针类型(C#编程指南)文章开始:msdn.microsoft.com/en-us/library/y31yhkeb.aspx

运行控制台应用程序并查看列出的内存地址:

你会立刻注意到变量b和变量refVal具有相同的内存地址11531252,而变量b和变量val具有不同的内存地址。

那么现在是百万美元的问题:C# 7.0 中的这个特性有什么用?简单地说,它可以提高性能。许多开发人员提到,对于游戏程序员来说,这将非常有用,他们现在可以传递这些别名来引用大型数据结构。这意味着他们不必复制大型数组(例如)以便处理它。使用ref,他们可以创建一个指向数组原始内存位置的别名,并直接读取或修改它。以这种方式思考,突然之间这个 C# 7.0 特性的用处就显而易见了。

我会经常使用它吗?我真的不知道。也许不经常,但是,就像本地函数一样,C# 7.0 的这个特性确实是开发人员工具包的一个很好的补充。当你想要摆脱在代码中传递大型结构时,它解决了一些非常棘手的问题。

广义异步返回类型

如果您使用 async/await(如果没有,请查看一下),那么 C# 7.0 的以下特性将非常方便。以前唯一支持的返回类型是Task<T>Taskvoid。即使是void也只用于事件处理程序,比如按钮点击。然而,挑战在于,在等待时分配了Task<T>,而async操作的结果在等待时是可用的。但是,这到底意味着什么呢?考虑一个返回Task<T>async方法:该值的生存时间为n秒。如果在生存时间内调用async方法,为什么要费力分配另一个Task<T>对象呢?这就是ValueTask<T>发挥作用的地方;它将允许定义其他类型,以便您可以从async方法中返回它们。因此,这减少了Task<T>的分配,从而带来了性能上的提升。

准备就绪

首先创建一个新的 WinForms 应用程序,并执行以下步骤:

  1. 在 Windows 表单中添加一个按钮、标签、定时器和文本框。

  1. 我们需要从 NuGet 添加System.Threading.Tasks.Extensions包以实现ValueTask<T>结构。如果您完成了元组的使用,这个过程对您来说应该很熟悉。选择 winform 项目,然后点击安装按钮。

请注意,我在撰写本书时使用的是 Visual Studio 2017 RC。在最终版本中,您可能不需要从 NuGet 添加System.Threading.Tasks.Extensions

  1. 将显示确认屏幕以允许您审查即将进行的更改。只需点击确定。接受许可协议。还要确保已将此using语句添加到您的项目中。
        using System.Threading.Tasks;

现在我们准备好编写我们的代码了。Windows 应用程序将在生存时间到期后调用一个async Task<T>方法。一旦这样做,该方法将读取一个值并将其缓存。这个缓存值将在 10 秒内有效(即生存时间)。如果在生存时间内运行该方法,则将使用并返回缓存值到表单。如果生存时间已过,则重复该过程并调用Task<T>方法。当您审查以下代码示例时,实现将变得更加清晰。

如何做...

  1. 首先在您的表单中添加以下变量。
        double timerTtl = 10.0D;
        private DateTime timeToLive;
        private int cacheValue;

  1. 在窗体加载事件中,使用计时器文本设置标签。

严格来说,这只是一些花里胡哨的东西。当涉及到说明一般化的异步返回类型时,这并不是真正必要的,但它有助于我们理解和理解这个概念。

        private void Form1_Load(object sender, EventArgs e)
        {
          lblTimer.Text = $"Timer TTL {timerTtl} sec (Stopped)"; 
        }

  1. 在设计器上将定时器间隔设置为 1000 毫秒,并将以下代码添加到timer1_Tick事件中。
        private void timer1_Tick(object sender, EventArgs e)
        {
          if (timerTtl == 0)
          {
            timerTtl = 5;
          }
          else
          {
            timerTtl -= 1; 
          }
          lblTimer.Text = $"Timer TTL {timerTtl} sec (Running)";
        }

  1. 现在创建一个模拟某种较长运行任务的方法。延迟一秒钟。使用Random关键字生成一个随机数,并将其赋值给cacheValue变量。设置生存时间,启动定时器,并将缓存值返回给调用代码。
        public async Task<int> GetValue()
        {
          await Task.Delay(1000);

          Random r = new Random();
          cacheValue = r.Next();
          timeToLive = DateTime.Now.AddSeconds(timerTtl);
          timer1.Start();
          return cacheValue;
        }

  1. 在调用代码中,检查当前缓存值的生存时间是否仍然有效。如果生存时间已过期,则运行分配并返回Task<T>以获取和设置缓存值的代码。如果生存时间仍然有效,则只返回缓存的整数值。

您会注意到我传递了一个布尔out变量,以指示已读取或设置了缓存值。

        public ValueTask<int> LoadReadCache(out bool blnCached)
        {
          if (timeToLive < DateTime.Now)
          {
            blnCached = false;
            return new ValueTask<int>(GetValue());
          }
          else
          {
            blnCached = true;
            return new ValueTask<int>(cacheValue);
          } 
        }

  1. 按钮点击的代码使用out变量isCachedValue,并相应地设置文本框中的文本。
        private async void btnTestAsync_Click(object sender, EventArgs e)
        {
          int iVal = await LoadReadCache(out bool isCachedValue);
          if (isCachedValue)
            txtOutput.Text = $"Cached value {iVal} read";
          else
            txtOutput.Text = $"New value {iVal} read";
        }

  1. 当您完成添加所有代码后,运行您的应用程序并点击测试异步按钮。这将从GetValue()方法中读取一个新值,将其缓存,并开始生存时间倒计时。

  1. 如果在生存时间到期之前再次点击按钮,则返回缓存值。

  1. 当生存时间到期时,单击“测试异步”按钮将再次调用GetValue()方法,进程重复。

它是如何工作的...

ValueTask<T>是 C# 7.0 的一个非常好的补充。然而,微软建议在对方法进行额外优化时对Task<T>ValueTask<T>的性能进行基准测试。然而,一个简单的优化就是简单地用ValueTask<T>替换Task<T>的实例。

访问器、构造函数和终结器的表达式主体

表达式主体成员在 C#开发者社区中非常受欢迎,以至于微软已经扩展了可以实现为表达式的允许成员。您现在可以在以下情况下使用此功能:

  • 构造函数

  • 终结器(在需要释放非托管代码时使用)

  • 属性和索引器上的getset访问器

准备工作

使用这个配方不需要特别准备什么。以下代码将使用旧与新的方法来演示每个方法的差异和实现。

如何做...

  1. 考虑类SomeClass。它包含一个构造函数,终结器和一个属性。
        public class SomeClass
        {
          private int _initialValue;

          // Property
          public int InitialValue
          {
            get
            {
              return _initialValue;
            }

            set
            {
              _initialValue = value;
            }
          }

          // Constructor
          public SomeClass(int initialValue)
          {
            InitialValue = initialValue;
          }

          // Finalizer
          ~SomeClass()
          {
            WriteLine("Release unmanaged code");
          }
        }

  1. 使用表达式主体成员,类SomeClass可以简化,并且代码行数减少。
        public class SomeClass
        {
          private int _initialValue;

          public int InitialValue
          {
            get => _initialValue;
            set => _initialValue = value;
          }

          public SomeClass(int initialValue) => 
                 InitialValue = initialValue;

          ~SomeClass() => WriteLine("Release unmanaged code");
        }

它是如何工作的...

如果您之前在 C# 6.0 中使用过表达式主体成员,您肯定会很高兴使用扩展功能。就我个人而言,我真的很高兴构造函数现在可以实现为一个表达式。

抛出异常

传统上,throw在 C#中一直是一个语句。正如我们所知,因为它是一个语句而不是一个表达式,我们不能在某些地方使用它。由于表达式主体成员,C# 7.0 引入了throw表达式。抛出异常的方式没有任何区别,只是可以从哪里抛出它们。

准备工作

抛出异常并不是什么新鲜事。自从写代码以来,您一直在这样做。我承认throw表达式是 C#中一个非常受欢迎的补充,这都归功于表达式主体成员。

如何做...

  1. 为了说明throw表达式的使用,创建一个名为GetNameLength()的方法在Chapter1类中。它只是检查名称的长度是否不为零。如果是,那么该方法将在表达式中立即抛出异常。
        public int GetNameLength(string firstName, string lastName)
        {
          return (firstName.Length + lastName.Length) > 0 ? 
            firstName.Length + lastName.Length : throw new 
            Exception("First name and last name is empty");
        }

  1. 要看到throw表达式的实际效果,请创建Chapter1类的实例并调用GetNameLength()方法。将两个空字符串作为参数传递。
        try
        {
          Chapter1 ch1 = new Chapter1();
          int nameLength = ch1.GetNameLength("", "");
        }
        catch (Exception ex)
        {
          WriteLine(ex.Message);
        }

  1. 运行控制台应用程序将返回异常消息作为输出。

它是如何工作的...

能够使用throw表达式使您的代码更容易编写和阅读。C# 7.0 中的新功能建立在 C# 6.0 奠定的出色基础之上。

第二章:类和泛型

类是软件开发的构建模块,对于构建良好的代码至关重要。在本章中,我们将看看类和泛型,以及为什么我们需要使用它们。我们将涵盖的内容如下:

  • 创建和实现抽象类

  • 创建和实现接口

  • 创建和使用泛型类或方法

  • 创建和使用泛型接口

介绍

如你所知,类只是相关方法和属性的容器,用于描述软件中的对象。对象是特定类的实例,并且有时模拟现实世界的事物。当想到汽车时,你可能会创建一个包含所有车辆共有属性(属性)的车辆类,比如自动或手动变速器,轮子数量(并非所有车辆都只有四个轮子),或燃料类型。

当我们创建一个车辆类的实例时,我们可以创建一个汽车对象、一个 SUV 对象等等。这就是类的力量所在,它可以描述我们周围的世界,并将其转化为编译器可以理解的编程语言。

创建和实现抽象类

许多开发人员听说过抽象类,但它们的实现是一个谜。作为开发人员,你如何识别抽象类并决定何时使用它?实际上,定义是非常简单的。一旦你理解了抽象类的基本定义,何时以及为什么使用它就变得显而易见。

想象一下,你正在开发一个管理猫收容所动物的应用程序。猫收容所康复狮子、老虎、美洲豹、豹子、猎豹、美洲狮,甚至家猫。描述所有这些动物的共同名词是“猫”。因此,你可以安全地假设所有这些动物的抽象是一只猫,因此,这个词标识了我们的抽象类。然后你会创建一个名为Cat的抽象类。

然而,你需要记住,你永远不会创建抽象类Cat的实例。所有继承自抽象类的类也共享一些功能。这意味着你将创建一个继承自抽象类CatLion类和Tiger类。换句话说,继承的类是一种猫。这两个类共享Sleep()Eat()Hunt()和其他各种方法的功能。通过这种方式,我们可以确保继承的类都包含这些共同的功能。

准备工作

让我们继续创建我们的猫的抽象类。然后我们将使用它来继承并创建其他对象来定义不同类型的猫。

操作步骤

  1. 在 Visual Studio 中创建一个新的控制台应用程序,并将其命名为ClassesAndGenerics

  2. 添加一个名为Cat的抽象类。为此,在类中添加abstract关键字。我们现在准备描述Cat抽象类:

        public abstract class Cat
        {

        }

abstract关键字告诉我们,它所应用的对象没有实现。当用于类声明时,它基本上告诉编译器该类将被用作基类。这意味着不能创建该类的实例。抽象类的实现方式是由继承自基类的派生类实现的。

  1. 你的控制台应用程序代码现在应该如下所示:
        class Program
        {
          static void Main(string[] args)
          {
          }
        }

        public abstract class Cat
        {

        }

  1. 在抽象类中添加三个方法,分别为Eat()Hunt()Sleep()。您会注意到这些方法没有包含具体的实现(花括号)。这是因为它们被定义为抽象方法。与抽象类一样,抽象类中包含的抽象方法没有具体的实现。这三个方法基本上描述了所有猫共有的功能。所有的猫都必须吃饭、狩猎和睡觉。因此,为了确保所有继承自Cat抽象类的类都包含这些功能,它被添加到了抽象类中。这些方法然后在派生类中实现,我们将在接下来的步骤中看到:
        public abstract class Cat 
        { 
          public abstract void Eat(); 
          public abstract void Hunt(); 
          public abstract void Sleep(); 
        }

  1. 我们想要定义两种类型的猫。我们想要定义的第一种猫是狮子。为此,我们创建一个Lion类:
        public class Lion 
        { 

        }

  1. 此时,Lion类只是一个普通类,不包含在Cat抽象类中定义的任何共有功能。要继承自Cat抽象类,我们需要在Lion类名后面添加: Cat。冒号表示Lion类继承自Cat抽象类。因此,Lion类是Cat抽象类的派生类:
        public class Lion : Cat 
        { 

        }

一旦指定Lion类继承自Cat类,Visual Studio 将显示错误。这是预期的,因为我们已经告诉编译器,Lion类需要继承Cat抽象类的所有特性,但我们实际上并没有将这些特性添加到Lion类中。派生类被认为是重写了抽象类中的方法,并且需要使用override关键字来明确地编写。

  1. 如果您将鼠标悬停在Lion类下面的红色波浪线上,Visual Studio 将通过灯泡功能提供错误的解释。正如您所看到的,Visual Studio 告诉您,虽然您已经定义了该类继承自抽象类,但您并没有实现Cat类的任何抽象成员:

因此,您可以看到使用抽象类是在系统中强制执行特定功能的一种绝妙方式。如果您在抽象类中定义了抽象成员,那么继承自该抽象类的派生类必须实现这些成员;否则,您的代码将无法编译。这可以用来强制执行公司采用的标准和实践,或者简单地允许其他开发人员在使用您的基类为其派生类实现某些最佳实践。随着 Visual Studio 2015 中代码分析器的出现,强制执行某些最佳代码实践的做法变得更加容易。

    1. 要实现 Visual Studio 警告我们的这些成员,将鼠标光标放在Lion类名上,然后按下Ctrl + .(句号)。您也可以点击灯泡弹出窗口中的显示潜在修复链接。Visual Studio 会给出一个小提示,显示它将对您的代码进行的更改。您可以通过点击预览更改链接来预览这些更改,也可以通过点击文档、项目或解决方案中的适当链接来修复所有出现的情况:

在 Visual Studio 添加了建议窗口中显示的更改之后,您的Lion类将是正确的,并且看起来像以下步骤中的代码清单。

  1. 您会注意到 Visual Studio 自动在每个重写的方法中添加了NotImplementedException异常的代码行 throw new NotImplementedException();
        public class Lion : Cat 
        { 
          public override void Eat() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Hunt() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Sleep() 
          { 
            throw new NotImplementedException(); 
          } 
        }

这是在覆盖基类中的方法时 Visual Studio 的默认行为。基本上,如果您必须在覆盖的方法中实例化Lion类而不写任何实现,将生成运行时异常。从我们的抽象类继承的想法是扩展它并实现共同功能。这就是我们需要实现该功能的地方,也是抽象类中没有实现的原因。抽象类只告诉我们需要实现以下方法。派生类执行实际的实现。

  1. 继续为Lion类的覆盖方法添加一些实现。首先,在您的类文件顶部添加using static语句以使用Console.WriteLine方法:
        using static System.Console;

  1. 然后,按照以下方式添加方法的实现:
        public override void Eat() 
        { 
          WriteLine($"The {LionColor} lion eats."); 
        } 

        public override void Hunt() 
        { 
          WriteLine($"The {LionColor} lion hunts."); 
        } 

        public override void Sleep() 
        { 
          WriteLine($"The {LionColor} lion sleeps."); 
        }

  1. 接下来,我们将创建另一个名为Tiger的类,它也派生自抽象类Cat。按照步骤 7 到步骤 10 创建Tiger类并继承Cat抽象类:
        public class Tiger : Cat 
        { 
          public override void Eat() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Hunt() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Sleep() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 为我们的Tiger类添加相同的实现如下:
        public override void Eat() 
        { 
          WriteLine($"The {TigerColor} tiger eats."); 
        } 

        public override void Hunt() 
        { 
          WriteLine($"The {TigerColor} tiger hunts."); 
        } 

        public override void Sleep() 
        { 
          WriteLine($"The {TigerColor} tiger sleeps."); 
        }

  1. 对于我们的Lion类,添加一个名为ColorSpectrum的枚举器和一个名为LionColor的属性。在这里,LionTiger类的实现将有所不同。虽然它们都必须实现抽象类中指定的共同功能,即Eat()Hunt()Sleep(),但只有狮子可以在其可用颜色范围内拥有棕色或白色的颜色:
        public enum ColorSpectrum { Brown, White } 
        public string LionColor { get; set; }

  1. 接下来,在我们的Lion类中添加Lion()构造函数。这将允许我们为猫保护区的狮子指定颜色。构造函数还以ColorSpectrum枚举器类型的变量作为参数:
        public Lion(ColorSpectrum color) 
        { 
          LionColor = color.ToString(); 
        }

  1. 与此类似,但颜色相当不同,Tiger类只能有一个ColorSpectrum枚举,定义老虎为橙色、白色、金色、蓝色(是的,您实际上可以得到一只蓝色老虎)或黑色。在Tiger类中添加ColorSpectrum枚举器以及一个名为TigerColor的属性:
       public enum ColorSpectrum { Orange, White, Gold, Blue,  Black } 
       public string TigerColor { get; set; }

  1. 最后,我们将为我们的Tiger类创建一个Tiger()构造函数,以将猫保护区中老虎的颜色设置为老虎所在的有效颜色。通过这样做,我们将特定于老虎和狮子的某些功能分离到各自的类中,而所有共同功能都包含在抽象类Cat中:
        public Tiger(ColorSpectrum color) 
        { 
          TigerColor = color.ToString(); 
        }

  1. 现在,我们需要从控制台应用程序实例化LionTiger类。您将看到我们从构造函数中设置了相应猫的颜色:
        Lion lion = new Lion(Lion.ColorSpectrum.White); 
        lion.Hunt(); 
        lion.Eat(); 
        lion.Sleep(); 

        Tiger tiger = new Tiger(Tiger.ColorSpectrum.Blue); 
        tiger.Hunt(); 
        tiger.Eat(); 
        tiger.Sleep(); 

        ReadLine();

  1. 当您运行控制台应用程序时,您会看到方法按顺序调用:

它是如何工作的...

虽然前面举的例子相当简单,但理论是正确的。抽象类跨所有猫和组的集体功能,以便它可以在每个派生类内共享。抽象类中不存在实现;它只定义了需要发生的事情。将抽象类视为从抽象类继承的类的一种蓝图。

虽然实现的内容由您决定,但抽象类要求您添加它定义的抽象方法。从现在开始,您可以为应用程序中类似的类创建一个坚实的基础,这些类应该共享功能。这就是继承的目的。让我们回顾一下抽象类的特点:

  • 您不能使用new关键字实例化抽象类。

  • 您只能向抽象类添加抽象方法和访问器。

  • 您永远不能将抽象类修改为sealedsealed修饰符阻止继承,而抽象类要求继承。

  • 从您的抽象类派生的任何类都必须包括从抽象类继承的抽象方法的实现。

  • 因为抽象类中的抽象方法没有实现,它们也没有主体。

创建和实现接口

对于许多开发人员来说,接口同样令人困惑,它们的目的并不清楚。一旦你理解了定义接口的概念,接口实际上是非常容易掌握的。

接口就像动词一样。例如,如果我们必须创建两个分别从抽象类Cat派生的类LionTiger,接口将描述某种动作。狮子和老虎可以咆哮(但不能发出喉音)。然后我们可以创建一个名为IRoarable的接口。如果我们必须从抽象类Cat派生一个名为Cheetah的类,我们将无法使用IRoarable接口,因为猎豹会发出喉音。我们需要创建一个IPurrable接口。

准备工作

创建一个接口与创建一个抽象类非常相似。不同之处在于接口描述了类可以做什么,在Cheetah类的情况下,通过实现IPurrable

如何做...

  1. 如果你之前还没有这样做,在上一个步骤中创建一个名为Cat的抽象类:
        public abstract class Cat 
        { 
          public abstract void Eat(); 
          public abstract void Hunt(); 
          public abstract void Sleep(); 
        }

  1. 接下来,添加一个名为Cheetah的类,它继承自抽象类Cat
        public class Cheetah : Cat 
        { 

        }

  1. 一旦你从抽象类Cat继承,Visual Studio 将通过灯泡功能显示警告。由于你从抽象类Cat继承,你必须在派生类Cheetah中实现抽象类中的抽象成员:

  2. 这很容易通过在文档中键入Ctrl +.(句号)并修复所有出现的情况来解决。你也可以为项目或解决方案这样做。对于我们的目的,我们只选择灯泡建议底部的文档链接。Visual Studio 将自动在Cheetah类中添加在抽象类中定义的抽象方法的实现:

  3. 你会注意到 Visual Studio 只会添加你需要重写的方法,但如果你尝试使用这个类,它会抛出NotImplementedException。使用抽象类的原因是在派生类Cheetah中实现抽象类Cat中定义的功能。不这样做违反了使用抽象类的规则:

        public class Cheetah : Cat 
        { 
          public override void Eat() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Hunt() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Sleep() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 为了添加一些实现,修改你的Cheetah类如下。重写方法中的实现很简单,但这样验证了在重写方法中写一些实现的规则:
        public class Cheetah : Cat 
        { 
          public override void Eat() 
          { 
            WriteLine($"The cheetah eats."); 
          } 

          public override void Hunt() 
          { 
            WriteLine($"The cheetah hunts."); 
          } 

          public override void Sleep() 
          { 
            WriteLine($"The cheetah sleeps."); 
          } 
        }

你会注意到WriteLine方法是在不使用Console类的情况下使用的。这是因为我们使用了 C# 6.0 中引入的一个新特性,允许开发人员通过在类文件顶部添加using static System.Console;语句将静态类引入作用域。

  1. 创建一个名为IPurrable的接口,它将在Cheetah类中实现。接口的一个常见命名约定规定接口名应以大写I为前缀:
        interface IPurrable 
        { 

        }

  1. 接下来,我们将在接口中添加一个任何实现接口的类都必须实现的方法。你会注意到接口的SoftPurr方法根本没有实现。但它指定了我们需要为Cheetah类发出的喉音传递一个整数值:
        interface IPurrable 
        { 
          void SoftPurr(int decibel); 
        }

  1. 下一步是在Cheetah类中实现IPurrable接口。为此,我们需要在Cat抽象类名后添加IPurrable接口名。如果Cheetah类没有继承自抽象类,那么接口名将直接跟在冒号后面:
        public class Cheetah : Cat, IPurrable 
        { 
          public override void Eat() 
          { 
            WriteLine($"The cheetah eats."); 
          } 

          public override void Hunt() 
          { 
            WriteLine($"The cheetah hunts."); 
          } 

          public override void Sleep() 
          { 
            WriteLine($"The cheetah sleeps."); 
          } 
        }

  1. 在指定Cheetah类实现IPurrable接口之后,Visual Studio 再次通过灯泡功能显示警告。它警告我们Cheetah类没有实现接口IPurrable中定义的SoftPurr方法:

  2. 与之前一样,我们可以让 Visual Studio 建议可能的修复方法,通过输入Ctrl + . (句号)。Visual Studio 建议接口可以被隐式或显式地实现:

  3. 知道何时使用隐式或显式实现也很容易。我们首先需要知道在何种情况下使用其中一种会更好。让我们首先通过选择灯泡建议中的第一个选项来隐式实现SoftPurr方法。您会看到这使用了在IPurrable接口中定义的SoftPurr方法,就好像它是Cheetah类的一部分一样:

        public class Cheetah : Cat, IPurrable 
        { 
          public void SoftPurr(int decibel) 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Eat() 
          { 
            WriteLine($"The cheetah eats."); 
          } 

          public override void Hunt() 
          { 
            WriteLine($"The cheetah hunts."); 
          } 

          public override void Sleep() 
          { 
            WriteLine($"The cheetah sleeps."); 
          } 
        }

  1. 如果我们看SoftPurr方法,它看起来像是Cheetah类中的一个普通方法。这没问题,除非我们的Cheetah类已经包含了一个名为SoftPurr的属性。继续为您的Cheetah类添加一个名为SoftPurr的属性:
        public class Cheetah : Cat, IPurrable 
        { 
          public int SoftPurr { get; set; } 

          public void SoftPurr(int decibel) 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Eat() 
          { 
            WriteLine($"The cheetah eats."); 
          } 

          public override void Hunt() 
          { 
            WriteLine($"The cheetah hunts."); 
          } 

          public override void Sleep() 
          { 
            WriteLine($"The cheetah sleeps."); 
          }         
        }

  1. Visual Studio 立即通过告诉我们Cheetah类已经包含了SoftPurr的定义来显示警告:

  2. 在这里,显式实现的使用变得明显。这指定了SoftPurr方法是在IPurrable接口中定义的实现的成员:

  3. 因此,选择第二个选项来显式实现接口将会将SoftPurr方法添加到您的Cheetah类中,如下所示:

        public class Cheetah : Cat, IPurrable 
        { 
          public int SoftPurr { get; set; } 

          void IPurrable.SoftPurr(int decibel) 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Eat() 
          { 
            WriteLine($"The cheetah eats."); 
          } 

          public override void Hunt() 
          { 
            WriteLine($"The cheetah hunts."); 
          } 

          public override void Sleep() 
          { 
            WriteLine($"The cheetah sleeps."); 
          }         
        }

编译器现在知道这是正在实现的接口,因此这是有效的代码。

  1. 为了本书的目的,让我们只使用隐式实现。让我们为SoftPurr方法编写一些实现,并使用新的nameof关键字(在 C# 6.0 中引入)以及插值字符串进行输出。同时,移除之前添加的SoftPurr属性:
        public void SoftPurr(int decibel) 
        { 
          WriteLine($"The {nameof(Cheetah)} purrs at {decibel} decibels."); 
        }

  1. 前往我们的控制台应用程序,我们可以调用我们的Cheetah类如下:
        Cheetah cheetah = new Cheetah(); 
        cheetah.Hunt(); 
        cheetah.Eat(); 
        cheetah.Sleep(); 
        cheetah.SoftPurr(60); 
        ReadLine();

  1. 运行应用程序将产生以下输出:

工作原理...

因此,您可能想知道抽象类和接口之间的区别是什么。基本上取决于您想要放置实现的位置。如果您需要在派生类之间共享功能,则抽象类是最适合您需求的选择。换句话说,我们有一些特定于所有猫(狮子、老虎和猎豹)的共同事物,例如狩猎、进食和睡觉。这时最好使用抽象类。

如果您的实现特定于一个类或多个类(但不是所有类),那么您最好的选择是使用接口。在这种情况下,IPurrable接口可以应用于多个类(例如,猎豹和家猫),但不能应用于所有猫(例如,狮子和老虎),因为并非所有猫都能发出咕噜声。

了解这种差异以及您需要放置实现的位置将有助于您决定是否需要使用抽象类还是接口。

创建和使用泛型类或方法

泛型是编写代码的一种非常有趣的方式。在设计时,您可以延迟指定代码中元素的数据类型,直到它们在代码中使用。这基本上意味着您的类或方法可以与任何数据类型一起使用。

准备工作

我们将首先编写一个泛型类,该类可以在其构造函数中接受任何数据类型作为参数并对其进行操作。

操作步骤...

  1. 声明一个泛型类实际上非常简单。我们所需要做的就是创建带有泛型类型参数<T>的类:
        public class PerformAction<T> 
        { 

        }

泛型类型参数基本上是特定类型的占位符,当实例化变量的类时需要定义该类型。这意味着泛型类PerformAction<T>永远不能在实例化类时不在尖括号内指定类型参数而直接使用。

  1. 接下来,创建一个泛型类型参数Tprivate变量。这将保存我们传递给泛型类的值:
        public class PerformAction<T> 
        { 
          private T _value; 
        }

  1. 现在我们需要为泛型类添加一个构造函数。构造函数将以T类型的值作为参数。私有变量_value将设置为传递给构造函数的参数:
        public class PerformAction<T> 
        { 
          private T _value; 

          public PerformAction(T value) 
          { 
            _value = value; 
          } 
        }

  1. 最后,为了完成我们的泛型类,创建一个名为IdentifyDataType()的 void 返回方法。这将告诉我们我们传递给泛型类的数据类型。我们可以使用GetType()找到变量的类型:
        public class PerformAction<T> 
        { 
          private T _value; 

          public PerformAction(T value) 
          { 
            _value = value; 
          } 

          public void IdentifyDataType() 
          { 
            WriteLine($"The data type of the supplied variable
                      is {_value.GetType()}"); 
          } 
        }

  1. 为了看到我们的泛型类真正的优势,实例化控制台应用程序中的泛型类,并在每个新实例化的尖括号内指定不同的数据类型参数:
        PerformAction<int> iAction = new PerformAction<int>(21); 
        iAction.IdentifyDataType(); 

        PerformAction<decimal> dAction = new 
                                 PerformAction<decimal>(21.55m); 
        dAction.IdentifyDataType(); 

        PerformAction<string> sAction = new 
                         PerformAction<string>("Hello Generics"); 
        sAction.IdentifyDataType();                         

        ReadLine();

  1. 运行控制台应用程序将输出您每次实例化泛型类时使用的给定数据类型:

我们使用完全相同的类,但让它使用三种非常不同的数据类型。这种灵活性是您代码中非常强大的一个特性。

C#的另一个特性是您可以约束实现的泛型类型:

  1. 我们可以通过告诉编译器只有实现了IDisposable接口的类型才能与泛型类一起使用来实现这一点。通过向其添加where T : IDisposable,更改您的泛型类。您的泛型类现在应该是这样的:
        public class PerformAction<T> where T : IDisposable 
        { 
          private T _value; 

          public PerformAction(T value) 
          { 
            _value = value; 
          } 

          public void IdentifyDataType() 
          { 
            WriteLine($"The data type of the supplied variable
                      is {_value.GetType()}"); 
          } 
        }

  1. 回到控制台应用程序,看一下泛型类的先前实例化:

Visual Studio 会告诉您,红色波浪线下划线的类型没有实现IDisposable,因此无法提供给PerformAction泛型类。

  1. 注释掉这些代码行,并将以下实例化添加到您的控制台应用程序中:
        DataSet dsData = new DataSet(); 
        PerformAction<DataSet> oAction = new 
                               PerformAction<DataSet>(dsData); 
        oAction.IdentifyDataType();

请注意,为了使其工作,您可能需要在代码文件中添加using System.Data;。这是必需的,这样您就可以声明一个DataSet

  1. 您可能知道,DataSet类型实现了IDisposable,因此它是可以传递给我们的泛型类的有效类型。继续运行控制台应用程序:

DataSet类型是有效的,泛型类按预期运行,识别传递给构造函数的参数的类型。

但是泛型方法呢?就像泛型类一样,泛型方法在设计时也不指定其类型。只有在调用方法时才知道。让我们来看看泛型方法的以下实现:

  1. 让我们继续创建一个名为MyHelperClass的新辅助类:
        public class MyHelperClass 
        { 
        }

  1. 在这个辅助类中,我们将创建一个名为InspectType的泛型方法。这个泛型方法有趣的地方在于它可以返回多种类型,因为返回类型也标记了泛型类型参数。您的泛型方法不一定要返回任何东西。它也可以声明为void
        public class MyHelperClass 
        { 
          public T InspectType<T>(T value)  
          { 

          } 
        }

  1. 为了说明这个泛型方法可以返回多种类型,我们将把传递给泛型方法的类型输出到控制台窗口,然后返回该类型并在控制台应用程序中显示它。您会注意到在返回时需要将返回类型强制转换为(T)
        public class MyHelperClass 
        { 
          public T InspectType<T>(T value)  
          { 
            WriteLine($"The data type of the supplied parameter
                      is {value.GetType()}"); 

            return (T)value; 
          } 
        }

  1. 在控制台应用程序中,继续创建一个名为MyEnum的枚举器。泛型方法也可以接受枚举器:
        public enum MyEnum { Value1, Value2, Value3 }

  1. 创建枚举器后,将以下代码添加到控制台应用程序。我们正在实例化和调用oHelper类,并向其传递不同的值:
        MyHelperClass oHelper = new MyHelperClass(); 
        var intExample = oHelper.InspectType(25); 
        WriteLine($"An example of this type is  {intExample}"); 

        var decExample = oHelper.InspectType(11.78m); 
        WriteLine($"An example of this type is  {decExample}"); 

        var strExample = oHelper.InspectType("Hello Generics"); 
        WriteLine($"An example of this type is  {strExample}"); 

        var enmExample = oHelper.InspectType(MyEnum.Value2); 
        WriteLine($"An example of this type is  {enmExample}"); 

        ReadLine();

  1. 如果运行控制台应用程序,您将看到泛型方法正确地识别了传递给它的参数的类型,然后将该类型返回给控制台应用程序中的调用代码:

泛型方法可以在多种情况下使用。然而,这只是对泛型类和方法的介绍。建议您进行进一步的研究,以了解如何适当地在代码中实现泛型。

它是如何工作的...

泛型的核心是能够重用单个类或方法。它允许开发人员在整个代码库中基本上不重复相似的代码。这与不要重复自己DRY)原则非常符合。这个设计原则规定特定的逻辑应该在代码中只表示一次。

例如,使用泛型类还允许开发人员在编译时创建类型安全的类。类型安全基本上意味着开发人员可以确保对象的类型,并且可以以特定的方式使用类,而不会遇到任何意外的行为。因此,编译器承担了类型安全的负担。

泛型还允许开发人员编写更少的代码,因为代码可以被重用,而且更少的代码也能更好地执行。

创建和使用通用接口

泛型接口的工作方式与泛型中的先前示例非常相似。假设我们想要在我们的代码中找到某些类的属性,但我们不能确定我们需要检查多少个类。泛型接口在这里会非常方便。

准备工作

我们需要检查几个类的属性。为了做到这一点,我们将创建一个通用接口,它将返回一个类的所有属性作为字符串列表。

如何做...

让我们看一下以下通用接口的实现:

  1. 继续创建一个名为IListClassProperties<T>的通用接口。该接口将定义一个需要使用的方法GetPropertyList(),它简单地使用 LINQ 查询返回一个List<string>对象:
        interface IListClassProperties<T> 
        { 
          List<string> GetPropertyList(); 
        }

  1. 接下来,创建一个名为InspectClass<T>的通用类。让这个通用类实现上一步创建的IListClassProperties<T>接口:
        public class InspectClass<T> : IListClassProperties<T> 
        { 

        }

  1. 通常情况下,Visual Studio 会突出显示InspectClass<T>通用类中未实现GetPropertyList()接口成员的情况:

  2. 为了显示任何潜在的修复,键入Ctrl + .(句号)并隐式实现接口:

  3. 这将在你的InspectClass<T>类中创建一个没有任何实现的GetPropertyList()方法。你将在稍后添加实现。如果你尝试在GetpropertyList()方法中没有添加任何实现的情况下运行你的代码,编译器将抛出NotImplementedException

        public class InspectClass<T> : IListClassProperties<T> 
        { 
          public List<string> GetPropertyList() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 接下来,在你的InspectClass<T>类中添加一个构造函数,它接受一个泛型类型参数,并将其设置为一个私有变量_classToInspect,你也需要创建这个变量。这是为了设置我们将用来实例化类的代码。我们将通过构造函数传递我们需要从中获取属性列表的对象,并且构造函数将设置私有变量_classToInspect,以便我们可以在我们的GetPropertyList()方法实现中使用它:
        public class InspectClass<T> : IListClassProperties<T> 
        { 
          T _classToInspect; 
          public InspectClass(T classToInspect) 
          { 
            _classToInspect = classToInspect; 
          } 

          public List<string> GetPropertyList() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 为了完成我们的类,我们需要向GetPropertyList()方法添加一些实现。在这里,LINQ 查询将被用来返回一个包含在构造函数中提供的类中的所有属性的List<string>对象:
        public List<string> GetPropertyList() 
        { 
          return _classToInspect.GetType()
                 .GetProperties().Select(p =>  p.Name).ToList(); 
        }

  1. 转到我们的控制台应用程序,继续创建一个名为Invoice的简单类。这是系统中可以使用的几个类之一,而Invoice类是较小的类之一。它通常只保存与你连接的数据存储的发票记录中特定记录相关的发票数据。我们需要找到这个类中的属性列表:
        public class Invoice 
        { 
          public int ID { get; set; } 
          public decimal TotalValue { get; set; } 
          public int LineNumber { get; set; } 
          public string StockItem { get; set; } 
          public decimal ItemPrice { get; set; } 
          public int Qty { get; set; } 
        }

  1. 现在我们可以使用实现IListClassProperties<T>泛型接口的InspectClass<T>泛型类。为此,我们将创建Invoice类的新实例。然后实例化InspectClass<T>类,将类型传递到尖括号中,并将oInvoice对象传递给构造函数。现在我们准备调用GetPropertyList()方法。结果返回到名为lstPropsList<string>对象。然后我们可以在列表上运行foreach,将每个property变量的值写入控制台窗口:
        Invoice oInvoice = new Invoice(); 
        InspectClass<Invoice> oClassInspector = new  
                          InspectClass<Invoice>(oInvoice); 
        List<string> lstProps = oClassInspector.GetPropertyList(); 

        foreach(string property in lstProps) 
        { 
          WriteLine(property); 
        } 
        ReadLine();

  1. 继续运行代码,查看检查Invoice类属性生成的输出!

如您所见,属性按照它们在Invoice类中的存在顺序列出。IListClassProperties<T>泛型接口和InspectClass<T>类不关心它们需要检查的类的类型。它们将接受任何类并运行代码,并产生结果。

然而,上述实现仍然存在轻微问题。让我们看看这个问题的一个变化:

  1. 考虑在控制台应用程序中的以下代码:
        InspectClass<int> oClassInspector = new InspectClass<int>(10); 
        List<string> lstProps = oClassInspector.GetPropertyList(); 
        foreach (string property in lstProps) 
        { 
          WriteLine(property); 
        } 
        ReadLine();

您可以看到,我们很容易地将整数值和类型传递给InspectClass<T>类,代码根本没有显示任何警告。实际上,如果您运行此代码,将不会返回任何内容,也不会输出到控制台窗口。我们需要在我们的泛型类和接口上实现约束。

  1. 在类的接口实现结束后,添加where T : class子句。现在代码需要看起来像这样:
        public class InspectClass<T> : IListClassProperties<T>
                                       where T : class 
        { 
          T _classToInspect; 
          public InspectClass(T classToInspect) 
          { 
            _classToInspect = classToInspect; 
          } 

          public List<string> GetPropertyList() 
          { 
            return _classToInspect.GetType().GetProperties()
                               .Select(p => p.Name).ToList(); 
          } 
        }

  1. 如果我们返回到我们的控制台应用程序代码,您会看到 Visual Studio 已经在传递给InspectClass<T>类的int类型下划线标记了:

这是因为我们对我们的泛型类和接口定义了一个约束。我们告诉编译器我们只接受引用类型。因此,这适用于任何类、接口数组、类型或委托。因此,我们的Invoice类将是一个有效的类型,约束不会适用于它。

我们还可以在类型参数约束中更加具体。这是因为我们可能不希望将参数限制为引用类型。例如,如果我们只想将泛型类和接口限制为只接受在我们当前系统中创建的类,我们可以实现T的参数需要从特定对象派生的约束。在这里,我们可以再次使用抽象类:

  1. 创建一个名为AcmeObject的抽象类,并指定从AcmeObject继承的所有类都实现一个名为ID的属性:
        public abstract class AcmeObject 
        { 
          public abstract int ID { get; set; } 
        }

  1. 现在我们可以确保我们在代码中创建的需要从中读取属性的对象是从AcmeObject派生的。要应用约束,修改泛型类,并在接口实现后放置where T : AcmeObject约束。您的代码现在应该看起来像这样:
        public class InspectClass<T> : IListClassProperties<T>
                                       where T : AcmeObject 
        { 
          T _classToInspect; 
          public InspectClass(T classToInspect) 
          { 
            _classToInspect = classToInspect; 
          } 

          public List<string> GetPropertyList() 
          { 
            return _classToInspect.GetType().GetProperties()
                             .Select(p =>  p.Name).ToList(); 
          } 
        }

  1. 在控制台应用程序中,修改Invoice类,使其继承自AcmeObject抽象类。根据抽象类中定义的实现ID属性:
        public class Invoice : AcmeObject 
        { 
          public override int ID { get; set; } 
          public decimal TotalValue { get; set; } 
          public int LineNumber { get; set; } 
          public string StockItem { get; set; } 
          public decimal ItemPrice { get; set; } 
          public int Qty { get; set; }             
        }

  1. 创建两个名为SalesOrderCreditNote的类。但这次,只让SalesOrder类继承自AcmeObject。保持CreditNote对象不变。这样我们可以清楚地看到约束如何应用:
        public class SalesOrder : AcmeObject 
        { 
          public override int ID { get; set; } 
          public decimal TotalValue { get; set; } 
          public int LineNumber { get; set; } 
          public string StockItem { get; set; } 
          public decimal ItemPrice { get; set; } 
          public int Qty { get; set; } 
        } 

        public class CreditNote 
        { 
          public int ID { get; set; } 
          public decimal TotalValue { get; set; } 
          public int LineNumber { get; set; } 
          public string StockItem { get; set; } 
          public decimal ItemPrice { get; set; } 
          public int Qty { get; set; } 
        }

  1. 创建获取InvoiceSalesOrder类的属性列表所需的代码。代码很简单,我们可以看到 Visual Studio 对这两个类都没有抱怨:
        Invoice oInvoice = new Invoice(); 
        InspectClass<Invoice> oInvClassInspector = new 
                              InspectClass<Invoice>(oInvoice); 
        List<string> invProps = oInvClassInspector.GetPropertyList(); 

        foreach (string property in invProps) 
        { 
          WriteLine(property); 
        } 
        ReadLine(); 
        SalesOrder oSalesOrder = new SalesOrder(); 
        InspectClass<SalesOrder> oSoClassInspector = new 
                     InspectClass<SalesOrder>(oSalesOrder); 
        List<string> soProps = oSoClassInspector.GetPropertyList(); 

        foreach (string property in soProps) 
        { 
          WriteLine(property); 
        } 
        ReadLine();

  1. 然而,如果我们试图对我们的CreditNote类做同样的事情,我们会发现 Visual Studio 会警告我们不能将CreditNote类传递给InspectClass<T>类,因为我们实现的约束只接受从我们的AcmeObject抽象类派生的对象。通过这样做,我们有效地控制了允许传递给我们的泛型类和接口的内容,通过约束的方式!

它是如何工作的...

说到泛型接口,我们已经看到我们可以通过实现泛型接口在泛型类上实现行为。使用泛型类和泛型接口的强大之处在前面已经很好地说明了。

话虽如此,我们确实认为知道何时使用约束也很重要,这样您就可以关闭泛型类,只接受您想要的特定类型。这确保了当有人意外地将整数传递给您的泛型类时,您不会受到任何意外。

最后,您可以使用的约束如下:

  • where T: struct: 类型参数必须是任何值类型

  • where T: class: 类型参数必须是任何引用类型

  • where T: new(): 类型参数需要有一个无参数的构造函数

  • where T: <base class name>: 类型参数必须从给定的基类派生

  • where T: <T must derive from object>: T类型参数必须从冒号后的对象派生

  • where T: <interface>: 类型参数必须实现指定的接口

第三章:C#中的面向对象编程

本章将向您介绍 C#和面向对象编程(OOP)的基础。在本章中,您将学习以下内容:

  • 在 C#中使用继承

  • 使用抽象

  • 利用封装

  • 实现多态

  • 单一职责原则

  • 开闭原则

  • 异常处理

介绍

在您作为软件创建者的职业生涯中,您会多次听到 OOP 这个术语。这种设计理念允许对象独立存在,并可以被代码的不同部分重复使用。这一切都是由我们所说的 OOP 的四大支柱所实现的:继承、封装、抽象和多态。

为了理解这一点,您需要开始思考执行特定任务的对象(基本上是实例化的类)。类需要遵循 SOLID 设计原则。这个原则在这里解释:

  • 单一职责原则(SRP)

  • 开闭原则

  • 里斯科夫替换原则(LSP)

  • 接口隔离原则

  • 依赖反转原则

让我们从解释 OOP 的四大支柱开始,然后我们将更详细地看一下 SOLID 原则。

在 C#中使用继承

在今天的世界中,继承通常与事物的结束联系在一起。然而,在 OOP 中,它与新事物的开始和改进联系在一起。当我们创建一个新类时,我们可以取一个已经存在的类,并在我们的新类上继承它。这意味着我们的新对象将具有继承类的所有特性,以及添加到新类的附加特性。这就是继承的根本。我们称从另一个类继承的类为派生类。

做好准备

为了说明继承的概念,我们将创建一些从另一个类继承的类,以形成新的、更具特色的对象。

如何做到...

  1. 创建一个新的控制台应用程序,并在其中添加一个名为SpaceShip的类。
        public class SpaceShip 
        { 

        }

  1. 我们的SpaceShip类将包含一些描述飞船基本情况的方法。继续将这些方法添加到您的SpaceShip类中:
        public class SpaceShip 
        { 
          public void ControlBridge() 
          { 

          } 
          public void MedicalBay(int patientCapacity) 
          { 

          } 
          public void EngineRoom(int warpDrives) 
          { 

          } 
          public void CrewQuarters(int crewCapacity) 
          { 

          } 
          public void TeleportationRoom() 
          { 

          } 
        }

因为SpaceShip类是所有其他星际飞船的一部分,它成为了每艘其他飞船的蓝图。

  1. 接下来,我们想创建一个Destroyer类。为了实现这一点,我们将创建一个Destroyer类,并在类名后使用冒号表示我们想要从另一个类(SpaceShip类)继承。因此,在创建Destroyer类时需要添加以下内容:
        public class Destroyer : SpaceShip 
        { 

        }

我们还可以说Destroyer类是从SpaceShip类派生的。因此,SpaceShip类是所有其他星际飞船的基类。

  1. 接下来,向Destroyer类添加一些仅适用于驱逐舰的方法。这些方法仅属于Destroyer类,而不属于SpaceShip类:
        public class Destroyer : SpaceShip 
        { 
          public void WarRoom() 
          { 

          } 
          public void Armory(int payloadCapacity) 
          { 

          } 

          public void WarSpecialists(int activeBattalions) 
          { 

          } 
        }

  1. 最后,创建一个名为Annihilator的第三个类。这是最强大的星际飞船,用于对抗行星。通过创建该类并标记为从Destroyer类派生的类,让Annihilator类继承Destroyer类:
        public class Annihilator : Destroyer 
        { 

        }

  1. 最后,向Annihilator类添加一些仅属于这种SpaceShip类的方法:
        public class Annihilator : Destroyer 
        { 
          public void TractorBeam() 
          { 

          } 

          public void PlanetDestructionCapability() 
          { 

          } 
        }

  1. 现在我们看到,当我们在控制台应用程序中创建SpaceShip类的新实例时,我们只能使用该类中定义的方法。这是因为SpaceShip类没有继承自其他类:

  1. 继续在控制台应用程序中创建SpaceShip类及其方法:
        SpaceShip transporter = new SpaceShip(); 
        transporter.ControlBridge(); 
        transporter.CrewQuarters(1500); 
        transporter.EngineRoom(2); 
        transporter.MedicalBay(350); 
        transporter.TeleportationRoom();

当我们实例化这个类的新实例时,您会看到这些是我们唯一可用的方法。

  1. 接下来,在Destroyer类中创建一个新实例。您会注意到Destroyer类包含的方法比我们在创建类时定义的要多。这是因为Destroyer类继承了SpaceShip类,因此继承了SpaceShip类的方法:

  1. 在控制台应用程序中创建Destroyer类及其所有方法:
        Destroyer warShip = new Destroyer(); 
        warShip.Armory(6); 
        warShip.ControlBridge(); 
        warShip.CrewQuarters(2200); 
        warShip.EngineRoom(4); 
        warShip.MedicalBay(800); 
        warShip.TeleportationRoom(); 
        warShip.WarRoom(); 
        warShip.WarSpecialists(1);

  1. 最后,创建Annihilator类的新实例。这个类包含了Destroyer类的所有方法,以及SpaceShip类的方法。这是因为Annihilator继承自Destroyer,而Destroyer又继承自SpaceShip

  1. 在控制台应用程序中创建Annihilator类及其所有方法:
        Annihilator planetClassDestroyer = new Annihilator(); 
        planetClassDestroyer.Armory(12); 
        planetClassDestroyer.ControlBridge(); 
        planetClassDestroyer.CrewQuarters(4500); 
        planetClassDestroyer.EngineRoom(7); 
        planetClassDestroyer.MedicalBay(3500); 
        planetClassDestroyer.PlanetDestructionCapability(); 
        planetClassDestroyer.TeleportationRoom(); 
        planetClassDestroyer.TractorBeam(); 
        planetClassDestroyer.WarRoom(); 
        planetClassDestroyer.WarSpecialists(3);

工作原理...

我们可以看到继承允许我们通过重用先前创建的另一个类中已经存在的功能来轻松扩展我们的类。但是需要注意的是,对SpaceShip类的任何更改都将被继承,一直到最顶层的派生类。

继承是 C#的一个非常强大的特性,它允许开发人员编写更少的代码,并重用工作和经过测试的方法。

使用抽象

通过抽象,我们从我们想要创建的对象中提取出所有派生对象必须具有的基本功能。简单来说,我们将共同功能抽象出来,放入一个单独的类中,用于为所有继承自它的类提供这些共享功能。

准备工作

为了解释抽象,我们将使用抽象类。想象一下,你正在处理需要通过训练逐渐晋升的实习太空宇航员。事实上,一旦你作为实习生学会了一项新技能,那项技能就会被学会,并且会一直保留在你身上,即使你学会了更高级的做事方式。你还必须在你创建的新对象中实现所有之前学到的技能。抽象类非常好地展示了这个概念。

如何做...

  1. 创建一个名为SpaceCadet的抽象类。这是在开始训练时可以获得的第一种宇航员类型。使用abstract关键字定义抽象类及其成员。需要注意的是,抽象类不能被实例化。成员代表SpaceCadet将拥有的技能,比如谈判和基本武器训练。
        public abstract class SpaceCadet 
        { 
          public abstract void ChartingStarMaps(); 
          public abstract void BasicCommunicationSkill(); 
          public abstract void BasicWeaponsTraining(); 
          public abstract void Negotiation(); 
        }

  1. 接下来,创建另一个名为SpacePrivate的抽象类。这个抽象类继承自SpaceCadet抽象类。基本上,我们要表达的是,当一个太空学员被训练成为太空士兵时,他们仍然会拥有作为太空学员学到的所有技能:
        public abstract class SpacePrivate : SpaceCadet 
        { 
          public abstract void AdvancedCommunicationSkill(); 
          public abstract void AdvancedWeaponsTraining(); 
          public abstract void Persuader(); 
        }

  1. 为了演示这一点,创建一个名为LabResearcher的类,并继承SpaceCadet抽象类。通过在新创建的类名后定义冒号和抽象类名,来继承抽象类。这告诉编译器LabResearcher类继承自SpaceCadet类:
        public class LabResearcher : SpaceCadet 
        { 

        }

因为我们继承了一个抽象类,编译器会在LabResearcher类名下划线,警告我们派生类没有实现SpaceCadet抽象类中的任何方法。

  1. 如果你将鼠标悬停在波浪线上,你会发现灯泡提示会告诉我们发现的问题:

  1. Visual Studio 在发现的问题上提供了一个很好的解决方案。通过输入Ctrl + . (控制键和句点),你可以让 Visual Studio 显示一些潜在的修复方法(在这种情况下,只有一个修复方法):

  1. 在 Visual Studio 添加了所需的方法之后,您会发现这些方法与SpaceCadet抽象类中定义的方法相同。因此,抽象类要求从抽象类继承的类实现抽象类中定义的方法。您还会注意到添加到LabResearcher类中的方法不包含任何实现,如果按原样使用,将会抛出异常:
        public class LabResearcher : SpaceCadet 
        { 
          public override void BasicCommunicationSkill() 
          { 
            thrownewNotImplementedException(); 
          } 

          publicoverridevoid BasicWeaponsTraining() 
          { 
            thrownewNotImplementedException(); 
          } 

          publicoverridevoid ChartingStarMaps() 
          { 
            thrownewNotImplementedException(); 
          } 

          publicoverridevoid Negotiation() 
          { 
            thrownewNotImplementedException(); 
          } 
        }

  1. 接下来,创建一个名为PlanetExplorer的类,并使该类继承自SpacePrivate抽象类。您会记得SpacePrivate抽象类继承自SpaceCadet抽象类:
        public class PlanetExplorer : SpacePrivate 
        { 

        }

  1. Visual Studio 将再次警告您,您的新类没有实现继承的抽象类的方法。然而,在这里,您会注意到灯泡提示通知您,您没有实现SpacePrivateSpaceCadet抽象类中的任何方法。这是因为SpacePrivate抽象类继承自SpaceCadet抽象类:

  1. 与以前一样,要解决识别出的问题,输入Ctrl + .(控制键和句点),让 Visual Studio 显示一些潜在的修复方法(在这种情况下,只有一个修复方法)。

  2. 在代码中添加修复后,您会发现PlanetExplorer类包含SpacePrivateSpaceCadet抽象类中的所有方法:

        public class PlanetExplorer : SpacePrivate 
        { 
          public override void AdvancedCommunicationSkill() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void AdvancedWeaponsTraining() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void BasicCommunicationSkill() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void BasicWeaponsTraining() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void ChartingStarMaps() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Negotiation() 
          { 
            throw new NotImplementedException(); 
          } 

          public override void Persuader() 
          { 
            throw new NotImplementedException(); 
          } 
        }

工作原理...

抽象化使我们能够定义一组共享的功能,这些功能将在所有从抽象类派生的类之间共享。从抽象类继承和从普通类继承的区别在于,使用抽象类,您必须实现该抽象类中定义的所有方法。

这使得类易于版本控制和更改。如果需要添加新功能,可以通过将该功能添加到抽象类中而不破坏任何现有代码来实现。Visual Studio 将要求所有继承类实现抽象类中定义的新方法。

因此,您可以放心,应用的更改将在您代码中从抽象类派生的所有类中实现。

利用封装

封装是什么?简单来说,它是隐藏类的内部工作,这些内部工作对于该类的实现并不必要。将封装视为以下内容:拥有汽车的大多数人知道汽车是用汽油驱动的-他们不需要知道内燃机的内部工作就能使用汽车。他们只需要知道当汽车快没油时需要加油,以及需要检查机油和轮胎气压。即使这样,通常也不是由汽车所有者来做。这对于类和封装来说也是如此。

类的所有者是使用它的人。该类的内部工作不需要暴露给使用该类的开发人员。因此,该类就像一个黑匣子。只要输入正确,开发人员就知道该类的功能是一致的。开发人员并不关心类如何得到输出,只要输入正确即可。

准备工作

为了说明封装的概念,我们将创建一个在内部工作上有些复杂的类。我们需要计算太空飞船的推重比TWR),以确定它是否能够垂直起飞。它需要施加比自身重量更大的推力来抵消重力并进入稳定轨道。这也取决于太空飞船从哪个行星起飞,因为不同的行星对其表面上的物体施加不同的重力。简单来说,推重比必须大于一。

如何做...

  1. 创建一个名为LaunchSuttle的新类。然后,向该类添加以下私有变量,用于引擎推力、航天飞机的质量、当地的重力加速度、地球、月球和火星的重力常数(这些是常数,因为它们永远不会改变)、宇宙引力常数,以及用于处理的行星的枚举器:
        public class LaunchShuttle 
        { 
          private double _EngineThrust; 
          private double _TotalShuttleMass; 
          private double _LocalGravitationalAcceleration; 

          private const double EarthGravity = 9.81; 
          private const double MoonGravity = 1.63; 
          private const double MarsGravity = 3.75; 
          private double UniversalGravitationalConstant; 

          public enum Planet { Earth, Moon, Mars } 
        }

  1. 对于我们的类,我们将添加三个重载的构造函数,这些函数对于根据实例化时的已知事实进行 TWR 计算至关重要(我们假设我们将始终知道发动机推力能力和航天飞机的质量)。我们将为第一个构造函数传递重力加速度。如果我们事先知道该值,这将非常有用。例如,地球的重力加速度为 9.81 m/s²。

第二个构造函数将使用Planet枚举器来计算使用常量变量值的 TWR。

第三个构造函数将使用行星的半径和质量来计算重力加速度,当这些值已知时,以返回 TWR:

        public LaunchShuttle(double engineThrust, 
          double totalShuttleMass, double gravitationalAcceleration) 
        { 
          _EngineThrust = engineThrust; 
          _TotalShuttleMass = totalShuttleMass; 
          _LocalGravitationalAcceleration =  gravitationalAcceleration; 

        } 

        public LaunchShuttle(double engineThrust, 
          double totalShuttleMass, Planet planet) 
        { 
          _EngineThrust = engineThrust; 
          _TotalShuttleMass = totalShuttleMass; 
          SetGraviationalAcceleration(planet); 

        } 

        public LaunchShuttle(double engineThrust, double 
          totalShuttleMass, double planetMass, double planetRadius) 
        { 
          _EngineThrust = engineThrust; 
          _TotalShuttleMass = totalShuttleMass; 
          SetUniversalGravitationalConstant(); 
          _LocalGravitationalAcceleration =  Math.Round(
            CalculateGravitationalAcceleration (
              planetRadius, planetMass), 2); 
        }

  1. 为了使用第二个重载的构造函数,将Planet枚举器作为参数传递给类,我们需要创建另一个方法,将其范围设置为private,以计算重力加速度。我们还需要将_LocalGravitationalAcceleration变量设置为与枚举器值匹配的特定常数。这个方法是类的用户不需要看到的,以便使用类。因此,它被设置为private,以隐藏用户的功能:
        private void SetGraviationalAcceleration(Planet planet) 
        { 
          switch (planet) 
          { 
            case Planet.Earth: 
              _LocalGravitationalAcceleration = EarthGravity; 
            break; 
            case Planet.Moon: 
              _LocalGravitationalAcceleration = MoonGravity; 
            break; 
            case Planet.Mars: 
              _LocalGravitationalAcceleration = MarsGravity; 
            break; 
            default: 
            break; 
          } 
        }

  1. 在以下方法中,只有一个被定义为公共的,因此对类的用户可见。创建私有方法来设置通用引力常数,并计算 TWR 和重力加速度。这些都被设置为私有,因为开发人员不需要知道这些方法的功能就能使用类:
        private void SetUniversalGravitationalConstant() 
        { 
          UniversalGravitationalConstant = 6.6726 * Math.Pow(10,  -11); 
        } 

        private double CalculateThrustToWeightRatio() 
        { 
          // TWR = Ft/m.g > 1 
          return _EngineThrust / (_TotalShuttleMass * 
                      _LocalGravitationalAcceleration); 
        } 

        private double CalculateGravitationalAcceleration(
                       double  radius, double mass) 
        { 
          return (UniversalGravitationalConstant * mass) / 
                                        Math.Pow(radius, 2); 
        } 

        public double TWR() 
       { 
         return Math.Round(CalculateThrustToWeightRatio(), 2); 
       }

  1. 最后,在您的控制台应用程序中,创建以下变量及其已知的值:
        double thrust = 220; // kN 
        double shuttleMass = 16.12; // t 
        double gravitationalAccelerationEarth = 9.81; 
        double earthMass = 5.9742 * Math.Pow(10, 24); 
        double earthRadius = 6378100; 
        double thrustToWeightRatio = 0;

  1. 创建LaunchShuttle类的新实例,并传递需要计算 TWR 的值:
        LaunchShuttle NasaShuttle1 = new LaunchShuttle(thrust, 
                   shuttleMass, gravitationalAccelerationEarth); 
        thrustToWeightRatio = NasaShuttle1.TWR(); 
        Console.WriteLine(thrustToWeightRatio);

  1. 当您在NasaShuttle1变量上使用点运算符时,您会注意到 IntelliSense 只显示TWR方法。该类不会暴露出如何计算得到 TWR 值的内部工作方式。开发人员唯一知道的是,LaunchShuttle类将始终返回正确的 TWR 值,给定相同的输入参数:

  1. 为了测试这一点,创建LaunchShuttle类的另外两个实例,并每次调用不同的构造函数:
        LaunchShuttle NasaShuttle2 = new LaunchShuttle(thrust, 
                       shuttleMass, LaunchShuttle.Planet.Earth); 
        thrustToWeightRatio = NasaShuttle2.TWR(); 
        Console.WriteLine(thrustToWeightRatio); 

        LaunchShuttle NasaShuttle3 = new LaunchShuttle(
           thrust,  shuttleMass, earthMass, earthRadius); 
        thrustToWeightRatio = NasaShuttle3.TWR(); 
        Console.WriteLine(thrustToWeightRatio); 

        Console.Read();

  1. 如果运行您的控制台应用程序,您会看到 TWR 返回相同的值。该值表明,一个重 16.12 吨的航天飞机,配备产生 220 千牛的推力的火箭,将能够从地球表面起飞(即使只是刚刚):

工作原理...

该类使用作用域规则,将类内部的某些功能隐藏在开发人员使用类时。如前所述,开发人员不需要知道如何进行计算以返回 TWR 值。所有这些都有助于使类更有用且易于实现。以下是 C#中可用的各种作用域及其用途的列表:

  • Public:这用于变量、属性、类型和方法,可在任何地方可见。

  • Private:这用于变量、属性、类型和方法,仅在定义它们的块中可见。

  • Protected:这用于变量、属性和方法。不要将其视为公共或私有。受保护的范围仅在使用它的类内部可见,以及在任何继承的类中可见。

  • Friend:这用于变量、属性和方法,只能被同一项目或程序集中的代码使用。

  • ProtectedFriend:这用于变量、属性和方法,是受保护和友元范围的组合(正如名称所示)。

实现多态性

多态性是一个概念,一旦您查看并理解了面向对象编程的其他支柱,就会很容易理解。多态性字面上意味着某物可以有多种形式。这意味着从单个接口,您可以创建多个实现。

这有两个小节,即静态和动态多态性。通过静态多态性,您正在处理方法和函数的重载。您可以使用相同的方法,但执行许多不同的任务。

通过动态多态性,您正在处理抽象类的创建和实现。这些抽象类充当了告诉您派生类应该实现什么的蓝图。接下来的部分将同时查看这两者。

准备工作

我们将首先说明抽象类的用法,这是动态多态性的一个例子。然后,我们将创建重载构造函数作为静态多态性的一个例子。

如何做…

  1. 创建一个名为Shuttle的抽象类,并给它一个名为TWR的成员,这是对航天飞机的推重比进行计算:
        public abstract class Shuttle 
        { 
          public abstract double TWR(); 
        }

  1. 接下来,创建一个名为NasaShuttle的类,并让它继承自抽象类Shuttle,方法是在NasaShuttle类声明的末尾冒号后放置抽象类名称:
        public class NasaShuttle : Shuttle 
        { 

        }

  1. Visual Studio 会下划线标记NasaShuttle类,因为您已经告诉编译器该类继承自抽象类,但尚未实现该抽象类的成员:

  1. 要解决识别出的问题,请键入Ctrl + .(控制键和句点),让 Visual Studio 为您显示一些潜在的修复方法(在这种情况下,只有一个修复方法):

  1. 然后,Visual Studio 会向NasaShuttle类添加缺少的实现。默认情况下,它将添加为未实现,因为您需要为抽象类中覆盖的抽象成员提供实现:
        public class NasaShuttle : Shuttle 
        { 
          public override double TWR() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 创建另一个名为RoscosmosShuttle的类,并从相同的Shuttle抽象类继承:
        public class RoscosmosShuttle : Shuttle 
        { 

        }

  1. 与以前一样,Visual Studio 会下划线标记RoscosmosShuttle类,因为您已经告诉编译器该类继承自抽象类,但尚未实现该抽象类的成员。

  2. 要解决识别出的问题,请键入Ctrl + .(控制键和句点),让 Visual Studio 为您显示一些潜在的修复方法(在这种情况下,只有一个修复方法)。

  3. 然后,重写的方法将作为未实现添加到RoscosmosShuttle类中。您刚刚看到了动态多态性的一个示例:

        public class RoscosmosShuttle : Shuttle 
        { 
          public override double TWR() 
          { 
            throw new NotImplementedException(); 
          } 
        }

  1. 要查看静态多态性的示例,请为NasaShuttle创建以下重载构造函数。构造函数名称保持不变,但构造函数的签名发生变化,这使其成为重载:
        public NasaShuttle(double engineThrust, 
          double  totalShuttleMass, double gravitationalAcceleration) 
        { 

        } 

        public NasaShuttle(double engineThrust, 
          double  totalShuttleMass, double planetMass, 
          double planetRadius) 
        { 

        }

工作原理…

多态性是您通过将良好的面向对象原则应用于类的设计而已经在使用的东西。通过抽象的Shuttle类,我们看到该类在用于从其抽象中派生这些新类时,采用了NasaShuttle类和RoscosmosShuttle类的形式。然后,NasaShuttle类的构造函数被覆盖,以提供相同的方法名称,但使用不同的签名进行实现。

这就是多态性的核心。很可能,您一直在使用它,却不知道它。

单一职责原则

在谈论 SOLID 原则时,我们将从单一职责原则SRP)开始。在这里,我们实际上是在说一个类有一个特定的任务需要完成,不应该做其他任何事情。

准备工作

当向星际飞船添加更多的部队时引发异常,导致其超载时,您将创建一个新的类并编写代码将错误记录到数据库中。对于此示例,请确保已将using System.Data;using System.Data.SqlClient;命名空间添加到您的应用程序中。

如何做...

  1. 创建一个名为StarShip的新类:
        public class Starship 
        { 

        }

  1. 向您的类中添加一个新方法,该方法将设置StarShip类的最大部队容量:
        public void SetMaximumTroopCapacity(int capacity) 
        {             

        }

  1. 在这个方法中,添加一个trycatch子句,将尝试设置最大的部队容量,但由于某种原因,它将失败。失败时,它将错误写入数据库内的日志表:
        try 
        { 
          // Read current capacity and try to add more 
        } 
        catch (Exception ex) 
        { 
          string connectionString = "connection string goes  here";
          string sql = $"INSERT INTO tblLog (error, date) VALUES
            ({ex.Message}, GetDate())";
          using (SqlConnection con = new 
                 SqlConnection(connectionString)) 
          { 
            SqlCommand cmd = new SqlCommand(sql); 
            cmd.CommandType = CommandType.Text; 
            cmd.Connection = con; 
            con.Open(); 
            cmd.ExecuteNonQuery(); 
          } 
          throw ex; 
        }

它是如何工作的...

如果您的代码看起来像前面的代码,那么您就违反了 SRP。StarShip类不再仅负责自身和与星际飞船有关的事物。它现在还必须履行将错误记录到数据库的角色。您在这里看到的问题是数据库记录代码不属于SetMaximumTroopCapacity方法的catch子句。更好的方法是创建一个单独的DatabaseLogging类,其中包含创建连接和将异常写入适当日志表的方法。您还会发现您将不得不在多个地方编写该记录代码(在每个catch子句中)。如果您发现自己重复编写代码(通过从其他地方复制和粘贴),那么您可能需要将该代码放入一个公共类中,并且您可能已经违反了 SRP 规则。

开闭原则

在创建类时,我们需要确保该类通过需要更改内部代码来禁止任何破坏性修改。我们说这样的类是封闭的。如果我们需要以某种方式更改它,我们可以通过扩展类来实现。这种可扩展性是我们说类是开放的扩展。

准备工作

您将创建一个类,通过查看 trooper 的类来确定 trooper 的技能。我们将向您展示许多开发人员创建这样一个类的方式,以及如何使用开闭原则创建它。

如何做...

  1. 创建一个名为StarTrooper的类:
        public class StarTrooper 
        { 

        }

  1. 在这个类中,添加一个名为TrooperClass的枚举器,以标识我们想要返回技能的 trooper 类型。还要创建一个List<string>变量,以包含特定 trooper 类的技能。最后,创建一个名为GetSkills的方法,返回给定 trooper 类的特定技能集。

这个类非常简单,但代码的实现是我们经常看到的。有时,您会看到一大堆if...else语句,而不是switch语句。虽然代码的功能很明确,但很难在不更改代码的情况下向StarTrooper类添加另一个 trooper 类。假设您现在必须向StarTrooper类添加一个额外的Engineer类。您将不得不修改TrooperClass枚举和switch语句中的代码。

代码的更改可能会导致您在先前正常工作的代码中引入错误。我们现在看到StarTrooper类没有关闭,无法轻松地扩展以适应其他TrooperClass对象:

        public enum TrooperClass { Soldier, Medic, Scientist } 
        List<string> TroopSkill; 

        public List<string> GetSkills(TrooperClass troopClass) 
        { 
          switch (troopClass) 
          { 
            case TrooperClass.Soldier: 
              return TroopSkill = new List<string>(new string[] {
                "Weaponry", "TacticalCombat",  "HandToHandCombat" }); 

            case TrooperClass.Medic: 
              return TroopSkill = new List<string>(new string[] {
                "CPR", "AdvancedLifeSupport" }); 

            case TrooperClass.Scientist: 
              return TroopSkill = new List<string>(new string[] {
                "Chemistry",  "MollecularDeconstruction", 
                "QuarkTheory" }); 

            default: 
              return TroopSkill = new List<string>(new string[]  {
                "none" }); 
          } 
        }

  1. 这个问题的解决方案是继承。我们不需要更改代码,而是扩展它。首先,重新编写前面的StarTrooper类并创建一个Trooper类。GetSkills方法声明为virtual
        public class Trooper 
        { 
          public virtual List<string> GetSkills() 
          { 
            return new List<string>(new string[] { "none" }); 
          } 
        }

  1. 现在,我们可以轻松地为可用的SoldierMedicScientisttrooper 类创建派生类。创建以下继承自Trooper类的派生类。您可以看到在创建GetSkills方法时使用了override关键字:
        public class Soldier : Trooper 
        { 
          public override List<string> GetSkills() 
          { 
            return new List<string>(new string[] { "Weaponry", 
                         "TacticalCombat", "HandToHandCombat" }); 
          } 
        } 

        public class Medic : Trooper 
        { 
          public override List<string> GetSkills() 
          { 
            return new List<string>(new string[] { 
                   "CPR",  "AdvancedLifeSupport" }); 
          } 
        } 

        public class Scientist : Trooper 
        { 
          public override List<string> GetSkills() 
          { 
            return new List<string>(new string[] { "Chemistry",
              "MollecularDeconstruction", "QuarkTheory" }); 
          } 
        }

  1. 当扩展类以添加Trooper的附加类时,代码变得非常容易实现。如果现在我们想要添加Engineer类,我们只需在从之前创建的Trooper类继承后重写GetSkills方法:
        public class Engineer : Trooper 
        { 
          public override List<string> GetSkills() 
          { 
            return new List<string>(new string[] {  
              "Construction", "Demolition" }); 
          } 
        }

它是如何工作的...

Trooper类派生的类是Trooper类的扩展。我们可以说每个类都是封闭的,因为修改它不需要改变原始代码。Trooper类也是可扩展的,因为我们已经能够通过创建从中派生的类轻松扩展该类。

这种设计的另一个副产品是更小、更易管理的代码,更容易阅读和理解。

异常处理

异常处理是您作为开发人员需要了解的内容,您还必须非常擅长辨别要向最终用户显示什么信息以及要记录什么信息。信不信由你,编写良好的错误消息比看起来更难。向用户显示太多信息可能会在软件中灌输一种不信任感。为了调试目的记录的信息太少对于需要修复错误的可怜人来说也毫无用处。这就是为什么您需要有一个异常处理策略

一个很好的经验法则是向用户显示一条消息,说明出了问题,但已向支持人员发送了通知。想想谷歌、Dropbox、Twitter(还记得蓝鲸吗?)和其他大公司。有趣的错误页面,上面有一个手臂掉了的小机器人,或者向用户显示一个流行的表情图,远比一个充满堆栈跟踪和红色文本的威胁性错误页面要好得多。这是一种暂时让用户从令人沮丧的情况中抽离的方式。最重要的是,它让您保持面子。

让我们首先看一下异常过滤器。这已经存在一段时间了。Visual Basic.NET(VB.NET)和 F#开发人员已经拥有了这个功能一段时间。它在 C# 6.0 中引入,并且功能远不止看上去的那么简单。乍一看,异常过滤器似乎只是指定需要捕获异常的条件。毕竟,这就是异常过滤器这个名字所暗示的。然而,仔细观察后,我们发现异常过滤器的作用远不止是一种语法糖。

准备工作

我们将创建一个名为Chapter3的新类,并调用一个方法来读取 XML 文件。文件读取逻辑由设置为true的布尔标志确定。想象一下,还有一些其他数据库标志,当设置时,也会将我们的布尔标志设置为true,因此,我们的应用程序知道要读取给定的 XML 文件。

首先确保已添加以下using语句:

using System.IO;

如何做...

  1. 创建一个名为Chapter3的类(如果还没有),其中包含两个方法。一个方法读取 XML 文件,第二个方法记录任何异常错误:
        public void ReadXMLFile(string fileName)
        {
          try
          {
            bool blnReadFileFlag = true;
            if (blnReadFileFlag)
            {
              File.ReadAllLines(fileName);
            }
          }
          catch (Exception ex)
          {
            Log(ex);
            throw;
          }
        }

        private void Log(Exception e)
        {
          /* Log the error */
        }

  1. 在控制台应用程序中,添加以下代码来调用ReadXMLFile方法,并将文件名传递给它以进行读取:
Chapter3 ch3 = new Chapter3();
string File = @"c:tempXmlFile.xml";
ch3.ReadXMLFile(File);

  1. 运行应用程序将生成一个错误(假设您的temp文件夹中实际上没有名为XMLFile.xml的文件)。Visual Studio 将在throw语句上中断:

  1. Log(ex)方法已记录了异常,但是看看 Watch1 窗口。我们不知道blnReadFileFlag的值是多少。当捕获异常时,堆栈被展开(为您的代码增加了开销)到实际的 catch 块。因此,异常发生之前的堆栈状态丢失了。

  1. 修改您的ReadXMLFileLog方法如下以包括异常过滤器:
        public void ReadXMLFile(string fileName)
        {
          try
          {
            bool blnReadFileFlag = true;
            if (blnReadFileFlag)
            {
              File.ReadAllLines(fileName);
            }
          }
          catch (Exception ex) when (Log(ex))
          {
          }
        }
        private bool Log(Exception e)
        {
          /* Log the error */
          return false;
        }

  1. 再次运行控制台应用程序,Visual Studio 将在导致异常的实际代码行上中断:

  1. 更重要的是,blnReadFileFlag的值仍然在作用域内。这是因为异常过滤器可以看到异常发生的地点的堆栈状态,而不是异常处理的地点。在 Visual Studio 的本地窗口中查看,您会发现变量在异常发生的地点仍然在作用域内。

它是如何工作的...

想象一下能够在日志文件中查看异常信息,并且所有局部变量值都可用。另一个有趣的地方要注意的是Log(ex)方法中的返回false语句。使用这种方法记录错误并返回false将允许应用程序继续并在其他地方处理异常。如您所知,捕获Exception ex将捕获一切。通过返回false,异常过滤器不会进入catch语句,并且可以使用更具体的catch异常(例如,在catch (Exception ex)语句之后的catch (FileNotFoundException ex))来处理特定错误。通常,在捕获异常时,FileNotFoundException不会在以下代码示例中被捕获:

catch (Exception ex)
{ 
}
catch (FileNotFoundException ex)
{ 
}

这是因为捕获异常的顺序是错误的。传统上,开发人员必须按照特异性的顺序捕获异常,这意味着FileNotFoundExceptionException更具体,因此必须在catch (Exception ex)之前放置。通过调用返回false的方法的异常过滤器,我们可以准确检查和记录异常:

catch (Exception ex) when (Log(ex))
{ 
}
catch (FileNotFoundException ex)
{ 
}

前面的代码将捕获所有异常,并在这样做时准确记录异常,但不会进入异常处理程序,因为Log(ex)方法返回false。异常过滤的另一个实现是,它们可以允许开发人员在发生故障时重试代码。您可能不希望特别捕获第一个异常,而是在方法中实现一种超时元素。当错误计数器达到最大迭代次数时,您可以捕获并处理异常。您可以在这里看到基于try子句计数捕获异常的示例:

public void TryReadXMLFile(string fileName)
{
  bool blnFileRead = false;
  do
  {
    int iTryCount = 0;
    try
    {
      bool blnReadFileFlag = true;
      if (blnReadFileFlag)
      File.ReadAllLines(fileName);
    }
    catch (Exception ex) when (RetryRead(ex, iTryCount++) == true)
    {
    }
  } while (!blnFileRead);
}

private bool RetryRead(Exception e, int tryCount)
{
  bool blnThrowEx = tryCount <= 10 ? blnThrowEx = 
       false : blnThrowEx = true;
  /* Log the error if blnThrowEx = false */
  return blnThrowEx;
}

异常过滤是处理代码中异常的一种非常有用且非常强大的方式。异常过滤的幕后工作并不像人们想象的那样立即显而易见,但这就是异常过滤的实际力量所在。

第四章:Visual Studio 中的代码分析器

在本章中,我们将看一下代码分析器以及它们如何帮助开发人员编写更好的代码。我们将涵盖以下主题:

  • 查找并安装分析器

  • 创建代码分析器

  • 创建自定义代码分析器

  • 仅在您的组织内部部署您的代码分析器

介绍

从 Visual Studio 2015 开始,开发人员可以创建特定于其项目或开发团队的自定义代码分析器。一些开发团队有一套需要遵守的标准。也许您是独立开发人员,希望使您的代码符合某些最佳实践。无论您的原因是什么,代码分析器都为开发人员打开了大门。

您可以确保您或您的团队发布的代码符合特定的代码质量标准。可以从 GitHub 下载几个代码分析器。我们将看一下其中一个名为 CodeCracker for C#的代码分析器。

查找并安装分析器

GitHub 上有很多代码分析器。快速搜索返回了 72 个存储库结果中的 28 个可能的 C#代码分析器。其中一些似乎是学生项目。也检查一下这些;其中一些代码非常聪明。至于这个示例,我们将使用 CodeCracker for C#来演示如何从 NuGet 包中安装分析器。

准备工作

您要做的就是为项目下载一个 NuGet 包。除此之外,您无需做任何特别的准备。

如何做...

  1. 首先创建一个新的控制台应用程序。您可以随意命名。在我的示例中,我只是称它为DiagAnalyzerDemo

  1. 从“工具”菜单中,选择 NuGet 包管理器,然后选择“解决方案的 NuGet 包管理器”。

  2. 在“浏览”选项卡中,搜索Code-Cracker。结果应返回 codecracker.CSharp NuGet 包。选择要应用 NuGet 包的项目,然后单击“安装”按钮。

  1. Visual Studio 将允许您查看即将进行的更改。单击“确定”按钮继续。

  2. 在显示许可条款时,单击“接受”。

  3. 安装 NuGet 包后,结果将显示在“输出”窗口中。

  4. 查看您的项目,您会注意到 CodeCracker.CSharp 分析器已添加到解决方案资源管理器中的“分析器”节点下。

  1. 如果展开 CodeCracker.CSharp 分析器,您将看到 NuGet 包中包含的所有单独的分析器。

  1. 然而,有一个更好的地方可以查看这些分析器。从“项目”菜单中,转到“[项目名称]”属性菜单项。在我的情况下,这是 DiagAnalyzerDemo 属性....

  2. 单击“打开”按钮打开规则集。

  1. 在这里,您将看到所有可用的分析器集合;从此屏幕,您可以修改特定分析器的操作。

  1. 在您的代码中,添加以下类。您可以随意命名,但为简单起见,请使用以下示例。您将看到我有一个构造函数,设置了一个名为DimensionWHL的属性。此属性只返回一个包含“宽度”、“高度”和“长度”值的数组。确实不是很好的代码。
        public class ShippingContainer
        {
          public int Width { get; set; }
          public int Height { get; set; }
          public int Length { get; set; }
          public int[] DimensionsWHL { get; set; }
          public ShippingContainer(int width, int height, int length)
          {
            Width = width;
            Height = height;
            Length = length;

            DimensionsWHL = new int[] { width, height, length };
          }
        }

  1. 返回到分析器屏幕并搜索单词“属性”。您将看到一个名为 CA1819 的分析器,指定属性永远不应返回数组。操作更改为警告,但如果愿意,可以通过单击“操作”列下的“警告”单词并选择“错误”来更改为错误。

  1. 保存更改并构建您的控制台应用程序。您将看到代码分析器 CA1819 的警告显示在错误列表中。如果将操作更改为错误,构建将会因为该错误而中断。

工作原理...

代码分析器可以为您提供许多功能,并帮助开发人员避免常见的不良编码实践,并强制执行特定的团队准则。每个代码分析器可以设置为不同的严重程度,最严重的实际上会导致构建失败。将代码分析器保留在项目的引用中允许您将其检入源代码控制;这在构建项目时进行评估。但是,您也可以将分析器存储在每台计算机上。这些分析器将用于个人代码改进、提示和个人使用。

代码分析器非常适合现代开发人员,因为它们在开发人员的控制下,并且可以轻松集成到 Visual Studio 中。

创建代码分析器

有些人可能已经看到了创建自己的代码分析器的好处。能够控制特定设计实现和团队特定的编码标准对您的团队来说是非常宝贵的。这对于加入您的团队的新开发人员尤其重要。我记得几年前开始为一家公司工作时,开发经理给了我一份需要遵守的代码标准文件。当时这很棒。它向我表明他们关心代码标准。当时,开发人员当然没有代码分析器。然而,跟踪我需要实施的所有标准是相当具有挑战性的。特别是对于公司实施的特定代码标准来说,情况尤其如此。

准备工作

在您创建自己的代码分析器之前,您需要确保已安装.NET 编译器平台 SDK。要做到这一点,请执行以下步骤:

  1. 向您的解决方案添加一个新项目,然后单击可扩展性。选择下载.NET 编译器平台 SDK,然后单击确定。

  1. 这实际上将创建一个带有索引文件的项目。打开的页面将提供下载.NET 编译器平台 SDK 的链接。单击该链接开始下载。

  1. 只需将下载的文件保存到硬盘上的一个目录中。然后在单击 VSIX 文件之前关闭 Visual Studio。

  1. .NET 编译器平台 SDK 安装程序现在将启动,并允许您选择要安装到的 Visual Studio 实例。

安装完成后,再次重新启动 Visual Studio。

如何做...

  1. 向您的 Visual Studio 解决方案添加一个新项目,然后单击可扩展性,选择带有代码修复的分析器(NuGet + VSIX)模板。给它一个合适的名称,然后单击确定以创建分析器项目。

  1. 您会发现 Visual Studio 已为您创建了三个项目:Portable.Test.Vsix。确保.Vsix项目设置为默认启动项目。

  1. Portable类中,查看DiagnosticAnalyzer.cs文件。您将看到一个名为AnalyzeSymbol()的方法。这个代码分析器所做的一切就是简单地检查namedTypeSymbol变量上是否存在小写字母。
        private static void AnalyzeSymbol(
          SymbolAnalysisContext context)
        {
          // TODO: Replace the following code with your own 
             analysis, generating Diagnostic objects for any 
             issues you find
          var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

          // Find just those named type symbols with names 
             containing lowercase letters.
          if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower))
          {
            // For all such symbols, produce a diagnostic.
            var diagnostic = Diagnostic.Create(Rule, 
              namedTypeSymbol.Locations[0], namedTypeSymbol.Name);

            context.ReportDiagnostic(diagnostic);
          }
        }

  1. 构建您的项目并单击F5开始调试。这将启动一个新的 Visual Studio 实例,具有自己的设置。这意味着您在这个实验性的 Visual Studio 实例中所做的任何更改都不会影响您当前的 Visual Studio 安装。您可以打开现有项目或创建新项目。我只是创建了一个控制台应用程序。从一开始,您会看到Program类名被下划线标记。将光标悬停在此处将显示 Visual Studio 的灯泡,并告诉您类型名称包含小写字母。

  1. 单击Ctrl + .或在工具提示中单击“显示潜在修复”链接,将显示您可以应用以纠正错误的修复程序。

工作原理...

代码分析器将检查托管程序集并报告任何相关信息。这可以是违反.NET Framework Design Guidelines中的编程和设计规则的任何代码。代码分析器将显示其执行的检查作为警告消息,并在可能的情况下建议修复,就像我们在前面的示例中看到的那样。为此,代码分析器使用由 Microsoft 创建的规则集或您定义的自定义规则集来满足特定需求。

创建自定义代码分析器

当您创建一个适合特定需求的代码分析器时,代码分析器的真正魔力就会显现出来。什么样的需求会被视为特定需求呢?嗯,任何特定于您自己业务需求的东西,而这些在现有的分析器中没有涵盖。不要误会我;对开发人员可用的现有分析器确实涵盖了许多良好的编程实践。只需在 GitHub 上搜索 C#代码分析器,就可以看到。

然而,有时您可能会遇到更适合您的工作流程或公司业务方式的情况。

例如,可以确保所有公共方法的注释包含的信息不仅仅是标准的<summary></summary>和参数信息(如果有)。您可能希望包含一个附加的标签,例如内部任务 ID(考虑 Jira)。另一个例子是确保创建的类符合特定的 XML 结构。您是否正在开发将仓库库存信息写入数据库的软件?您是否使用非库存零件?您如何在代码中验证非库存和库存零件?代码分析器可以在这里提供解决方案。

前面的示例可能是相当独特的,可能与您或您的需求无关,但这就是代码分析器的美妙之处。您可以创建它们以满足您的需求。让我们看一个非常简单的例子。假设您组织中的开发人员需要使用特定的代码库。这个代码库是一组经常使用的代码,而且维护得很好。它包含在开发人员创建新项目时使用的 Visual Studio 模板中。我们需要确保,如果开发人员创建特定类(用于采购订单或销售订单),它实现了特定接口。这些接口存在于模板中,但类不存在。这是因为应用程序并不总是使用销售或采购订单。该接口是为了使销售和采购订单能够接收,称为 IReceivable。

准备工作

执行以下步骤:

  1. 创建一个新的 Visual Studio 项目,命名为PurchaseOrderAnalyzer

  1. 确保默认情况下创建以下项目。

如何做...

  1. 展开PurchaseOrderAnalyzer (Portable)项目并打开DiagnosticAnalyzer.cs文件。

  1. 如前所述,您将看到您的诊断分析器类。它应该读取public class PurchaseOrderAnalyzerAnalyzer : DiagnosticAnalyzer。将以下代码添加到此类的顶部,替换DiagnosticIdTitleMessageFormatDescriptionCategoryRule变量的代码。请注意,我在类中添加了两个名为ClassTypesToCheckMandatoryInterfaces的枚举器。我只希望此分析器在类名为PurchaseOrderSalesOrder时才起作用。我还希望IReceiptable接口在ClassTypesToCheck枚举中定义的类中是强制性的。
        public const string DiagnosticId = "PurchaseOrderAnalyzer";

        public enum ClassTypesToCheck { PurchaseOrder, SalesOrder }
        public enum MandatoryInterfaces { IReceiptable }

        private static readonly LocalizableString Title = 
          "Interface Implementation Available"; 
        private static readonly LocalizableString 
          MessageFormat = "IReceiptable Interface not Implemented"; 
        private static readonly LocalizableString Description = 
          "You need to implement the IReceiptable interface"; 
        private const string Category = "Naming";

        private static DiagnosticDescriptor Rule = new 
          DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, 
          Category, DiagnosticSeverity.Warning, 
          isEnabledByDefault: true, description: Description);

  1. 确保Initialize方法包含以下代码:
        public override void Initialize(AnalysisContext context)
        {
          context.RegisterSymbolAction(AnalyzeSymbol, 
            SymbolKind.NamedType);
        }

  1. 创建AnalyzeSymbol方法。您可以将此方法命名为任何您喜欢的名称。只需确保无论您如何命名此方法,它都与Initialize中的RegisterSymbolAction()方法中的方法名称匹配。
        private static void AnalyzeSymbol(SymbolAnalysisContext context)
        {

        }

  1. 再添加一个名为blnInterfaceImplemented的布尔值,它将存储接口是否已实现的truefalse。我们接下来要做的检查是忽略抽象类。实际上,您可能也想检查抽象类,但我想排除它以展示代码分析器的灵活性。
        bool blnInterfaceImplemented = false;
        if (!context.Symbol.IsAbstract)
        {

        }

  1. 现在,您需要获取您正在检查的符号的名称。为此,请创建一个名为namedTypeSymbol的对象,您可以在该对象上调用Name方法来返回符号名称。在名为PurchaseOrder的类上,这应该返回PurchaseOrder作为名称。将ClassTypesToCheck枚举作为名为classesToCheckList<string>对象返回。然后,对类名进行检查,看它是否包含在classesToCheck列表中。通过在Equals检查中添加StringComparison.OrdinalIgnoreCase来忽略大小写是很重要的。这将确保分析器将分析名为purchaseorderPURCHASEORDERPurchaseOrderPurchaseorderpurchaseOrder的类。将代码添加到if条件中,不包括抽象类。
        var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
        List<string> classesToCheck = Enum.GetNames(
          typeof(ClassTypesToCheck)).ToList();

        if (classesToCheck.Any(s => s.Equals(
          namedTypeSymbol.Name, StringComparison.OrdinalIgnoreCase)))
        {

        }

类名的推荐大写风格是 PascalCase。PascalCase 包括大写标识符的第一个字母和每个后续连接的单词。如果标识符有三个或更多字符,则应用此规则。这意味着在类名中使用连接的单词 purchase 和 order 时必须使用 PascalCase。这将导致PurchaseOrder。请参阅 MSDN 中的 Capitalization Styles 文章。

  1. if条件中,要检查类名是否为PurchaseOrderSalesOrder,请添加以下代码。在这里,我们将检查匹配的PurchaseOrderSalesOrder类上定义的接口。我们通过调用AllInterfaces()方法来实现这一点,并检查它是否与IReceiptable枚举的nameof匹配。实际上,我们可能希望检查多个接口,但出于我们的目的,我们只检查IReceiptable接口的实现。如果我们发现接口在之前检查中匹配了类名上的实现,我们将设置blnInterfaceImplemented = true;(它当前初始化为false)。这意味着,如果接口没有匹配,那么我们将为省略IReceiptable接口产生诊断。这是通过创建和报告包含先前定义的Rule和类名位置的诊断来完成的。
        string interfaceName = nameof(
          MandatoryInterfaces.IReceiptable);

        if (namedTypeSymbol.AllInterfaces.Any(s => s.Name.Equals(
          interfaceName, StringComparison.OrdinalIgnoreCase)))
        {
          blnInterfaceImplemented = true;
        }

        if (!blnInterfaceImplemented)
        {
          // Produce a diagnostic.
          var diagnostic = Diagnostic.Create(Rule, 
            namedTypeSymbol.Locations[0], namedTypeSymbol.Name);
          context.ReportDiagnostic(diagnostic);
        }

  1. 如果所有代码都添加到AnalyzeSymbol()方法中,该方法应如下所示:
        private static void AnalyzeSymbol(SymbolAnalysisContext context)
        {
          bool blnInterfaceImplemented = false;
          if (!context.Symbol.IsAbstract)
          {
            var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
            List<string> classesToCheck = Enum.GetNames(
              typeof(ClassTypesToCheck)).ToList();

            if (classesToCheck.Any(s => s.Equals(namedTypeSymbol.Name, 
              StringComparison.OrdinalIgnoreCase)))
            {
              string interfaceName = nameof(
                MandatoryInterfaces.IReceiptable);

              if (namedTypeSymbol.AllInterfaces.Any(s => s.Name.Equals(
                interfaceName, StringComparison.OrdinalIgnoreCase)))
              {
                blnInterfaceImplemented = true;
              }

              if (!blnInterfaceImplemented)
              {
                // Produce a diagnostic.
                var diagnostic = Diagnostic.Create(Rule, 
                  namedTypeSymbol.Locations[0], namedTypeSymbol.Name);
                context.ReportDiagnostic(diagnostic);
              }
            }
          }
        }

  1. 现在,我们需要为代码分析器创建一个修复程序。如果我们发现类没有实现我们的接口,我们希望为开发人员提供一个快速修复的灯泡功能。打开名为CodeFixProvider.cs的文件。您会看到其中包含一个名为public class PurchaseOrderAnalyzerCodeFixProvider : CodeFixProvider的类。首先要做的是找到title字符串常量,并将其更改为更合适的标题。这是在 Visual Studio 中单击灯泡时显示的菜单弹出窗口。
        private const string title = "Implement IReceiptable";

  1. 我已经将大部分代码修复代码保持不变,除了执行实际修复的代码。找到名为RegisterCodeFixesAsync()的方法。我将该方法重命名为ImplementRequiredInterfaceAsync(),以在RegisterCodeFix()方法中调用。代码应如下所示:
        public sealed override async Task RegisterCodeFixesAsync(
          CodeFixContext context)
        {
          var root = await context.Document.GetSyntaxRootAsync(
            context.CancellationToken).ConfigureAwait(false);

          var diagnostic = context.Diagnostics.First();
          var diagnosticSpan = diagnostic.Location.SourceSpan;

          // Find the type declaration identified by the diagnostic.
          var declaration = root.FindToken(diagnosticSpan.Start)
            .Parent.AncestorsAndSelf().OfType
            <TypeDeclarationSyntax>().First();

          // Register a code action that will invoke the fix.
          context.RegisterCodeFix(
            CodeAction.Create(
              title: title,
              createChangedSolution: c => 
              ImplementRequiredInterfaceAsync(context.Document, 
                declaration, c),
            equivalenceKey: title),
          diagnostic);
        }

  1. 您会注意到,我已经重新使用了用于将符号大写的修复程序来实现接口。其余的代码保持不变。实际上,您很可能希望检查类上是否实现了其他接口,并保持这些实现。在这个演示中,我们只是假设正在创建一个名为PurchaseOrderSalesOrder的新类,而没有现有的接口。
        private async Task<Solution> ImplementRequiredInterfaceAsync(
          Document document, TypeDeclarationSyntax typeDecl, 
          CancellationToken cancellationToken)
        {
          // Get the text of the PurchaseOrder class and return one 
             implementing the IPurchaseOrder interface
          var identifierToken = typeDecl.Identifier;

          var newName = $"{identifierToken.Text} : IReceiptable";

          // Get the symbol representing the type to be renamed.
          var semanticModel = await document.GetSemanticModelAsync(
            cancellationToken);
          var typeSymbol = semanticModel.GetDeclaredSymbol(
            typeDecl, cancellationToken);

          // Produce a new solution that has all references to 
             that type renamed, including the declaration.
          var originalSolution = document.Project.Solution;
          var optionSet = originalSolution.Workspace.Options;
          var newSolution = await Renamer.RenameSymbolAsync(
            document.Project.Solution, typeSymbol, newName, 
            optionSet, cancellationToken).ConfigureAwait(false);

          return newSolution;
        }

  1. 确保PurchaseOrderAnalyzer.Vsix项目设置为启动项目,然后单击“调试”。将启动 Visual Studio 的新实例。在这个 Visual Studio 实例中创建一个新的控制台应用程序,并将其命名为PurchaseOrderConsole。向该项目添加一个名为IReceiptable的新接口,并添加以下代码。
        interface IReceiptable
        {
          void MarkAsReceipted(int orderNumber);
        }

  1. 现在,向项目添加一个名为PurchaseOrder的新类,其中包含以下代码。
        public class PurchaseOrder 
        {

        }

  1. 完成此操作后,如果为IReceiptablePurchaseOrder添加了单独的文件,您的项目可能如下所示。

  1. 查看PurchaseOrder类时,您会注意到类名PurchaseOrder下有一个波浪线。

  1. 将鼠标悬停在波浪线上,您将看到灯泡显示通知您IReceiptable接口未实现。

  1. 当您查看潜在的修复时,您将看到我们在CodeFixProvider.cs文件中更改的title在飞出菜单文本中显示为private const string title = "Implement IReceiptable";。然后建议的代码显示为实现正确的接口IReceiptable

  1. 单击此按钮会修改我们的PurchaseOrder类,生成以下代码:
        public class PurchaseOrder : IReceiptable 
        {

        }

  1. 应用代码修复后,您会看到类名下的波浪线已经消失。正如预期的那样,Visual Studio 现在告诉我们需要通过在IReceiptable接口名称下划线标记IReceiptable.MarkAsReceipted(int)来实现接口成员。

  1. 将鼠标悬停在IReceiptable接口名称上,您将看到代码修复的灯泡。这是标准的 Visual Studio 分析器在这里起作用。

  1. 单击要应用的修复程序,实现IReceiptable成员和PurchaseOrder类在代码中正确定义。

它的工作原理...

本示例中的示例甚至没有开始涉及代码分析器的可能性。了解可能性的一个很好方法是查看 GitHub 上的一些代码分析器。查看代码并开始编写自己的代码分析器。与编程中的大多数概念一样,学习的唯一方法就是编写代码。互联网上有大量的信息可供使用。不过,建议在开始编写自己的代码分析器之前,先看看是否已经有一个分析器可以满足您的需求(或者接近满足您的需求)。

例如,如果您需要确保方法注释包含附加信息,请尝试查找一个已经执行类似操作的分析器。例如,如果您找到一个检查公共方法是否有注释的分析器,您可以轻松地修改此分析器以满足自己的需求。学习的最佳方法是实践,但每个人都需要一个起点。站在他人的肩膀上是学习新编程概念的一部分。

仅在组织内部部署您的代码分析器

代码分析器是一种检查和自动纠正代码的绝妙方法。然而,您创建的分析器有时可能不适合公开使用,因为它们可能包含专有信息。通过 NuGet,您可以创建私有存储库并与同事共享。例如,您可以使用公司服务器上的共享位置,并轻松管理 NuGet 包。

准备工作

确保您的组织中的所有开发人员都可以访问共享位置。这可以是您的网络管理员提供的任何共享文件访问位置。您可能希望将这些包的访问权限限制为开发人员。一个不错的解决方案是在 Azure 上创建一个存储账户来共享 NuGet 包。这是我在这里使用的方法,我使用了一个名为 Acme Corporation 的虚构公司。

我不会详细介绍如何在 Azure 上设置存储账户,但我会谈谈如何从本地机器访问它。

我鼓励你和你的组织考虑使用 Azure。我不会过多扩展使用 Azure 的好处,只是说它可以节省大量时间。如果我想测试特定应用程序的特定功能在特定操作系统上,几分钟内我就能启动一个虚拟机并通过远程桌面连接到它。它立即可以使用。

在 Azure 上创建存储账户后,你会在“访问密钥”选项卡上找到访问密钥。

  1. 记下密钥和存储账户名称。

  1. 我还创建了一个名为packages的文件服务。要到达这里,点击“概述”。然后,在“服务”标题下,点击“文件”。在文件服务窗口上,选择packages并查看文件共享的属性信息。

你的存储账户可能与本书中的示例不同,这取决于你的命名。

  1. 记下属性中指定的 URL。使用该 URL,通过将路径中的https://部分更改为\\,并将任何后续的/更改为\,映射一个网络驱动器。

  1. 将此路径添加到文件夹文本框,并确保已选中使用不同凭据进行连接。

使用存储账户名称作为用户名,使用其中一个密钥作为密码。现在你已经将一个网络驱动器映射到了你的 Azure 存储账户。

如何做...

  1. 看一下我们创建的PurchaseOrderAnalyzer项目。你会看到有一个包含两个名为install.ps1uninstall.ps1的 PowerShell 脚本的tools文件夹。在这里,你可以指定任何特定于安装的资源或卸载软件包时要执行的操作。

  1. 打开Diagnostic.nuspec文件,你会注意到其中包含了关于你即将部署的 NuGet 程序包的信息。务必修改此文件,因为它包含了对开发人员使用你的 NuGet 程序包很重要的信息。
        <?xml version="1.0"?>
        <package >
          <metadata>
            <id>PurchaseOrderAnalyzer</id>
            <version>1.1.1.1</version>
            <title>Purchase Order Analyzer</title>
            <authors>Dirk Strauss</authors>
            <owners>Acme Corporation</owners>
            <licenseUrl>http://www.acmecorporation.com/poanalyzer/
             license</licenseUrl>
            <projectUrl>http://www.acmecorporation.com/poanalyzer
             </projectUrl>
            <requireLicenseAcceptance>true</requireLicenseAcceptance>
            <description>Validate the creation of Purchase Order Objects 
             withing Acme Corporation's development projects
            </description>
            <releaseNotes>Initial release of the Purchase Order 
             Analyzer.</releaseNotes>
            <copyright>Copyright</copyright>
            <tags>PurchaseOrderAnalyzer, analyzers</tags>
            <frameworkAssemblies>
              <frameworkAssembly assemblyName="System" 
               targetFramework="" />
            </frameworkAssemblies>
          </metadata>
          <!-- The convention for analyzers is to put language 
           agnostic dlls in analyzersportable50 and language 
           specific analyzers in either analyzersportable50cs or 
           analyzersportable50vb -->
          <files>
            <file src="img/*.dll" target="analyzersdotnetcs" 
             exclude="**Microsoft.CodeAnalysis.*;
             **System.Collections.Immutable.*;
             **System.Reflection.Metadata.*;
             **System.Composition.*" />
            <file src="img/tools*.ps1" target="tools" />
          </files>
        </package>

  1. 继续构建你的代码分析器。你会看到在项目的bin文件夹中创建了一个名为PurchaseOrderAnalyzer.1.1.1.1.nupkg的文件。将该文件复制到你之前在 Azure 存储账户中创建的映射驱动器。

  2. 在 Visual Studio 中,添加一个新的 WinForms 应用程序。你可以随意命名。现在可以将存储账户添加为 NuGet 位置。转到工具,NuGet 程序包管理器,然后单击“解决方案的 NuGet 程序包管理器...”。你会注意到,在当前设置为 nuget.org 的包源旁边,有一个小齿轮图标。点击它。

我为这个示例在一个单独的机器上创建了 Visual Studio WinForms 应用程序,但如果你没有单独的机器,可以尝试使用虚拟机进行测试。如果你无法访问 Azure,也可以使用 VirtualBox。

  1. 在“选项”屏幕上,通过单击“可用包源”下方的绿色加号图标,可以添加一个额外的 NuGet 程序包源。

  1. 在“选项”窗口底部,输入一个适当的位置名称,并输入 Azure 存储账户的路径。这是你在映射网络驱动器时输入的相同路径。在点击“确定”之前,点击“更新”按钮。然后点击“确定”按钮。

  1. 现在可以将包源更改为设置为你映射到的 Azure 存储账户位置。这样做并单击 NuGet 程序包管理器的“浏览”选项卡将显示此文件共享上的所有程序包。右侧“选项”部分中的信息是你在Diagnostic.nuspec文件中定义的信息。

  1. 现在可以继续安装代码分析器 NuGet 包。安装完成后,代码分析器将在项目的References下的Analyzers节点下可见。

  1. 代码分析器也完全按预期工作。创建一个名为PurchaseOrder的类,看看分析器是如何运作的。

它是如何工作的...

NuGet 包是将代码部署到大众或少数开发人员的最简单方式。它可以轻松实现代码和模板的共享,因此使用 NuGet 来部署代码分析器是非常合理的。使用 NuGet 设置一个私有存储库来在组织内共享代码非常简单。

第五章:正则表达式

正则表达式regex)对许多开发人员来说是一种神秘。我们承认,我们经常使用它们,以至于需要更深入地了解它们的工作原理。另一方面,互联网上有许多经过验证的正则表达式模式,只需重复使用已经存在的模式比尝试自己创建一个更容易。正则表达式的主题远远超出了本书中的单一章节所能解释的范围。

因此,在本章中,我们只是介绍了一些正则表达式的概念。要更深入地了解正则表达式,需要进一步学习。然而,为了本书的目的,我们将更仔细地看看如何创建正则表达式以及如何将其应用于一些常见的编程问题。在本章中,我们将涵盖以下内容:

  • 开始使用正则表达式-匹配有效日期

  • 清理输入

  • 动态正则表达式匹配

介绍

正则表达式是通过使用特殊字符描述字符串的模式,这些特殊字符表示需要匹配的特定文本。正则表达式的使用在编程中并不是一个新概念。为了使正则表达式工作,它需要使用一个执行所有繁重工作的正则表达式引擎。

在.NET Framework 中,微软提供了正则表达式的使用。要使用正则表达式,您需要将System.Text.RegularExpressions程序集导入到您的项目中。这将允许编译器使用您的正则表达式模式并将其应用于您需要匹配的特定文本。

其次,正则表达式有一组特殊含义的元字符,这些字符是[ ], { }, ( ), *, +, , ?, |, $, ., 和 ^

例如,使用花括号{ }使开发人员能够指定特定字符集需要出现的次数。另一方面,使用方括号则确切地定义了需要匹配的内容。

例如,如果我们指定了[abc],那么模式将寻找小写的 A、B 和 C。因此,正则表达式还允许您定义一个范围,例如[a-c],这与[abc]模式的解释方式完全相同。

正则表达式还允许您使用^字符定义要排除的字符。因此,键入[^a-c]将找到小写的 D 到 Z,因为模式告诉正则表达式引擎排除小写的 A、B 和 C。

正则表达式还定义了dD作为[0-9][⁰-9]的一种快捷方式。因此,d匹配所有数字值,而D匹配所有非数字值。另一个快捷方式是wW,它们匹配从小写 A 到 Z 的任何字符,不考虑大小写,从 0 到 9 的所有数字值,以及下划线字符。因此,w[a-zA-Z0-9_],而W[^a-zA-Z0-9_]

正则表达式的基础相当容易理解,但您还可以做很多其他事情。

开始使用正则表达式-匹配有效日期

如果您还没有这样做,请创建一个新的控制台应用程序,并在项目中添加一个名为RegExDemo的类。此时您的代码应该看起来像这样:

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

public class RegExDemo
{

}

准备工作

为了本书的目的,我们使用控制台应用程序来说明正则表达式的使用。实际上,您可能不会将这种逻辑混在生产代码之间,因为这将导致代码被重写。添加类似正则表达式的最佳位置是在扩展方法中的帮助类中。

如何做...

  1. 在控制台应用程序中,添加以下using语句,以便我们可以在.NET 中使用正则表达式程序集:
        using System.Text.RegularExpressions;

  1. 我们将创建一个正则表达式来验证 yyyy-mm-dd、yyyy/mm/dd 或 yyyy.mm.dd 的日期模式。一开始,正则表达式看起来可能令人生畏,但请耐心等待。当您完成代码并运行应用程序时,我们将解析这个正则表达式。希望表达式逻辑会变得清晰。

  2. RegExDemo类中,创建一个名为ValidDate()的新方法,该方法以字符串作为参数。这个字符串将是我们想要验证的日期模式:

        public void ValidDate(string stringToMatch) 
        { 

        }

  1. 将以下正则表达式模式添加到方法中的变量中:
        string pattern = $@"^(19|20)dd-./
                         -./$";

  1. 最后,添加正则表达式以匹配提供的字符串参数:
        if (Regex.IsMatch(stringToMatch, pattern)) 
            Console.WriteLine($"The string {stringToMatch} 
                              contains a valid date."); 
        else 
            Console.WriteLine($"The string {stringToMatch} DOES 
                              NOT contain a valid date.");

  1. 当您完成这些操作后,您的方法应该如下所示:
        public void ValidDate(string stringToMatch) 
        { 
          string pattern = $@"^(19|20)dd-./
                           -./$"; 

          if (Regex.IsMatch(stringToMatch, pattern)) 
              Console.WriteLine($"The string {stringToMatch} contains
                                a valid date."); 
          else 
              Console.WriteLine($"The string {stringToMatch} DOES 
              NOT contain a valid date.");             
        }

  1. 回到您的控制台应用程序,添加以下代码并通过单击“开始”调试您的应用程序:
        RegExDemo oRecipe = new RegExDemo(); 
        oRecipe.ValidDate("1912-12-31"); 
        oRecipe.ValidDate("2018-01-01"); 
        oRecipe.ValidDate("1800-01-21"); 
        oRecipe.ValidDate($"{DateTime.Now.Year}
                          .{DateTime.Now.Month}.{DateTime.Now.Day}"); 
        oRecipe.ValidDate("2016-21-12");  
        Console.Read();

您会注意到,如果您添加了using static System.Console;命名空间,那么您只需要调用Read()而不是Console.Read()。这种新功能,您可以导入静态命名空间,是在 C# 6.0 中添加的。

  1. 日期字符串被传递给正则表达式,并且模式与参数中的日期字符串匹配。输出显示在控制台应用程序中:

  1. 仔细观察输出,您会注意到有一个错误。我们正在验证格式为 yyyy-mm-dd、yyyy/mm/dd 和 yyyy.mm.dd 的日期字符串。如果我们使用这个逻辑,我们的正则表达式错误地将一个有效的日期标记为无效。这是日期2016.4.10,它是 2016 年 4 月 10 日,实际上是有效的。

我们很快会解释日期1800-01-21为什么无效。

  1. 返回到您的ValidDate()方法,并将正则表达式更改为如下所示:
        string pattern = $@"^(19|20)dd-./
                         -./$";

  1. 再次运行控制台应用程序并查看输出:

这次正则表达式对所有给定的日期字符串都起作用了。但我们到底做了什么?它是如何工作的。

它是如何工作的...

让我们仔细看看前面代码示例中使用的两个表达式。将它们与彼此进行比较,您可以看到我们在黄色中所做的更改:

在我们了解这个变化意味着什么之前,让我们分解表达式并查看各个组件。我们的正则表达式基本上是在说,我们必须匹配所有以 19 或 20 开头并具有以下分隔符的字符串日期:

  • 破折号(-)

  • 小数点(.)

  • 斜杠(/)

为了更好地理解表达式,我们需要了解表达式<有效年份><有效分隔符><有效月份><有效分隔符><有效日期>的以下格式。

我们还需要能够告诉正则表达式引擎考虑一个另一个模式。单词|元字符表示。为了使正则表达式引擎在不分割整个表达式的情况下考虑这个词,我们将其包装在括号()中。

以下是正则表达式中使用的符号:

条件性或描述
年份部分描述
(19
dd 匹配 0 到 9 之间的两个个位数。要匹配 0 到 9 之间的一个数字,您将使用 d。
有效分隔符字符集描述
[-./]匹配字符集中的任何一个字符。这些是我们的有效分隔符。要匹配空格日期分隔符,您可以将其更改为[- ./],在字符集中的任何位置添加一个空格。我们在破折号和小数点之间添加了空格。
月份和日期的有效数字描述
0[1-9]匹配以零开头,后跟 1 到 9 之间的任意数字。这将匹配 01、02、03、04、05、06、07、08 和 09。
1[0-2]匹配以 1 开头,后跟 0 到 2 之间的任意数字。这将匹配 10、11 或 12。
[1-9]匹配 1 到 9 之间的任意数字。
[12][0-9]匹配以 1 或 2 开头,后跟 0 到 9 之间的任意数字。这将匹配所有 10 到 29 之间的数字字符串。
3[01]匹配以 3 开头,后跟 0 或 1。这将匹配 30 或 31。
字符串的开始和结束描述
^告诉正则表达式引擎从给定字符串的开头开始匹配。
`
---
年份部分描述
(19
dd 匹配 0 到 9 之间的两个个位数。要匹配 0 到 9 之间的一个数字,您将使用 d。
有效分隔符字符集描述
[-./]匹配字符集中的任何一个字符。这些是我们的有效分隔符。要匹配空格日期分隔符,您可以将其更改为[- ./],在字符集中的任何位置添加一个空格。我们在破折号和小数点之间添加了空格。
月份和日期的有效数字描述
0[1-9]匹配以零开头,后跟 1 到 9 之间的任意数字。这将匹配 01、02、03、04、05、06、07、08 和 09。
1[0-2]匹配以 1 开头,后跟 0 到 2 之间的任意数字。这将匹配 10、11 或 12。
[1-9]匹配 1 到 9 之间的任意数字。
[12][0-9]匹配以 1 或 2 开头,后跟 0 到 9 之间的任意数字。这将匹配所有 10 到 29 之间的数字字符串。
3[01]匹配以 3 开头,后跟 0 或 1。这将匹配 30 或 31。
字符串的开始和结束描述
^告诉正则表达式引擎从给定字符串的开头开始匹配。
告诉正则表达式引擎停止匹配给定字符串的末尾。

我们创建的第一个正则表达式解释如下:

  • ^: 从字符串开头开始匹配

  • (19|20): 检查字符串是否以 19 或 20 开头

  • dd: 检查后,跟着两个 0 到 9 之间的单个数字

  • [-./]: 年份部分结束,后跟日期分隔符

  • (0[1-9]|1[0-2]): 通过查找以 0 开头的数字,后跟 1 到 9 之间的数字,以 1 开头的数字,后跟 0 到 2 之间的任意数字

  • [-./]: 月份逻辑结束,后跟日期分隔符

  • (0[1-9]|[12][0-9]|3[01]): 然后,通过查找以 0 开头的数字,后跟 1 到 9 之间的数字,或者以 1 或 2 开头的数字,后跟 0 到 9 之间的任意数字,或者匹配 3 的数字,后跟 0 到 1 之间的任意数字,找到日期逻辑

  • $: 这样做直到字符串的末尾

我们的第一个正则表达式是不正确的,因为我们的月份逻辑是错误的。我们的月份逻辑规定,通过查找以 0 开头的数字,后跟 1 到 9 之间的任意数字,或者以 1 开头的数字,后跟 0 到 2 之间的任意数字(0[1-9]|1[0-2])

然后会找到 01、02、03、04、05、06、07、08、09 或 10、11、12。它没有匹配的日期是2016.4.10(日期分隔符在这里没有区别)。这是因为我们的月份是单个数字,而我们正在寻找以零开头的月份。为了解决这个问题,我们必须修改月份逻辑的表达式,以包括只有 1 到 9 之间的单个数字。我们通过在表达式末尾添加[1-9]来实现这一点。

修改后的正则表达式如下:

  • ^: 从字符串开头开始匹配

  • (19|20): 检查字符串是否以 19 或 20 开头

  • dd: 检查后,跟着两个 0 到 9 之间的单个数字

  • [-./]: 年份部分结束,后跟日期分隔符

  • (0[1-9]|1[0-2]): 通过查找以 0 开头的数字,后跟 1 到 9 之间的任意数字,或者以 1 开头的数字,后跟 0 到 2 之间的任意数字或 1 到 9 之间的任意单个数字,找到月份逻辑

  • [-./]: 月份逻辑结束,后跟日期分隔符

  • (0[1-9]|[12][0-9]|3[01]): 然后,通过查找以 0 开头的数字,后跟 1 到 9 之间的数字,或者以 1 或 2 开头的数字,后跟 0 到 9 之间的任意数字,或者匹配 3 的数字,后跟 0 到 1 之间的任意数字,找到日期逻辑

  • $: 这样做直到字符串的末尾

这是一个基本的正则表达式,我们说基本是因为我们可以做很多事情来使表达式更好。我们可以包含逻辑来考虑替代日期格式,如 mm-dd-yyyy 或 dd-mm-yyyy。我们可以添加逻辑来检查二月,并验证它是否只包含 28 天,除非是闰年,那么我们需要允许二月的第二十九天。此外,我们还可以扩展正则表达式,以检查一月、三月、五月、七月、八月、十月和十二月是否有 31 天,而四月、六月、九月和十一月只有 30 天。

清理输入

有时,您需要清理输入。这可能是为了防止 SQL 注入或确保输入的 URL 有效。在本教程中,我们将查看如何用星号替换字符串中的不良词汇。我们确信有更优雅和代码高效的方法来使用正则表达式编写清理逻辑(特别是当我们有一个大量的黑名单词汇集合时),但我们想在这里阐明一个概念。

准备工作

确保您已将正确的程序集添加到您的类中。在您的代码文件顶部,如果尚未这样做,请添加以下行代码:

using System.Text.RegularExpressions;

如何做...

  1. 在您的RegExDemo类中创建一个名为SanitizeInput()的新方法,并让它接受一个字符串参数:
        public string SanitizeInput(string input) 
        { 

        }

  1. 在方法中添加一个List<string>类型的列表,其中包含我们要从输入中删除的不良词汇:
        List<string> lstBad = new List<string>(new string[]
        {  "BadWord1", "BadWord2", "BadWord3" });

实际上,您可能会利用数据库调用从数据库表中读取黑名单单词。您通常不会像这样硬编码它们在一个列表中。

  1. 开始构造我们将用来查找黑名单单词的正则表达式。您使用|(OR)元字符将单词连接起来,以便正则表达式将匹配任何一个单词。当列表完成后,您可以在正则表达式的两侧附加b表达式。这表示一个词边界,因此只匹配整个单词:
        string pattern = ""; 
        foreach (string badWord in lstBad) 
        pattern += pattern.Length == 0 ? $"{badWord}" 
          :  $"|{badWord}"; 

        pattern = $@"b({pattern})b";

  1. 最后,我们将添加Regex.Replace()方法,该方法接受输入并查找模式中定义的单词的出现,同时忽略大小写,并用*****替换不良单词:
        return Regex.Replace(input, pattern, "*****", 
                             RegexOptions.IgnoreCase);

  1. 完成后,您的SanitizeInput()方法将如下所示:
        public string SanitizeInput(string input) 
        { 
          List<string> lstBad = new List<string>(new string[]
          { "BadWord1", "BadWord2", "BadWord3" }); 
          string pattern = ""; 
          foreach (string badWord in lstBad) 
          pattern += pattern.Length == 0 ? $"{badWord}" : $"|{badWord}"; 

          pattern = $@"b({pattern})b"; 

          return Regex.Replace(input, pattern, "*****", 
                               RegexOptions.IgnoreCase);             
        }

  1. 在控制台应用程序中,添加以下代码调用SanitizeInput()方法并运行您的应用程序(如果您已经在上一个示例中实例化了RegExDemo的实例,则不需要再次实例化):
        string textToSanitize = "This is a string that contains a  
          badword1, another Badword2 and a third badWord3"; 
        RegExDemo oRecipe = new RegExDemo(); 
        textToSanitize = oRecipe.SanitizeInput(textToSanitize); 
        WriteLine(textToSanitize); 
        Read();

  1. 运行应用程序时,您将在控制台窗口中看到以下内容:

让我们更仔细地看一下生成的正则表达式。

工作原理...

让我们逐步了解代码的执行过程。我们需要得到一个看起来像这样的正则表达式:b(wordToMatch1|wordToMatch2|wordToMatch3)b

这基本上是说“找到任何单词,只有被b标记的整个单词”。当我们查看我们创建的列表时,我们会看到我们想要从输入字符串中删除的单词:

然后我们创建了一个简单的循环,使用 OR 元字符创建要匹配的单词列表。在foreach循环完成后,我们得到了一个BadWord1|BadWord2|BadWord3模式。然而,这仍然不是一个有效的正则表达式:

为了完成生成有效的正则表达式的模式,我们需要在模式的两侧添加b表达式,告诉正则表达式引擎只匹配整个单词。正如您所看到的,我们正在使用字符串插值。

然而,这里我们需要非常小心。首先编写代码,完成模式而不使用@符号,如下所示:

pattern = $"b({pattern})b";

如果运行控制台应用程序,您会看到不良单词没有被匹配和过滤掉。这是因为我们没有转义b之前的字符。因此,编译器解释这行代码:

生成的表达式[](BadWord1| BadWord2| BadWord3)[]不是一个有效的表达式,因此不会对输入字符串进行消毒。

要纠正这个问题,我们需要在字符串前面添加@符号,告诉编译器将字符串视为文字。这意味着任何转义序列都将被忽略。正确格式化的代码行如下:

pattern = $@"b({pattern})b";

一旦您这样做,模式的字符串将被编译器直接解释,正确的正则表达式模式将被生成:

有了我们正确的正则表达式模式,我们调用了Regex.Replace()方法。它接受要检查的输入,要匹配的正则表达式,要替换匹配单词的文本,并且可选地允许忽略大小写。

当字符串返回到控制台应用程序中的调用代码时,字符串将被正确消毒:

正则表达式可能会变得非常复杂,并且可以用于执行多种任务,以格式化和验证输入和其他文本。

动态正则表达式匹配

动态正则表达式匹配到底是什么意思?嗯,这不是一个官方术语,但这是一个我们用来解释在运行时使用变量生成特定表达式的正则表达式的术语。假设您正在开发一个需要为 ACME 公司实现文档版本管理的文档管理系统。为了做到这一点,系统验证文档是否具有有效的文件名。

一个业务规则规定,上传在特定日期的任何文件的文件名必须以acm(ACME)和今天的日期以 yyyy-mm-dd 格式为前缀。它们只能是文本文件、Word 文档(仅限.docx)和 Excel 文档(仅限.xlsx)。任何不符合此文件格式的文档都将由另一种方法处理,该方法负责存档和无效文档的处理。

您的方法需要执行的唯一任务是将新文档处理为版本一文档。

在生产系统中,可能需要进一步的逻辑来确定是否在同一天之前已经上传了相同的文档。然而,这超出了本章的范围。我们只是试图搭建场景。

准备工作

确保您已将正确的程序集添加到您的类中。如果还没有这样做,请在代码文件的顶部添加以下代码行:

using System.Text.RegularExpressions;

如何做...

  1. 一个非常好的方法是使用扩展方法。这样,您可以直接在文件名变量上调用扩展方法并进行验证。在控制台应用程序中,首先添加一个名为CustomRegexHelper的新类,带有public static修饰符:
        public static class CustomRegexHelper 
        { 

        }

  1. 将通常的扩展方法代码添加到CustomRegexHelper类中,并调用ValidAcmeCompanyFilename方法:
        public static bool ValidAcmeCompanyFilename(this string  value) 
        { 

        }

  1. 在您的ValidAcmeCompanyFilename方法中,添加以下正则表达式。我们将在本食谱的工作原理...部分解释这个正则表达式的构成:
        return Regex.IsMatch(value,  $@"^acm[_]{DateTime.Now.Year}[_]
          ({DateTime.Now.Month}|0[{DateTime.Now.Month}])[_]
          ({DateTime.Now.Day}|0[{DateTime.Now.Day}])(.txt|.docx|.xlsx)$");

  1. 完成后,您的扩展方法应该如下所示:
        public static class CustomRegexHelper 
        { 
          public static bool ValidAcmeCompanyFilename(this String value) 
          { 
            return Regex.IsMatch(value, $@"^acm[_]{DateTime.Now.Year}[_]
              ({DateTime.Now.Month}|0[{DateTime.Now.Month}])[_]
              ({DateTime.Now.Day}|0[{DateTime.Now.Day}])(.txt|.docx|.xlsx)$"); 
          } 
        }

  1. 回到控制台应用程序,在void返回类型的方法中创建名为DemoExtensionMethod()的方法:
        public static void DemoExtensionMethod() 
        { 

        }

  1. 添加一些输出文本,显示当前日期和有效的文件名类型:
        Console.WriteLine($"Today's date is: {DateTime.Now.Year}-
                          {DateTime.Now.Month}-{DateTime.Now.Day}");
        Console.WriteLine($"The file must match:  acm_{DateTime.Now.Year}
          _{DateTime.Now.Month}_{DateTime.Now.  Day}.txt including 
          leading month and day zeros");
        Console.WriteLine($"The file must match:  acm_{DateTime.Now.Year}
          _{DateTime.Now.Month}_{DateTime.Now.  Day}.docx including 
          leading month and day zeros");
        Console.WriteLine($"The file must match:  acm_{DateTime.Now.Year}
          _{DateTime.Now.Month}_{DateTime.Now.  Day}.xlsx including 
          leading month and day zeros");

  1. 然后,添加文件名检查代码:
        string filename = "acm_2016_04_10.txt"; 
        if (filename.ValidAcmeCompanyFilename()) 
          Console.WriteLine($"{filename} is a valid file name"); 
        else 
          Console.WriteLine($"{filename} is not a valid file name"); 

        filename = "acm-2016_04_10.txt"; 
        if (filename.ValidAcmeCompanyFilename()) 
          Console.WriteLine($"{filename} is a valid file name"); 
        else 
          Console.WriteLine($"{filename} is not a valid file name");

  1. 您会注意到if语句包含对包含文件名的变量的扩展方法的调用:
        filename.ValidAcmeCompanyFilename()

  1. 如果您已完成此操作,您的方法应该如下所示:
        public static void DemoExtensionMethod() 
        { 
          Console.WriteLine($"Today's date is: {DateTime.Now.Year}-
          {DateTime.Now.Month}-{DateTime.Now.Day}");    
          Console.WriteLine($"The file must match: acm_{DateTime.Now.Year}
            _{DateTime.Now.Month}_{DateTime.Now.Day}.txt including leading 
            month and day zeros");    
          Console.WriteLine($"The file must match: acm_{DateTime.Now.Year}
            _{DateTime.Now.Month}_{DateTime.Now.Day}.docx including leading
            month and day zeros");    
          Console.WriteLine($"The file must match: acm_{DateTime.Now.Year}
            _{DateTime.Now.Month}_{DateTime.Now.Day}.xlsx including leading
            month and day zeros"); 

          string filename = "acm_2016_04_10.txt"; 
          if (filename.ValidAcmeCompanyFilename()) 
            Console.WriteLine($"{filename} is a valid file name"); 
          else 
            Console.WriteLine($"{filename} is not a valid file name"); 

          filename = "acm-2016_04_10.txt"; 
          if (filename.ValidAcmeCompanyFilename()) 
            Console.WriteLine($"{filename} is a valid file name"); 
          else 
            Console.WriteLine($"{filename} is not a valid file name"); 
        }

  1. 返回到控制台应用程序,添加以下代码,简单地调用void方法。这只是为了模拟之前讨论的版本方法:
        DemoExtensionMethod();

  1. 完成后,运行您的控制台应用程序:

工作原理...

让我们更仔细地看一下生成的正则表达式。我们正在看的代码行是扩展方法中的return语句:

return Regex.IsMatch(value,  $@"^acm[_]{DateTime.Now.Year}__(.txt|.docx|.xlsx)$");

为了理解发生了什么,我们需要将这个表达式分解成不同的组件:

条件 OR 描述
&#124; 这表示OR元字符。
文件前缀和分隔符 描述
acm 文件名必须以文本acm开头。
[_] 文件名中日期组件和前缀之间唯一有效的分隔符是下划线。
日期部分 描述
{DateTime.Now.Year} 文件名的日期部分的插值年份。
{DateTime.Now.Month} 文件名的日期部分的插值月份。
0[{DateTime.Now.Month}] 文件名的日期部分的插值月份,带有前导零。
{DateTime.Now.Day} 文件名的日期部分的插值天数。
0[{DateTime.Now.Day}] 文件名的日期部分的插值天数,带有前导零。
有效文件格式 描述
(.txt&#124;.docx&#124;.xlsx) 匹配这些文件扩展名中的任何一个,用于文本文档、Word 文档或 Excel 文档。
字符串的开始和结束 描述
^ 告诉正则表达式引擎从给定字符串的开头开始匹配
` 条件 OR
--- ---
&#124; 这表示OR元字符。
文件前缀和分隔符 描述
acm 文件名必须以文本acm开头。
[_] 文件名中日期组件和前缀之间唯一有效的分隔符是下划线。
日期部分 描述
{DateTime.Now.Year} 文件名的日期部分的插值年份。
{DateTime.Now.Month} 文件名的日期部分的插值月份。
0[{DateTime.Now.Month}] 文件名的日期部分的插值月份,带有前导零。
{DateTime.Now.Day} 文件名的日期部分的插值天数。
0[{DateTime.Now.Day}] 文件名的日期部分的插值天数,带有前导零。
有效文件格式 描述
(.txt&#124;.docx&#124;.xlsx) 匹配这些文件扩展名中的任何一个,用于文本文档、Word 文档或 Excel 文档。
字符串的开始和结束 描述
^ 告诉正则表达式引擎从给定字符串的开头开始匹配
告诉正则表达式引擎停在给定字符串的末尾进行匹配

以这种方式创建正则表达式允许我们始终使其保持最新。由于我们必须始终将当前日期与正在验证的文件进行匹配,这就产生了一个独特的挑战,可以很容易地通过使用字符串插值、DateTime和正则表达式的OR语句来克服。

浏览一些更有用的正则表达式,你会发现这一章甚至还没有开始探讨可以实现的内容。还有很多东西可以探索和学习。互联网上有许多资源,还有一些免费(一些在线)和商业工具可以帮助你创建正则表达式。

第六章:处理文件、流和序列化

处理文件、流和序列化是作为开发人员您将多次进行的工作。创建导入文件,将数据导出到文件,保存应用程序状态,使用文件定义构建文件以及许多其他场景在您的职业生涯中的某个时刻都会出现。在本章中,我们将看到以下内容:

  • 创建和提取 ZIP 存档

  • 内存流压缩和解压缩

  • 异步和等待文件处理

  • 如何使自定义类型可序列化

  • 使用 ISerializable 进行自定义序列化到 FileStream

  • 使用 XmlSerializer

  • JSON 序列化器

介绍

能够处理文件肯定会让您作为开发人员具有优势。如今,开发人员可以使用许多用于处理文件的框架,以至于人们往往会忘记一些您想要的功能已经包含在.NET Framework 中。让我们看看我们可以用文件做些什么。

如果您发现自己需要在 ASP.NET 应用程序中创建 Excel 文件,请查看 CodePlex 上提供的出色 EPPlus .NET 库。在撰写本文时,URL 为:epplus.codeplex.com/,并且根据 GNU 图书馆通用公共许可证LGPL)许可。还考虑捐赠给 EPPlus。这些人编写了一个非常易于使用和文档完善的令人难以置信的库。

2017 年 3 月 31 日宣布,CodePlex 将在 2017 年 12 月 15 日完全关闭。根据 EPPlus CodePlex 页面上的 DISCUSSIONS 标签(epplus.codeplex.com/discussions/662424),源代码将在 CodePlex 在 2017 年 10 月进入只读模式之前移至 GitHub。

创建和提取 ZIP 存档

你可以做的最基本的事情之一是处理 ZIP 文件。 .NET Framework 在提供这个功能方面做得非常好。您可能需要在需要上传多个文件到网络共享的应用程序中提供 ZIP 功能。能够将多个文件压缩成一个 ZIP 文件并上传,比起上传多个较小的文件更有意义。

准备工作

执行以下步骤:

  1. 创建一个控制台应用程序,将其命名为FilesExample

  1. 右键单击“引用”节点,从上下文菜单中选择“添加引用…”:

  1. 在“引用管理器”中,搜索compression一词。将 System.IO.Compression 和 System.IO.Compression.FileSystem 引用添加到您的项目中,然后单击“确定”按钮。

在撰写本文时,引用管理器中有 System.IO.Compression 版本 4.1.0.0 和 System.IO.Compression 版本 4.0.0.0 可用。我创建的示例只使用了版本 4.1.0.0。

  1. 在添加了引用之后,您的解决方案应如下所示:

  1. 在您的temp文件夹中创建一个名为Documents的文件夹:

  1. 在这个文件夹里,创建几个不同大小的文件:

您现在可以开始编写一些代码了。

如何做…

  1. 将以下using语句添加到您的Program.cs文件的顶部:
        using System.IO;
        using System.IO.Compression;

  1. 创建一个名为ZipIt()的方法,并将代码添加到其中以压缩Documents目录。代码非常简单易懂。然而,我想强调一下CreateFromDirectory()方法的使用。请注意,我们已将压缩级别设置为CompressionLevel.Optimal,并将includeBaseDirectory参数设置为false
        private static void ZipIt(string path)
        {
          string sourceDirectory = $"{path}Documents";

          if (Directory.Exists(sourceDirectory))
          {
            string archiveName = $"{path}DocumentsArchive.zip";
            ZipFile.CreateFromDirectory(sourceDirectory, archiveName, 
                                        CompressionLevel.Optimal, false);
          } 
        }

  1. 运行控制台应用程序,再次查看temp文件夹。您将看到创建了以下 ZIP 文件:

  1. 查看 ZIP 文件的内容将显示Documents文件夹中包含的文件:

  1. 查看 ZIP 文件的属性,您将看到它已经压缩到 36 KB:

  1. 解压 ZIP 文件同样很容易。创建一个名为UnZipIt()的方法,并将路径传递给temp文件夹。然后,指定要解压缩文件的目录,并设置名为destinationDirectory的变量。调用ExtractToDirectory()方法,并将archiveNamedestinationDirectory变量作为参数传递:
        private static void UnZipIt(string path)
        {
          string destinationDirectory = $"{path}DocumentsUnzipped";

          if (Directory.Exists(path))
          {
            string archiveName = $"{path}DocumentsArchive.zip";
            ZipFile.ExtractToDirectory(archiveName, destinationDirectory);
          }
        }

  1. 运行您的控制台应用程序并查看输出文件夹:

  1. DocumentsUnzipped文件夹中查看提取的文件,您将看到我们开始时的原始文件:

工作原理...

在.NET 中使用 ZIP 文件真的非常简单。.NET Framework 为诸如创建存档等繁琐任务做了很多重活。它还允许开发人员在不必“自己动手”创建存档方法的情况下保持一定的代码标准。

内存流压缩和解压

有时,您需要对大量文本进行内存压缩。您可能希望将其写入文件或数据库。也许您需要将文本作为附件发送电子邮件,另一个系统将接收并解压缩。无论原因如何,内存压缩和解压缩都是非常有用的功能。最好的方法是使用扩展方法。如果您现在还没有想明白,我非常喜欢使用扩展方法。

准备工作

代码非常简单。您不需要太多准备工作。只需确保在您的项目中包含以下using语句,并且在以下路径C:\temp\Documents\file 3.txt有一个名为file 3.txt的包含文本的文件。您可以继续使用前面一篇文章中创建的控制台应用程序。

using System.IO.Compression;
using System.Text;
using static System.Console;

如何做...

  1. 创建一个名为ExtensionMethods的类,其中包含两个扩展方法,名为CompressStream()DecompressStream()。这两个扩展方法都将作用于字节数组并返回一个字节数组:
        public static class ExtensionMethods
        {
          public static byte[] CompressStream(this byte[] originalSource)
          {

          }

          public static byte[] DecompressStream(this byte[] originalSource)
          {

          }
        }

  1. 查看CompressStream()扩展方法,您需要创建一个新的MemoryStream以返回给调用代码。利用using语句,以便在对象移出范围时正确处理对象的释放。接下来,添加一个new GZipStream对象,它将压缩我们提供的内容到outStream对象中。您会注意到,CompressionMode.Compress作为参数传递给GZipStream对象。最后,将originalSource写入GZipStream对象,对其进行压缩并返回给调用方法:
        public static byte[] CompressStream(this byte[] originalSource)
        {
          using (var outStream = new MemoryStream())
          {
            using (var gzip = new GZipStream(outStream, 
                   CompressionMode.Compress))
            {
              gzip.Write(originalSource, 0, originalSource.Length);
            }

            return outStream.ToArray();
          } 
        }

  1. 接下来,将注意力转向DecompressStream()扩展方法。这个过程实际上非常简单。从originalSource创建一个新的MemoryStream,并将其命名为sourceStream。创建另一个名为outStreamMemoryStream以返回给调用代码。接下来,创建一个新的GZipStream对象,并将其传递给sourceStream,同时设置CompressionMode.Decompress值。将解压缩的流复制到outStream并返回给调用代码:
        public static byte[] DecompressStream(this byte[] originalSource)
        {
          using (var sourceStream = new MemoryStream(originalSource))
          {
            using (var outStream = new MemoryStream())
            {
              using (var gzip = new GZipStream(sourceStream, 
                     CompressionMode.Decompress))
             {
               gzip.CopyTo(outStream); 
             }
             return outStream.ToArray();
           }
         }
       }

  1. 我创建了一个名为InMemCompressDecompress()的方法,以说明内存压缩和解压的用法。我正在读取C:tempDocumentsfile 3.txt文件的内容到一个名为inputString的变量中。然后,我使用默认编码来获取字节,原始长度,压缩长度和解压长度。如果您想要恢复原始文本,请确保在您的代码中包含newString = Encoding.Default.GetString(newFromCompressed);这一行,并将其输出到控制台窗口。不过,需要警告一下:如果您读取了大量文本,将其显示在控制台窗口可能没有太多意义。最好将其写入文件,以检查文本是否与压缩前的文本相同:
        private static void InMemCompressDecompress()
        {
          string largeFile = @"C:\temp\Documents\file 3.txt";

          string inputString = File.ReadAllText(largeFile);
          var bytes = Encoding.Default.GetBytes(inputString);

          var originalLength = bytes.Length;
          var compressed = bytes.CompressStream();
          var compressedLength = compressed.Length;

          var newFromCompressed = compressed.DecompressStream();
          var newFromCompressedLength = newFromCompressed.Length;

          WriteLine($"Original string length = {originalLength}");
          WriteLine($"Compressed string length = {compressedLength}");
          WriteLine($"Uncompressed string length = 
                    {newFromCompressedLength}");

          // To get the original Test back, call this
          //var newString = Encoding.Default.GetString(newFromCompressed);
        }

  1. 确保在正确的目录中有一个名为File 3.txt的文件。还要确保文件包含一些文本。您可以看到,我要在内存中压缩的文件大小约为 1.8 MB:

  1. 运行控制台应用程序将显示文件的原始长度,压缩长度,然后解压长度。预期的是,解压长度与原始字符串长度相同:

工作原理...

内存压缩和解压允许开发人员在处理包含大量数据的对象时使用即时压缩和解压。例如,当您需要将日志信息读取和写入数据库时,这可能非常有用。这是.NET Framework 如何为开发人员提供了构建世界一流解决方案的完美平台的另一个例子。

异步和等待文件处理

使用异步和等待,开发人员可以在执行诸如文件处理之类的密集任务时保持其应用程序完全响应。这使得使用异步代码成为一个完美的选择。如果您有几个需要复制的大文件,异步和等待方法将是保持表单响应的完美解决方案。

准备工作

确保已将以下using语句添加到代码文件的顶部:

using System.IO;
using System.Threading;

为了使异步代码工作,我们需要包含线程命名空间。

操作步骤...

  1. 创建名为AsyncDestinationAsyncSource的两个文件夹:

  1. AsyncSource文件夹中,添加一些要处理的大文件:

  1. 创建一个新的 WinForms 应用程序,并向表单添加一个表单时间控件,一个按钮和一个名为lblTimer的标签。将计时器命名为 asyncTimer,并将其间隔设置为1000毫秒(1 秒):

  1. 在构造函数上面的代码中,将CancellationTokenSource对象和elapsedTime变量添加到Form1类中:
        CancellationTokenSource cts;
        int elapsedTime = 0;

  1. 在构造函数中,设置计时器标签文本:
        public Form1()
        {
          InitializeComponent();

          lblTimer.Text = "Timer Stopped";
        }

  1. 在按钮点击事件处理程序中,添加两个 if 条件。第一个条件将在首次点击按钮时运行。第二个条件将在再次点击按钮以取消进程时运行。请注意,这是btnCopyFileAsyncasync事件处理程序:
        private async void btnCopyFilesAsync_Click(
          object sender, EventArgs e)
        {
          if (btnCopyFilesAsync.Text.Equals("Copy Files Async"))
          {

          }

          if (btnCopyFilesAsync.Text.Equals("Cancel Async Copy"))
          {

          }
        }

  1. 为计时器添加一个Tick事件,并更新计时器标签文本:
        private void asyncTimer_Tick(object sender, EventArgs e)
        {
          lblTimer.Text = $"Duration = {elapsedTime += 1} seconds";
        }

  1. 在按钮点击事件中查看第二个if条件。将按钮文本设置回原来的内容,然后调用CancellationTokenSource对象的Cancel()方法:
        if (btnCopyFilesAsync.Text.Equals("Cancel Async Copy"))
        {
          btnCopyFilesAsync.Text = "Copy Files Async";
          cts.Cancel();

  1. 在第一个if语句中,设置源和目标目录。还要更新按钮文本,以便再次点击时运行取消逻辑。实例化CancellationTokenSource,将elapsedTime变量设置为0,然后启动计时器。现在我们可以开始枚举源文件夹中的文件,并将结果存储在fileEntries变量中:
        if (btnCopyFilesAsync.Text.Equals("Copy Files Async"))
        {
          string sourceDirectory = @"C:\temp\AsyncSource\";
          string destinationDirectory = @"C:\temp\AsyncDestination\";
          btnCopyFilesAsync.Text = "Cancel Async Copy";
          cts = new CancellationTokenSource();
          elapsedTime = 0;
          asyncTimer.Start();

          IEnumerable<string> fileEntries = Directory
            .EnumerateFiles(sourceDirectory);
        }

  1. 首先迭代源文件夹中的文件,并异步将文件从源文件夹复制到目标文件夹。这可以在代码行await sfs.CopyToAsync(dfs, 81920, cts.Token);中看到。值81920只是缓冲区大小,取消令牌cts.Token被传递给异步方法:
        foreach (string sourceFile in fileEntries)
        {
          using (FileStream sfs = File.Open(sourceFile, FileMode.Open))
          {
            string destinationFilePath = $"{destinationDirectory}{
              Path.GetFileName(sourceFile)}";
            using (FileStream dfs = File.Create(destinationFilePath))
            {
              try
              {
                await sfs.CopyToAsync(dfs, 81920, cts.Token);
              }
              catch (OperationCanceledException ex)
              {
                asyncTimer.Stop();
                lblTimer.Text = $"Cancelled after {elapsedTime} seconds";
              }
            }
          }
        }

  1. 最后,如果令牌未被取消,停止计时器并更新计时器标签:
        if (!cts.IsCancellationRequested)
        {
          asyncTimer.Stop();
          lblTimer.Text = $"Completed in {elapsedTime} seconds";
        }

  1. 将所有代码放在一起,您将看到这些如何完美地配合在一起:
        private async void btnCopyFilesAsync_Click(object sender, 
          EventArgs e)
        {
          if (btnCopyFilesAsync.Text.Equals("Copy Files Async"))
          {
            string sourceDirectory = @"C:\temp\AsyncSource\";
            string destinationDirectory = @"C:\temp\AsyncDestination\";
            btnCopyFilesAsync.Text = "Cancel Async Copy";
            cts = new CancellationTokenSource();
            elapsedTime = 0;
            asyncTimer.Start();

            IEnumerable<string> fileEntries = Directory
              .EnumerateFiles(sourceDirectory);

            //foreach (string sourceFile in Directory
                       .EnumerateFiles(sourceDirectory))
            foreach (string sourceFile in fileEntries)
            {
              using (FileStream sfs = File.Open(sourceFile, FileMode.Open))
              {
                string destinationFilePath = $"{destinationDirectory}
                {Path.GetFileName(sourceFile)}";
                using (FileStream dfs = File.Create(destinationFilePath))
                {
                  try
                  {
                    await sfs.CopyToAsync(dfs, 81920, cts.Token);
                  }
                  catch (OperationCanceledException ex)
                  {
                    asyncTimer.Stop();
                    lblTimer.Text = $"Cancelled after {elapsedTime}
                      seconds";
                  }
                }
              }
            }

            if (!cts.IsCancellationRequested)
            {
              asyncTimer.Stop();
              lblTimer.Text = $"Completed in {elapsedTime} seconds";
            }
          }
          if (btnCopyFilesAsync.Text.Equals("Cancel Async Copy"))
          {
            btnCopyFilesAsync.Text = "Copy Files Async";
            cts.Cancel();
          }
        }

工作原理...

当 Windows 窗体首次打开时,您会看到计时器标签默认为 Timer Stopped。单击“复制文件异步”按钮以开始复制过程:

当应用程序完成处理时,您会看到大文件已被复制到目标文件夹:

在复制过程运行时,您的 Windows 窗体保持活动和响应。计时器标签也继续计数。通常,对于这样的过程,窗体将无响应:

当文件复制完成时,计时器标签将显示异步复制过程的持续时间。一个有趣的实验是玩弄这段代码,看看你能够优化它以提高复制速度:

Windows 窗体不仅保持响应,而且还允许您在任何时候取消进程。当单击“复制文件异步”按钮时,文本将更改为“取消异步复制”:

单击取消按钮或将CancellationTokenSource对象设置为取消状态,这将停止异步文件复制过程。

如何使自定义类型可序列化?

序列化是将对象的状态转换为一组字节的过程(根据使用的序列化类型,可以是 XML、二进制、JSON),然后可以将其保存在流中(考虑MemoryStreamFileStream)或通过 WCF 或 Web API 进行传输。使自定义类型可序列化意味着您可以通过添加System.SerializableAttribute将序列化应用于自定义类型。以下是自定义类型的示例:

  • 类和泛型类

  • 结构体

  • 枚举

序列化的一个现实世界的例子可能是为特定对象创建一个恢复机制。想象一个工作流场景。在某个时间点,工作流的状态需要被持久化。您可以序列化该对象的状态并将其存储在数据库中。当工作流需要在将来的某个时间点继续时,您可以从数据库中读取对象并将其反序列化为与其在被持久化到数据库之前完全相同的状态。

尝试序列化一个不可序列化的类型将导致您的代码抛出SerializationException

准备就绪

如果您从控制台应用程序运行此示例,请确保控制台应用程序通过在Program.cs文件顶部添加using System来导入System命名空间。还要确保添加using System.Runtime.Serialization.Formatters.Binary

如何做...

  1. 首先添加一个名为Cat的抽象类。这个类简单地定义了WeightAge的字段。请注意,为了使您的类可序列化,您需要向其添加[Serializable]属性。
        [Serializable]
        public abstract class Cat
        {
          // fields
          public int Weight;
          public int Age; 
        }

  1. 接下来,创建一个名为Tiger的类,它是从Cat类派生的。请注意,Tiger类也必须添加[Serializable]属性。这是因为序列化不是从基类继承的。每个派生类必须自己实现序列化:
        [Serializable]
        public class Tiger : Cat
        {
          public string Trainer;
          public bool IsTamed;
        }

  1. 接下来,我们需要创建一个序列化Tiger类的方法。创建一个Tiger类型的新对象并为其设置一些值。然后,我们使用BinaryFormatterTiger类序列化为stream并将其返回给调用代码:
        private static Stream SerializeTiger()
        {
          Tiger tiger = new Tiger();
          tiger.Age = 12;
          tiger.IsTamed = false;
          tiger.Trainer = "Joe Soap";
          tiger.Weight = 120;

          MemoryStream stream = new MemoryStream();
          BinaryFormatter fmt = new BinaryFormatter();
          fmt.Serialize(stream, tiger);
          stream.Position = 0;
          return stream;
        }

  1. 反序列化更容易。我们创建一个DeserializeTiger方法并将stream传递给它。然后我们再次使用BinaryFormatterstream反序列化为Tiger类型的对象:
        private static void DeserializeTiger(Stream stream)
        {
          stream.Position = 0;
          BinaryFormatter fmt = new BinaryFormatter();
          Tiger tiger = (Tiger)fmt.Deserialize(stream);
        }

  1. 要查看序列化和反序列化的结果,请从SerializeTiger()方法中读取结果到一个新的Stream并在控制台窗口中显示它。然后,调用DeserializeTiger()方法:
        Stream str = SerializeTiger();
        WriteLine(new StreamReader(str).ReadToEnd());
        DeserializeTiger(str);

它是如何工作的...

当序列化的数据写入控制台窗口时,您将看到一些标识信息。但大部分看起来会混乱。这是因为显示的是二进制序列化数据。

当这些序列化的数据被反序列化时,它被转换回Tiger类型的对象。您可以清楚地看到序列化对象中原始字段的值是可见的。

使用 ISerializable 进行自定义序列化到 FileStream

如果您想更好地控制序列化的内容,应该在对象上实现ISerializable。这使开发人员完全控制序列化的内容。请注意,您仍然需要在对象上添加[ISerializable]属性。最后,开发人员还需要实现一个反序列化构造函数。但是,使用ISerializable确实有一个注意事项。根据 MSDN 的说法,您的对象与.NET Framework 的新版本和序列化框架的任何改进的向前兼容性可能不适用于您的对象。您还需要在对象的所有派生类型上实现ISerializable

准备工作

我们将创建一个新的类,希望使用ISerializable来控制自己的序列化。确保您的应用程序已经在using语句中添加了using System.Runtime.Serialization;

如何做...

  1. 创建一个名为Vehicle的类。您会注意到这个类实现了ISerializable,同时还有[Serializable]属性。您必须这样做,以便公共语言运行时可以识别这个类是可序列化的:
        [Serializable]
        public class Vehicle : ISerializable
        {

        }

  1. 对于这个类,添加以下字段和构造函数:
        // Primitive fields
        public int VehicleType;
        public int EngineCapacity;
        public int TopSpeed;

        public Vehicle()
        {

        }

  1. 当您在Vehicle类上实现ISerilizable时,Visual Studio 会提醒您在类内部未实现ISerializable接口。通过点击接口名称旁边的灯泡并接受更正来添加实现。Visual Studio 现在将在您的类内部添加GetObjectData()方法。请注意,如果您不在方法中添加一些代码,该方法将添加一个NotImplementedException。在这里添加非常基本的代码,只需将字段的值添加到SerializationInfo对象中:
        public void GetObjectData(SerializationInfo info, 
          StreamingContext context)
        {
          info.AddValue("VehicleType", VehicleType);
          info.AddValue("EngineCapacity", EngineCapacity);
          info.AddValue("TopSpeed", TopSpeed);
        }

  1. 如前所述,我们需要添加反序列化构造函数,用于反序列化字段。这部分需要手动添加:
        // Deserialization constructor
        protected Vehicle(SerializationInfo info, StreamingContext context)
        {
          VehicleType = info.GetInt32("VehicleType");
          EngineCapacity = info.GetInt32("EngineCapacity");
          TopSpeed = info.GetInt32("TopSpeed");
        }

  1. 在添加所有代码后,您的类应该如下所示:
        [Serializable]
        public class Vehicle : ISerializable
        {
          // Primitive fields
          public int VehicleType;
          public int EngineCapacity;
          public int TopSpeed;

          public Vehicle()
          {

          }
          public void GetObjectData(SerializationInfo info, 
            StreamingContext context)
          {
            info.AddValue("VehicleType", VehicleType);
            info.AddValue("EngineCapacity", EngineCapacity);
            info.AddValue("TopSpeed", TopSpeed);
          }

          // Deserialization constructor
          protected Vehicle(SerializationInfo info, 
            StreamingContext context)
          {
            VehicleType = info.GetInt32("VehicleType");
            EngineCapacity = info.GetInt32("EngineCapacity");
            TopSpeed = info.GetInt32("TopSpeed");
          }
        }

  1. 我们只需将序列化的类写入文件中。在本示例中,只需为文件硬编码一个输出路径。接下来,创建Vehicle类的一个新实例,并为字段设置一些值:
        string serializationPath = @"C:\temp\vehicleInfo.dat";
        Vehicle vehicle = new Vehicle();
        vehicle.VehicleType = (int)VehicleTypes.Car;
        vehicle.EngineCapacity = 1600;
        vehicle.TopSpeed = 230;

        if (File.Exists(serializationPath))
          File.Delete(serializationPath);

  1. 还要确保在类的顶部添加VehicleTypes枚举器:
        public enum VehicleTypes
        {
          Car = 1,
          SUV = 2,
          Utility = 3
        }

  1. 然后添加代码,将类序列化到硬编码路径中的文件中。为此,我们添加一个FileStream和一个BinaryFormatter对象,将vehicle序列化到文件中:
        using (FileStream stream = new FileStream(serializationPath, 
          FileMode.Create))
        {
          BinaryFormatter fmter = new BinaryFormatter();
          fmter.Serialize(stream, vehicle);
        }

  1. 最后,我们添加代码来读取包含序列化数据的文件,并创建包含Vehicle状态的Vehicle对象。虽然反序列化代码立即在序列化代码之后运行,但请注意,这只是为了演示目的。Vehicle的反序列化可以在将来的任何时间点通过从文件中读取来进行:
        using (FileStream stream = new FileStream(serializationPath, 
          FileMode.Open))
        {
          BinaryFormatter fmter = new BinaryFormatter();
          Vehicle deserializedVehicle = (Vehicle)fmter.Deserialize(stream);
        }

工作原理...

在运行代码后,您会发现vehicleInfo.dat文件已经在您指定的路径创建了:

在文本编辑器中打开文件将显示序列化信息。正如您可能注意到的那样,一些类信息仍然可见:

如果我们在反序列化代码中添加断点并检查创建的deserializedVehicle对象,您会看到Vehicle状态已经重新生成到序列化之前的状态:

使用 XmlSerializer

从名称上您可能猜到,XmlSerializer将数据序列化为 XML。它可以更好地控制序列化数据的 XML 结构。使用此序列化程序的典型实际示例是与 XML Web 服务保持兼容性。它也是在使用某种消息队列(如 MSMQ 或 RabbitMQ)传输数据时使用的一种简单介质。

XmlSerializer的默认行为是序列化公共字段和属性。使用System.Xml.Serialization命名空间中的属性,您可以控制 XML 的结构。

准备工作

由于我们将在此示例中使用List<>,请确保已添加using System.Collections.Generic;命名空间。我们还希望更多地控制 XML 的结构,因此还包括using System.Xml.Serialization;命名空间,以便我们可以使用适当的属性。最后,对于 LINQ 查询,您需要添加using System.Linq;命名空间。

如何做...

  1. 首先创建一个Student类。
        public class Student
        {
          public string StudentName;
          public double SubjectMark;
        }

  1. 接下来,创建一个名为FundamentalProgramming的主题类。已经对此类的字段应用了几个属性:
  • XmlRoot

  • XmlElement

  • XmlIgnore

  • XmlAttribute

  • XmlArray

我们可以看到XmlRoot属性指定了ElementName称为FundamentalsOfProgramming。因此,此属性定义了生成的 XML 的根。XmlElement指定了一个名为LecturerFullName的元素,而不是LecturerXmlIgnore属性将导致XmlSerializer在序列化期间忽略此字段,而XmlAttribute将在生成的 XML 的根元素上创建一个属性。最后,我们使用XmlArray属性序列化List<Student>集合:

        [XmlRoot(ElementName = "FundamentalsOfProgramming", 
          Namespace = "http://serialization")]
        public class FundamentalProgramming
        {
          [XmlElement(ElementName = "LecturerFullName", 
            DataType = "string")]
          public string Lecturer;

          [XmlIgnore]
          public double ClassAverage;

          [XmlAttribute]
          public string RoomNumber;

          [XmlArray(ElementName = "StudentsInClass", 
            Namespace = "http://serialization")]
          public List<Student> Students; 
        }

  1. 在调用代码中,设置Student对象并将它们添加到List<Student>对象students中:
        string serializationPath = @"C:tempclassInfo.xml";
        Student studentA = new Student()
        {
          StudentName = "John Smith"
          , SubjectMark = 86.4
        };
        Student studentB = new Student()
        {
          StudentName = "Jane Smith"
          , SubjectMark = 67.3
        };
        List<Student> students = new List<Student>();
        students.Add(studentA);
        students.Add(studentB);

  1. 现在我们创建FundementalProgramming类并填充字段。ClassAverage被忽略的原因是因为我们将始终计算此字段的值:
        FundamentalProgramming subject = new FundamentalProgramming();
        subject.Lecturer = "Prof. Johan van Niekerk";
        subject.RoomNumber = "Lecture Auditorium A121";
        subject.Students = students;
        subject.ClassAverage = (students.Sum(mark => mark.SubjectMark) / 
          students.Count());

  1. 添加以下代码以序列化subject对象,注意将对象类型传递给XmlSerializer作为typeof(FundamentalProgramming)
        using (FileStream stream = new FileStream(serializationPath, 
          FileMode.Create))
        {
          XmlSerializer xmlSer = new XmlSerializer(typeof(
            FundamentalProgramming));
          xmlSer.Serialize(stream, subject);
        }

  1. 最后,添加代码将 XML 反序列化回FundamentalProgramming对象:
        using (FileStream stream = new FileStream(serializationPath, 
          FileMode.Open))
        {
          XmlSerializer xmlSer = new XmlSerializer(typeof(
            FundamentalProgramming));
          FundamentalProgramming fndProg = (FundamentalProgramming)
            xmlSer.Deserialize(stream);
        }

它是如何工作的...

当您运行控制台应用程序时,您会发现它在代码中指定的路径创建了一个 XML 文档。查看此 XML 文档,您会发现 XML 元素的定义与我们在类中使用属性指定的完全相同。请注意,FundamentalsOfProgramming根元素将RoomNumber字段作为属性。字段ClassAverage已被忽略,并且不在 XML 中。最后,您可以看到List<Student>对象已经很好地序列化到 XML 文件中。

在对 XML 进行反序列化时,您会注意到序列化的值被显示。但是ClassAverage没有值,因为它从未被序列化。

JSON 序列化器

BinaryFormatter不同,JSON 序列化以人类可读的格式序列化数据。使用XmlSerializer也会产生人类可读的 XML,但是 JSON 序列化产生的数据大小比XmlSerializer小。JSON 主要用于交换数据,并且可以与许多不同的编程语言一起使用(就像 XML 一样)。

准备工作

从工具菜单中,转到 NuGet 包管理器,单击“解决方案的 NuGet 包管理器...”菜单。在“浏览”选项卡中,搜索 Newtonsoft.Json 并安装 NuGet 包。Newtonsoft.Json 是.NET 的高性能 JSON 框架。安装后,您将看到已将 Newtonsoft.Json 引用添加到您的项目中。

在类的using语句中,添加以下命名空间using Newtonsoft.Json;using Newtonsoft.Json.Linq;到您的代码中。

如何做...

  1. 首先创建我们之前用于XmlSerializerFundamentalProgrammingStudent类。这次,删除所有属性以生成以下代码:
        public class FundamentalProgramming
        {
          public string Lecturer;
          public double ClassAverage;
          public string RoomNumber;
          public List<Student> Students;
        }

        public class Student
        {
          public string StudentName;
          public double SubjectMark;
        }

  1. 在调用代码中,设置Student对象,如以前所述,并将它们添加到List<Student>中:
        string serializationPath = @"C:\temp\classInfo.txt";
        Student studentA = new Student()
        {
          StudentName = "John Smith"
          , SubjectMark = 86.4
        };
        Student studentB = new Student()
        {
          StudentName = "Jane Smith"
          , SubjectMark = 67.3
        };
        List<Student> students = new List<Student>();
        students.Add(studentA);
        students.Add(studentB);

  1. 创建类型为FundamentalProgrammingsubject对象,并为字段分配值:
        FundamentalProgramming subject = new FundamentalProgramming();
        subject.Lecturer = "Prof. Johan van Niekerk";
        subject.RoomNumber = "Lecture Auditorium A121";
        subject.Students = students;
        subject.ClassAverage = (students.Sum(mark => mark.SubjectMark) / 
          students.Count());
        WriteLine($"Calculated class average = {subject.ClassAverage}");

  1. 向您的代码添加一个JsonSerializer对象,并将格式设置为缩进。使用JsonWriter,将subject序列化到serializationPath文件classInfo.txt中:
        JsonSerializer json = new JsonSerializer();
        json.Formatting = Formatting.Indented;
        using (StreamWriter sw = new StreamWriter(serializationPath))
        {
          using (JsonWriter wr = new JsonTextWriter(sw))
          {
            json.Serialize(wr, subject);
          }
        }
        WriteLine("Serialized to file using JSON Serializer");

  1. 代码的下一部分将从之前创建的classInfo.txt文件中读取文本,并创建一个名为jobjJObject,该对象使用Newtonsoft.Json.Linq命名空间来查询 JSON 对象。使用JObject来解析从文件返回的字符串。这就是使用Newtonsoft.Json.Linq命名空间的强大之处。我可以使用 LINQ 查询jobj对象来返回学生的分数并计算平均值:
        using (StreamReader sr = new StreamReader(serializationPath))
        {
          string jsonString = sr.ReadToEnd();
          WriteLine("JSON String Read from file");
          JObject jobj = JObject.Parse(jsonString);
          IList<double> subjectMarks = jobj["Students"].Select(
            m => (double)m["SubjectMark"]).ToList();
          var ave = subjectMarks.Sum() / subjectMarks.Count();
          WriteLine($"Calculated class average using JObject = {ave}");
        }

  1. 如果需要对 JSON 对象进行反序列化,反序列化逻辑非常容易实现。我们使用JsonReader从文件中获取文本并进行反序列化:
        using (StreamReader sr = new StreamReader(serializationPath))
        {
          using (JsonReader jr = new JsonTextReader(sr))
          {
            FundamentalProgramming funProg = json.Deserialize
              <FundamentalProgramming>(jr);
          }
        }

它是如何工作的...

运行控制台应用程序后,您可以查看 JSON 序列化器创建的文件。

班级平均值计算的结果和对 JSON 对象的 LINQ 查询结果完全相同。

最后,可以在代码中添加断点并检查funProg对象,从文件中的 JSON 文本中反序列化的对象可以看到。如您所见,对象状态与序列化到文件之前的状态相同。

你还记得在本教程开始时我提到过 JSON 产生的数据比 XML 少得多吗?我创建了包含 10,000 名学生的Student类,使用 XML 和 JSON 进行了序列化。两个文件大小的比较非常惊人。显然,JSON 产生了一个更小的文件。

第七章:使用异步编程使应用程序响应

本章将向您介绍异步编程。它将涵盖以下内容:

  • 异步函数的返回类型

  • 在异步编程中处理任务

  • 异步编程中的异常处理

介绍

异步编程是 C#中的一个令人兴奋的特性。它允许您在主线程上继续程序执行,同时长时间运行的任务完成其执行。当这个长时间运行的任务完成时,来自线程池的一个线程将返回到包含该任务的方法,以便长时间运行的任务可以继续执行。学习和理解异步编程的最佳方法是亲身体验。以下示例将向您说明一些基础知识。

异步函数的返回类型

在异步编程中,async方法可以具有三种可能的返回类型。它们如下:

  • void

  • Task

  • Task<TResult>

我们将在下一个示例中查看每种返回类型。

准备工作

异步方法中void返回类型的用途是什么?通常,void与事件处理程序一起使用。只要记住void不返回任何内容,因此您无法等待它。因此,如果调用void返回类型的异步方法,您的调用代码应能够继续执行代码,而无需等待异步方法完成。

使用返回类型为Task的异步方法,您可以利用await运算符暂停当前线程的执行,直到调用的异步方法完成。请记住,返回类型为Task的异步方法基本上不返回操作数。因此,如果它被编写为同步方法,它将是一个void返回类型的方法。这个说法可能令人困惑,但在接下来的示例中将会变得清晰。

最后,具有return语句的异步方法具有TResult的返回类型。换句话说,如果异步方法返回布尔值,您将创建一个返回类型为Task<bool>的异步方法。

让我们从void返回类型的异步方法开始。

如何做...

  1. 在 Visual Studio 中创建一个名为winformAsync的新 Windows 表单项目。我们将创建一个新的 Windows 表单应用程序,以便我们可以创建一个按钮点击事件。

  2. 在 winformAsync Forms Designer 上,打开工具箱并选择按钮控件,该控件位于所有 Windows Forms 节点下:

  1. 将按钮控件拖放到 Form1 设计器上。

  2. 选择按钮控件后,双击控件以在代码后台创建点击事件。Visual Studio 将为您插入事件代码:

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

              private void button1_Click(object sender, EventArgs e) 
              { 

              } 
          } 
      }

  1. 更改button1_Click事件并在点击事件中添加async关键字。这是一个void返回异步方法的示例:
      private async void button1_Click(object sender, EventArgs e) 
      { 
      }

  1. 接下来,创建一个名为AsyncDemo的新类:
      public class AsyncDemo 
      { 
      }

  1. 要添加到AsyncDemo类的下一个方法是异步方法,该方法返回TResult(在本例中为布尔值)。此方法只是检查当前年份是否为闰年。然后将布尔值返回给调用代码:
      async Task<bool> TaskOfTResultReturning_AsyncMethod() 
      { 
          return await Task.FromResult<bool>
          (DateTime.IsLeapYear(DateTime.Now.Year)); 
      }

  1. 要添加的下一个方法是返回void的方法,该方法返回Task类型,以便您可以await该方法。该方法本身不返回任何结果,使其成为void返回方法。但是,为了使用await关键字,您需要从这个异步方法返回Task类型:
      async Task TaskReturning_AsyncMethod() 
      { 
          await Task.Delay(5000); 
          Console.WriteLine("5 second delay");     
      }

  1. 最后,添加一个方法,该方法将调用之前的异步方法并显示闰年检查的结果。您会注意到我们在两个方法调用中都使用了await关键字:
      public async Task LongTask() 
      { 
         bool isLeapYear = await TaskOfTResultReturning_AsyncMethod();    
         Console.WriteLine($"{DateTime.Now.Year} {(isLeapYear ? " is " : 
                           "  is not  ")} a leap year"); 
         await TaskReturning_AsyncMethod(); 
      }

  1. 在按钮点击事件中,添加以下代码,以异步方式调用长时间运行的任务:
      private async void button1_Click(object sender, EventArgs e) 
      { 
          Console.WriteLine("Button Clicked"); 
          AsyncDemo oAsync = new AsyncDemo(); 
          await oAsync.LongTask(); 
          Console.WriteLine("Button Click Ended"); 
      }

  1. 运行应用程序将显示 Windows 表单应用程序:

  1. 在单击 button1 按钮之前,请确保输出窗口可见。要执行此操作,请单击“查看”,然后单击“输出”。您也可以按住Ctrl + W + O

  1. 显示输出窗口将允许我们看到我们在AsyncDemo类和 Windows 应用程序中添加的Console.Writeline()输出。

  2. 单击 button1 按钮将在输出窗口中显示输出。在代码执行期间,窗体保持响应:

  1. 最后,您还可以在单独的调用中使用await运算符。修改LongTask()方法中的代码如下:
      public async Task LongTask() 
      { 
          Task<bool> blnIsLeapYear = TaskOfTResultReturning_AsyncMethod(); 

          for (int i = 0; i <= 10000; i++) 
          { 
              // Do other work that does not rely on 
              // blnIsLeapYear before awaiting 
          } 

          bool isLeapYear = await TaskOfTResultReturning_AsyncMethod();    
          Console.WriteLine($"{DateTime.Now.Year} {(isLeapYear ?      
                            " is " : "  is not  ")} a leap year"); 

          Task taskReturnMethhod = TaskReturning_AsyncMethod(); 

          for (int i = 0; i <= 10000; i++) 
          { 
              // Do other work that does not rely on 
              // taskReturnMethhod before awaiting 
          } 

          await taskReturnMethhod; 
      }

工作原理...

在前面的代码中,我们看到了void返回类型的异步方法,该方法在button1_Click事件中使用。我们还创建了一个返回Task的方法,该方法不返回任何内容(如果在同步编程中使用,将是void),但返回Task类型允许我们await该方法。最后,我们创建了一个返回Task<TResult>的方法,该方法执行任务并将结果返回给调用代码。

在异步编程中处理任务

基于任务的异步模式TAP)现在是创建异步代码的推荐方法。它在线程池中异步执行,并不在应用程序的主线程上同步执行。它允许我们通过调用Status属性来检查任务的状态。

准备工作

我们将创建一个任务来读取一个非常大的文本文件。这将通过使用异步Task来完成。确保您已将using System.IO;命名空间添加到您的 Windows 窗体应用程序中。

操作步骤...

  1. 创建一个大型文本文件(我们称之为taskFile.txt)并将其放在名为C:\temp\taskFile\的文件夹中:

  1. AsyncDemo类中,创建一个名为ReadBigFile()的方法,该方法返回一个Task<TResult>类型,该类型将用于返回从我们的大型文本文件中读取的字节数的整数:
      public Task<int> ReadBigFile() 
      {     
      }

  1. 将以下代码添加到打开和读取文件字节的代码中。您将看到我们正在使用ReadAsync()方法,该方法异步从流中读取一系列字节,并通过从该流中读取的字节数推进该流的位置。您还会注意到我们正在使用缓冲区来读取这些字节:
      public Task<int> ReadBigFile() 
      { 
          var bigFile = File.OpenRead(@"C:\temp\taskFile\taskFile.txt"); 
          var bigFileBuffer = new byte[bigFile.Length]; 
          var readBytes = bigFile.ReadAsync(bigFileBuffer, 0,
          (int)bigFile.Length); 

          return readBytes; 
      }

您可以期望从ReadAsync()方法处理的异常包括ArgumentNullExceptionArgumentOutOfRangeExceptionArgumentExceptionNotSupportedExceptionObjectDisposedExceptionInvalidOperatorException

  1. 最后,在var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, (int)bigFile.Length);行之后添加最终的代码部分,该行使用 lambda 表达式指定任务需要执行的工作。在这种情况下,它是读取文件中的字节:
      public Task<int> ReadBigFile() 
      { 
          var bigFile = File.OpenRead(@"C:temptaskFile.txt"); 
          var bigFileBuffer = new byte[bigFile.Length]; 
          var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, 
          (int)bigFile.Length); 
          readBytes.ContinueWith(task => 
          { 
              if (task.Status == TaskStatus.Running) 
                  Console.WriteLine("Running"); 
              else if (task.Status == TaskStatus.RanToCompletion) 
                  Console.WriteLine("RanToCompletion"); 
              else if (task.Status == TaskStatus.Faulted) 
                  Console.WriteLine("Faulted"); 

              bigFile.Dispose(); 
          }); 
          return readBytes; 
      }

  1. 如果您之前没有这样做,请在 Windows 窗体应用程序的 Forms Designer 中添加一个按钮。在 winformAsync Forms Designer 中,打开工具箱并选择 Button 控件,该控件位于所有 Windows 窗体节点下:

  1. 将 Button 控件拖放到 Form1 设计器上:

  2. 选择 Button 控件,双击控件以在代码后台创建单击事件。Visual Studio 将为您插入事件代码:

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

              private void button1_Click(object sender, EventArgs e) 
              { 

              } 
          } 
      }

  1. 更改button1_Click事件并在单击事件中添加async关键字。这是一个void返回的异步方法的示例:
      private async void button1_Click(object sender, EventArgs e) 
      { 

      }

  1. 现在,请确保您添加代码以异步调用AsyncDemo类的ReadBigFile()方法。记得将方法的结果(即读取的字节数)读入整数变量中:
      private async void button1_Click(object sender, EventArgs e) 
      { 
          Console.WriteLine("Start file read"); 
          AsyncDemo oAsync = new AsyncDemo(); 
          int readResult = await oAsync.ReadBigFile(); 
          Console.WriteLine("Bytes read = " + readResult); 
      }

  1. 运行您的应用程序将显示 Windows 窗体应用程序:

  1. 在单击 button1 按钮之前,请确保输出窗口可见:

  1. 从“视图”菜单中,单击“输出”菜单项,或键入Ctrl + W + O以显示“输出”窗口。这将允许我们查看我们在AsyncDemo类和 Windows 应用程序中添加的Console.Writeline()输出的内容。

  2. 单击 button1 按钮将在输出窗口中显示输出。在代码执行期间,窗体保持响应:

请注意,输出窗口中显示的信息将与屏幕截图不同。这是因为您使用的文件与我的不同。

工作原理...

任务在来自线程池的单独线程上执行。这允许应用程序在处理大文件时保持响应。任务可以以多种方式使用以改进代码。这个示例只是其中之一。

异步编程中的异常处理

异步编程中的异常处理一直是一个挑战。特别是在 catch 块中。以下功能(在 C# 6.0 中引入)允许您在异常处理程序的catchfinally块中编写异步代码。

准备工作

应用程序将模拟读取日志文件的操作。假设第三方系统总是在在另一个应用程序中处理日志文件之前备份日志文件。在进行此处理时,日志文件将被删除并重新创建。但是,我们的应用程序需要定期读取此日志文件。因此,我们需要为文件不存在于我们期望的位置的情况做好准备。因此,我们将故意省略主日志文件,以便我们可以强制出现错误。

操作步骤...

  1. 创建一个文本文件和两个文件夹来包含日志文件。但是,我们只会在BackupLog文件夹中创建一个单独的日志文件。将您的文本文件命名为taskFile.txt并将其复制到BackupLog文件夹中。MainLog文件夹将保持空白:

  1. 在我们的AsyncDemo类中,编写一个方法来读取由enum值指定的文件夹中的日志文件:
      private async Task<int> ReadLog(LogType logType)
      {
         string logFilePath = String.Empty;
         if (logType == LogType.Main)
            logFilePath = @"C:\temp\Log\MainLog\taskFile.txt";
         else if (logType == LogType.Backup)
            logFilePath = @"C:\temp\Log\BackupLog\taskFile.txt";

         string enumName = Enum.GetName(typeof(LogType), (int)logType);

         var bigFile = File.OpenRead(logFilePath);
         var bigFileBuffer = new byte[bigFile.Length];
         var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, 
         (int)bigFile.Length);
         await readBytes.ContinueWith(task =>
         {
            if (task.Status == TaskStatus.RanToCompletion)
               Console.WriteLine($"{enumName} Log RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
               Console.WriteLine($"{enumName} Log Faulted");

            bigFile.Dispose();
         });
         return await readBytes;
      }

  1. 创建如下所示的enum值:
      public enum LogType { Main = 0, Backup = 1 }

  1. 然后,我们将创建一个主ReadLogFile()方法,尝试读取主日志文件。由于我们尚未在MainLog文件夹中创建日志文件,因此代码将抛出FileNotFoundException。然后在ReadLogFile()方法的catch块中运行异步方法并await它(这在以前的 C#版本中是不可能的),将读取的字节返回给调用代码:
      public async Task<int> ReadLogFile()
      {
         int returnBytes = -1;
         try
         {
            returnBytes = await ReadLog(LogType.Main);
         }
         catch (Exception ex)
         {
            try
            {
               returnBytes = await ReadLog(LogType.Backup);
            }
            catch (Exception)
            {
               throw;
            }
         }
         return returnBytes;
      }

  1. 如果您之前没有这样做,请在 Windows 窗体应用程序的 Forms Designer 中添加一个按钮。在 winformAsync Forms Designer 中,打开工具箱并选择 Button 控件,该控件位于所有 Windows 窗体节点下:

  1. 将 Button 控件拖放到 Form1 设计器上:

  2. 选择 Button 控件后,双击控件以在代码后台创建单击事件。Visual Studio 将为您插入事件代码:

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

              private void button1_Click(object sender, EventArgs e) 
              { 

              } 
          } 
      }

  1. 更改button1_Click事件并在单击事件中添加async关键字。这是一个void返回异步方法的示例:
      private async void button1_Click(object sender, EventArgs e) 
      { 

      }

  1. 接下来,我们将编写代码来创建AsyncDemo类的新实例,并尝试读取主日志文件。在实际示例中,此时代码并不知道主日志文件不存在:
      private async void button1_Click(object sender, EventArgs  e) 
      { 
          Console.WriteLine("Read backup file");
          AsyncDemo oAsync = new AsyncDemo();
          int readResult = await oAsync.ReadLogFile();
          Console.WriteLine("Bytes read = " + readResult);
      }

  1. 运行应用程序将显示 Windows 窗体应用程序:

  1. 在单击 button1 按钮之前,请确保输出窗口可见:

  1. 从“视图”菜单中,单击“输出”菜单项,或键入Ctrl + W + O以显示“输出”窗口。这将允许我们查看我们在AsyncDemo类和 Windows 应用程序中添加的Console.Writeline()输出的内容。

  2. 为了模拟文件未找到异常,我们从MainLog文件夹中删除了文件。您会看到异常被抛出,catch块运行代码来读取备份日志文件:

工作原理...

我们可以在catchfinally块中等待的事实使开发人员拥有更大的灵活性,因为异步结果可以在整个应用程序中一致地等待。正如您从我们编写的代码中可以看到的,一旦异常被抛出,我们就会异步地读取备份文件的读取方法。

第八章:使用并行和多线程进行高性能编程

本章将介绍如何使用多线程和并行编程来提高代码的性能。在本章中,我们将介绍以下内容:

  • 创建和中止低优先级后台线程

  • 增加最大线程池大小

  • 创建多个线程

  • 锁定一个线程,直到争用的资源可用

  • 使用 Parallel.Invoke 调用方法的并行调用

  • 使用并行 foreach 循环

  • 取消并行 foreach 循环

  • 在并行 foreach 循环中捕获错误

  • 调试多个线程

介绍

如果您今天在一台计算机上找到了单核 CPU,那可能意味着您站在一个博物馆里。今天的每台新计算机都利用了多核的优势。程序员可以在自己的应用程序中利用这种额外的处理能力。随着应用程序的规模和复杂性不断增长,在许多情况下,它们实际上需要利用多线程。

虽然并非每种情况都适合实现多线程代码逻辑,但了解如何使用多线程来提高应用程序性能是很有益的。本章将带您了解 C#编程中这一激动人心的技术的基础知识。

创建和中止低优先级后台线程

我们之所以要专门研究后台线程,是因为默认情况下,由主应用程序线程或Thread类构造函数创建的所有线程都是前台线程。那么,前台线程和后台线程有什么区别呢?嗯,后台线程与前台线程相同,唯一的区别是如果所有前台线程终止,后台线程也会停止。如果您的应用程序中有一个进程不能阻止应用程序终止,这是很有用的。换句话说,在应用程序运行时,后台线程必须继续运行。

做好准备

我们将创建一个简单的应用程序,将创建的线程定义为后台线程。然后暂停、恢复和中止线程。

如何做...

  1. 在 Visual Studio 中创建一个新的控制台应用程序。

  2. 接下来,在您的控制台应用程序中添加一个名为Demo的类。

  3. Demo类中,添加一个名为DoBackgroundTask()的方法,使用public void修饰符,并将以下控制台输出添加到其中:

        public void DoBackgroundTask()
        {
          WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} has
          a threadstate of {Thread.CurrentThread.ThreadState} with
          {Thread.CurrentThread.Priority} priority");
          WriteLine($"Start thread sleep at {DateTime.Now.Second}
                    seconds");
          Thread.Sleep(3000);
          WriteLine($"End thread sleep at {DateTime.Now.Second} seconds");
        }

确保您已经在using语句中添加了System.Threadingstatic System.Consoleusing语句。

  1. 在您的控制台应用程序的void Main方法中,创建一个Demo类的新实例,并将其添加到名为backgroundThread的新线程中。将这个新创建的线程定义为后台线程,然后启动它。最后,将线程休眠 5 秒。我们需要这样做是因为我们创建了一个后台线程,它被设置为休眠 3 秒。后台线程不会阻止前台线程终止。因此,如果主应用程序线程(默认情况下是前台线程)在后台线程完成之前终止,应用程序将终止并终止后台线程:
        static void Main(string[] args)
        {
          Demo oRecipe = new Demo();
          var backgroundThread = new Thread(oRecipe.DoBackgroundTask);
          backgroundThread.IsBackground = true;
          backgroundThread.Start();
          Thread.Sleep(5000);
        }

  1. 按下F5运行您的控制台应用程序。您将看到我们已经创建了一个具有普通优先级的后台线程:

  1. 让我们修改我们的线程,并将其优先级降低到低。将以下代码添加到您的控制台应用程序中:
        backgroundThread.Priority = ThreadPriority.Lowest;

这行代码会降低线程优先级:

        Demo oRecipe = new Demo();
        var backgroundThread = new Thread(oRecipe.DoBackgroundTask);
        backgroundThread.IsBackground = true;
        backgroundThread.Priority = ThreadPriority.Lowest;
        backgroundThread.Start();
        Thread.Sleep(5000);

  1. 再次运行您的控制台应用程序。这次,您将看到线程优先级已经设置为最低优先级:

  1. 返回到您的DoBackgroundTask()方法,并在调用Thread.Sleep(3000);之前添加Thread.CurrentThread.Abort();。这行代码将过早终止后台线程。您的代码应该如下所示:
        public void DoBackgroundTask()
        {
          WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} has a
          threadstate of {Thread.CurrentThread.ThreadState} with
          {Thread.CurrentThread.Priority} priority");   
          WriteLine($"Start thread sleep at {DateTime.Now.Second} 
                    seconds");
          Thread.CurrentThread.Abort();
          Thread.Sleep(3000);
          WriteLine($"End thread sleep at {DateTime.Now.Second} seconds");
        }

  1. 当您运行控制台应用程序时,您会发现在调用Thread.Sleep方法之前线程被中止。然而,通常不建议以这种方式中止线程:

它是如何工作的...

能够创建后台线程是在与主线程不干扰主应用程序线程的情况下在不同线程上工作的好方法。另一个附加的好处是,后台线程在主应用程序线程完成后立即终止。这个过程确保您的应用程序将正常终止。

增加最大线程池大小

.NET 中的线程池位于System.Threading.ThreadPool类中。通常,人们对创建自己的线程和使用线程池进行了很多讨论。流行的观点规定,线程池应该用于短暂的工作。这是因为线程池的大小是有限的。系统中有许多其他进程将使用线程池。因此,您不希望您的应用程序占用线程池中的所有线程。

规则是您不能将最大工作线程或完成线程的数量设置为少于计算机上的处理器数量。您也不允许将最大工作线程或完成线程的数量设置为小于最小线程池大小。

准备就绪

我们将读取当前计算机上的处理器数量。然后,我们将获取线程池大小的最小和最大允许值,生成在最小和最大线程池大小之间的随机数,并设置线程池中的最大线程数。

如何做...

  1. Demo类中创建一个名为IncreaseThreadPoolSize()的新方法。

  2. 首先,添加代码以使用Environment.ProcessorCount读取当前计算机上的处理器数量:

        public class Demo
        {
          public void IncreaseThreadPoolSize()
          {
             int numberOfProcessors = Environment.ProcessorCount;
             WriteLine($"Processor Count = {numberOfProcessors}");
          }
        }

  1. 接下来,我们检索线程池中可用的最大和最小线程:
        int maxworkerThreads; 
        int maxconcurrentActiveRequests; 
        int minworkerThreads; 
        int minconcurrentActiveRequests; 
        ThreadPool.GetMinThreads(out minworkerThreads, 
          out  minconcurrentActiveRequests);
        WriteLine($"ThreadPool minimum Worker = {minworkerThreads} 
          and minimum Requests = {minconcurrentActiveRequests}");
        ThreadPool.GetMaxThreads(out maxworkerThreads, 
          out  maxconcurrentActiveRequests);
        WriteLine($"ThreadPool maximum Worker = {maxworkerThreads} 
          and maximum Requests = {maxconcurrentActiveRequests}");

  1. 然后,我们生成在线程池中最大和最小线程数之间的随机数:
        Random rndWorkers = new Random(); 
        int newMaxWorker = rndWorkers.Next(minworkerThreads, 
          maxworkerThreads);
        WriteLine($"New Max Worker Thread generated = {newMaxWorker}"); 

        Random rndConRequests = new Random(); 
        int newMaxRequests = rndConRequests.Next(
        minconcurrentActiveRequests, maxconcurrentActiveRequests);
        WriteLine($"New Max Active Requests generated = {newMaxRequests}");

  1. 现在,我们需要尝试通过调用SetMaxThreads方法设置线程池中的最大线程数,并将其设置为我们新的随机最大值,以及工作线程和完成端口线程的最大值。超过此最大数量的任何请求都将排队,直到线程池线程再次变为活动状态。如果SetMaxThreads方法成功,该方法将返回 true;否则,它将返回false。确保SetMaxThreads方法成功是一个好主意:
        bool changeSucceeded = ThreadPool.SetMaxThreads(
          newMaxWorker, newMaxRequests); 
        if (changeSucceeded) 
        { 
           WriteLine("SetMaxThreads completed"); 
           int maxworkerThreadCount; 
           int maxconcurrentActiveRequestCount; 
           ThreadPool.GetMaxThreads(out maxworkerThreadCount, 
           out maxconcurrentActiveRequestCount); 
           WriteLine($"ThreadPool Max Worker = {maxworkerThreadCount} 
           and Max Requests = {maxconcurrentActiveRequestCount}"); 
        } 
        else 
           WriteLine("SetMaxThreads failed");

工作线程是线程池中的工作线程的最大数量,而完成端口线程是线程池中异步 I/O 线程的最大数量。

  1. 当您按照列出的步骤添加了所有代码后,您的IncreaseThreadPoolSize()方法应该如下所示:
        public class Demo
        { 
          public void IncreaseThreadPoolSize() 
          { 
            int numberOfProcessors = Environment.ProcessorCount; 
            WriteLine($"Processor Count = {numberOfProcessors}"); 

            int maxworkerThreads; 
            int maxconcurrentActiveRequests; 
            int minworkerThreads; 
            int minconcurrentActiveRequests; 
            ThreadPool.GetMinThreads(out minworkerThreads, 
              out minconcurrentActiveRequests);  
            WriteLine($"ThreadPool minimum Worker = {minworkerThreads}
              and minimum Requests = {minconcurrentActiveRequests}"); 
            ThreadPool.GetMaxThreads(out maxworkerThreads, 
              out maxconcurrentActiveRequests);
            WriteLine($"ThreadPool maximum Worker = {maxworkerThreads} 
              and maximum Requests = {maxconcurrentActiveRequests}"); 

            Random rndWorkers = new Random(); 
            int newMaxWorker = rndWorkers.Next(minworkerThreads, 
              maxworkerThreads);
            WriteLine($"New Max Worker Thread generated = {newMaxWorker}"); 

            Random rndConRequests = new Random(); 
            int newMaxRequests = rndConRequests.Next(
              minconcurrentActiveRequests, 
              maxconcurrentActiveRequests);        
            WriteLine($"New Max Active Requests generated = 
                      {newMaxRequests}");

            bool changeSucceeded = ThreadPool.SetMaxThreads(
              newMaxWorker, newMaxRequests); 
            if (changeSucceeded) 
            { 
              WriteLine("SetMaxThreads completed"); 
              int maxworkerThreadCount; 
              int maxconcurrentActiveRequestCount; 
              ThreadPool.GetMaxThreads(out maxworkerThreadCount, 
                out maxconcurrentActiveRequestCount);             
              WriteLine($"ThreadPool Max Worker = {maxworkerThreadCount} 
              and Max Requests = {maxconcurrentActiveRequestCount}"); 
            } 
            else 
              WriteLine("SetMaxThreads failed"); 

          } 
        }

  1. 前往您的控制台应用程序,创建Demo类的新实例,并调用IncreaseThreadPoolSize()方法:
        Demo oRecipe = new Demo(); 
        oRecipe.IncreaseThreadPoolSize(); 
        Console.ReadLine();

  1. 最后,运行您的控制台应用程序并注意输出:

它是如何工作的...

从控制台应用程序中,我们可以看到处理器数量为2。因此,线程池线程的最小数量也等于2。然后,我们读取最大线程池大小,并生成一个在最小和最大数字之间的随机数。最后,我们将最大线程池大小设置为我们随机生成的最小和最大值。

虽然这只是一个概念验证,而不是在生产应用程序中会做的事情(将线程池设置为随机数),但它清楚地说明了开发人员设置线程池为指定值的能力。

此示例中的代码是为 32 位编译的。尝试将应用程序更改为 64 位应用程序,然后再次运行代码。看看 64 位的差异。

创建多个线程

有时,我们需要创建多个线程。然而,在我们继续之前,我们需要等待这些线程完成它们需要做的事情。对于这一点,使用任务是最合适的。

准备工作

确保在Recipes类的顶部添加using System.Threading.Tasks;语句。

如何做...

  1. 在您的Demo类中创建一个名为MultipleThreadWait()的新方法。然后,创建一个名为RunThread()的第二个方法,使用private修饰符,它以秒为参数使线程睡眠。这将模拟以可变时间做一些工作的过程:
        public class Demo 
        { 
          public void MultipleThreadWait() 
          {         

          } 

          private void RunThread(int sleepSeconds) 
          {         

          } 
        }

实际上,您可能不会调用相同的方法。您可以出于所有目的和目的,调用三个单独的方法。然而,在这里,为了简单起见,我们将调用相同的方法,但睡眠持续时间不同。

  1. 在您的MultipleThreadWait()方法中添加以下代码。您会注意到我们创建了三个任务,然后创建了三个线程。然后我们启动这三个线程,并让它们分别睡眠352秒。最后,我们调用Task.WaitAll方法等待后续执行应用程序:
        Task thread1 = Task.Factory.StartNew(() => RunThread(3)); 
        Task thread2 = Task.Factory.StartNew(() => RunThread(5)); 
        Task thread3 = Task.Factory.StartNew(() => RunThread(2)); 

        Task.WaitAll(thread1, thread2, thread3); 
        WriteLine("All tasks completed");

  1. 然后,在RunThread()方法中,我们读取当前线程 ID,然后使线程睡眠所提供的毫秒数。这只是秒数乘以1000的整数值:
        int thread
        ID = Thread.CurrentThread.ManagedThreadId; 

        WriteLine($"Sleep thread {threadID} for {sleepSeconds} 
          seconds at {DateTime.Now.Second} seconds"); 
        Thread.Sleep(sleepSeconds * 1000); 
        WriteLine($"Wake thread {threadID} at {DateTime.Now.Second} 
                  seconds");

  1. 当您完成代码后,您的Demo类应该如下所示:
        public class Demo 
        { 
          public void MultipleThreadWait() 
          { 
            Task thread1 = Task.Factory.StartNew(() => RunThread(3)); 
            Task thread2 = Task.Factory.StartNew(() => RunThread(5)); 
            Task thread3 = Task.Factory.StartNew(() => RunThread(2)); 

            Task.WaitAll(thread1, thread2, thread3); 
            WriteLine("All tasks completed"); 
          } 

          private void RunThread(int sleepSeconds) 
          { 
            int threadID = Thread.CurrentThread.ManagedThreadId; 
            WriteLine($"Sleep thread {threadID} for {sleepSeconds} 
              seconds at {DateTime.Now.Second}          seconds"); 
            Thread.Sleep(sleepSeconds * 1000); 
            WriteLine($"Wake thread {threadID} at {DateTime.Now.Second} 
                      seconds"); 
          } 
        }

  1. 最后,在您的控制台应用程序中添加一个Demo类的新实例并调用MultipleThreadWait()方法:
        Demo oRecipe = new Demo(); 
        oRecipe.MultipleThreadWait(); 
        Console.ReadLine();

  1. 运行您的控制台应用程序并查看生成的输出:

它是如何工作的...

您会注意到创建了三个线程(thread 3thread 4thread 5)。然后通过让它们睡眠不同的时间来暂停它们。每个线程唤醒后,代码会等待所有三个线程完成后才继续执行应用程序代码。

将一个线程锁定,直到有争用的资源可用

有时我们希望将特定线程的进程独占访问。我们可以使用lock关键字来实现这一点。因此,这将以线程安全的方式执行此进程。因此,当一个线程运行进程时,它将在锁定范围内获得对进程的独占访问。如果另一个线程尝试在锁定的代码内部访问进程,它将被阻塞并必须等待其轮到释放锁定。

准备工作

对于此示例,我们将使用任务。确保在您的Demo类的顶部添加using System.Threading.Tasks;语句。

如何做...

  1. Demo类中,添加一个名为threadLock的对象,并使用private修饰符。然后,添加两个名为LockThreadExample()ContendedResource()的方法,它们以秒为参数来睡眠:
        public class Demo 
        { 
          private object threadLock = new object(); 
          public void LockThreadExample() 
          {         

          } 

          private void ContendedResource(int sleepSeconds) 
          {         

          } 
        }

将要锁定的对象定义为私有是最佳实践。

  1. LockThreadExample()方法中添加三个任务。它们将创建尝试同时访问相同代码部分的线程。此代码将等待所有线程完成后才终止应用程序:
        Task thread1 = Task.Factory.StartNew(() => ContendedResource(3));
        Task thread2 = Task.Factory.StartNew(() => ContendedResource(5));
        Task thread3 = Task.Factory.StartNew(() => ContendedResource(2)); 

        Task.WaitAll(thread1, thread2, thread3); 
        WriteLine("All tasks completed");

  1. ContendedResource()方法中,使用private threadLock对象创建一个锁,然后使线程睡眠传递给方法的秒数:
        int threadID = Thread.CurrentThread.ManagedThreadId; 
        lock (threadLock) 
        { 
          WriteLine($"Locked for thread {threadID}"); 
          Thread.Sleep(sleepSeconds * 1000); 
        } 
        WriteLine($"Lock released for thread {threadID}");

  1. 回到控制台应用程序,添加以下代码来实例化一个新的Demo类并调用LockThreadExample()方法:
        Demo oRecipe = new Demo(); 
        oRecipe.LockThreadExample(); 
        Console.ReadLine();

  1. 运行控制台应用程序并查看控制台窗口中的输出信息:

它是如何工作的...

我们可以看到线程 4获得了对争用资源的独占访问。与此同时,线程 3线程 5试图访问被线程 4锁定的争用资源。这导致另外两个线程等待,直到线程 4完成并释放锁。其结果是代码按顺序执行,可以在控制台窗口输出中看到。每个线程都等待轮到自己访问资源并锁定其线程。

调用 Parallel.Invoke 并行调用方法

Parallel.Invoke允许我们并行执行任务。有时,您需要同时执行操作,并通过这样做加快处理速度。因此,您可以期望处理任务所需的总时间等于运行时间最长的进程。使用Parallel.Invoke非常容易。

准备工作

确保您已经在Demo类的顶部添加了using System.Threading.Tasks;语句。

如何做到...

  1. 首先在Demo类中创建两个方法,分别称为ParallelInvoke()PerformSomeTask(),并将秒数作为参数传递:
        public class Demo 
        { 
          public void ParallelInvoke() 
          {         

          } 

          private void PerformSomeTask(int sleepSeconds) 
          {         

          } 
        }

  1. 将以下代码添加到ParallelInvoke()方法中。这段代码将调用Paralell.Invoke来运行PerformSomeTask()方法:
        WriteLine($"Parallel.Invoke started at 
          {DateTime.Now.Second} seconds"); 
        Parallel.Invoke( 
          () => PerformSomeTask(3), 
          () => PerformSomeTask(5), 
          () => PerformSomeTask(2) 
        ); 

        WriteLine($"Parallel.Invoke completed at 
          {DateTime.Now.Second} seconds");

  1. PerformSomeTask()方法中,使线程睡眠传递给方法的秒数(通过将其乘以1000将秒转换为毫秒):
        int threadID = Thread.CurrentThread.ManagedThreadId; 
        WriteLine($"Sleep thread {threadID} for 
          {sleepSeconds}  seconds"); 
        Thread.Sleep(sleepSeconds * 1000); 
        WriteLine($"Thread {threadID} resumed");

  1. 当你添加了所有的代码后,你的Demo类应该是这样的:
        public class Demo 
        { 
          public void ParallelInvoke() 
          { 
            WriteLine($"Parallel.Invoke started at 
                      {DateTime.Now.Second} seconds"); 
            Parallel.Invoke( 
              () => PerformSomeTask(3), 
              () => PerformSomeTask(5), 
              () => PerformSomeTask(2) 
            ); 

            WriteLine($"Parallel.Invoke completed at {DateTime.Now.Second} 
                      seconds");            
          } 

          private void PerformSomeTask(int sleepSeconds) 
          {         
            int threadID = Thread.CurrentThread.ManagedThreadId; 
            WriteLine($"Sleep thread {threadID} for {sleepSeconds} 
                      seconds"); 
            Thread.Sleep(sleepSeconds * 1000); 
            WriteLine($"Thread {threadID} resumed"); 
          } 
        }

  1. 在控制台应用程序中,实例化Demo类的一个新实例,并调用ParallelInvoke()方法:
        Demo oRecipe = new Demo(); 
        oRecipe.ParallelInvoke(); 
        Console.ReadLine();

  1. 运行控制台应用程序,并查看控制台窗口中产生的输出:

它是如何工作的...

因为我们在并行运行所有这些线程,我们可以假设最长的进程将表示所有任务的总持续时间。这意味着进程的总持续时间将是 5 秒,因为最长的任务将花费 5 秒完成(我们将线程 3设置为最多睡眠 5 秒)。

正如我们所看到的,Parallel.Invoke的开始和结束之间的时间差确实是 5 秒。

使用并行 foreach 循环

不久前,在一次工作撤退期间(是的,我工作的公司真的很酷),我的同事之一格雷厄姆·鲁克向我展示了一个并行foreach循环。它确实大大加快了处理速度。但问题是,如果你处理的数据量很小或者任务很少,使用并行foreach循环就没有意义。并行foreach循环在需要进行大量处理或处理大量数据时表现出色。

准备工作

我们将首先看看并行foreach循环在哪些情况下不比标准的foreach循环表现更好。为此,我们将创建一个包含 500 个项目的小列表,只需迭代列表,将项目写入控制台窗口。

对于第二个例子,它展示了并行foreach循环的强大之处,我们将使用相同的列表,并为列表中的每个项目创建一个文件。并行foreach循环的强大和好处将在第二个例子中显而易见。您需要添加using System.Diagnostics;using System.IO;命名空间来运行这个示例。

如何做到...

  1. 首先在Demo类中创建两个方法。一个方法称为ReadCollectionForEach(),并传递一个List<string>参数。创建第二个方法称为ReadCollectionParallelForEach(),它也接受一个List<string>参数:
        public class Demo 
        { 
          public double ReadCollectionForEach(List<string> intCollection) 
          {         

          } 

          public double ReadCollectionParallelForEach(List<string> 
            intCollection) 
          {         

          } 
        }

  1. ReadCollectionForEach()方法中,添加一个标准的foreach循环,它将迭代传递给它的字符串集合,并将它找到的值写入控制台窗口。然后清除控制台窗口。使用计时器来跟踪foreach循环期间经过的总秒数:
        var timer = Stopwatch.StartNew(); 
        foreach (string integer in intCollection) 
        { 
          WriteLine(integer); 
          Clear(); 
        } 
        return timer.Elapsed.TotalSeconds;

  1. 在第二个名为ReadCollectionParallelForEach()的方法中也是如此。但是,不要使用标准的foreach循环,而是添加一个Parallel.ForEach循环。您会注意到Parallel.ForEach循环看起来略有不同。Parallel.ForEach的签名要求您传递一个可枚举的数据源(List<string> intCollection)并定义一个操作,这是为每次迭代调用的委托(integer):
        var timer = Stopwatch.StartNew(); 
        Parallel.ForEach(intCollection, integer => 
        { 
          WriteLine(integer); 
          Clear(); 
        }); 
        return timer.Elapsed.TotalSeconds;

  1. 当您添加了所有必需的代码后,您的Demo类应该如下所示:
        public class Demo 
        { 
          public double ReadCollectionForEach(List<string> intCollection) 
          {         
            var timer = Stopwatch.StartNew(); 
            foreach (string integer in intCollection) 
            { 
              WriteLine(integer); 
              Clear(); 
            } 
            return timer.Elapsed.TotalSeconds; 
          } 

          public double ReadCollectionParallelForEach(List<string> 
            intCollection) 
          {         
            var timer = Stopwatch.StartNew(); 
            Parallel.ForEach(intCollection, integer => 
            { 
              WriteLine(integer); 
              Clear(); 
            }); 
            return timer.Elapsed.TotalSeconds; 
          } 
        }

  1. 在控制台应用程序中,创建List<string>集合并将其传递给Demo类中创建的两个方法。您会注意到我们只创建了一个包含 500 个项目的集合。代码完成后,返回经过的时间(以秒为单位)并将其输出到控制台窗口:
        List<string> integerList = new List<string>(); 
        for (int i = 0; i <= 500; i++) 
        { 
          integerList.Add(i.ToString()); 
        } 
        Demo oRecipe = new Demo(); 
        double timeElapsed1 = oRecipe.ReadCollectionForEach(integerList); 
        double timeElapsed2 = oRecipe.ReadCollectionParallelForEach(
          integerList); 
        WriteLine($"foreach executed in {timeElapsed1}"); 
        WriteLine($"Parallel.ForEach executed in {timeElapsed2}");

  1. 运行您的应用程序。从显示的输出中,您将看到性能上的差异。Parallel.ForEach循环实际上花费的时间比foreach循环长:

  1. 现在让我们使用一个不同的例子。我们将创建一个处理密集型任务,并测量Parallel.ForEach循环将为我们带来的性能增益。创建两个名为CreateWriteFilesForEach()CreateWriteFilesParallelForEach()的方法,两者都以List<string>集合作为参数:
        public class Demo 
        { 
          public void CreateWriteFilesForEach(List<string> intCollection) 
          {         

          } 

          public void CreateWriteFilesParallelForEach(List<string> 
            intCollection) 
          {         

          } 
        }

  1. 将以下代码添加到CreateWriteFilesForEach()方法中。此代码启动计时器并在List<string>对象上执行标准的foreach循环。然后将经过的时间写入控制台窗口:
        WriteLine($"Start foreach File method"); 
        var timer = Stopwatch.StartNew(); 
        foreach (string integer in intCollection) 
        {     

        } 
        WriteLine($"foreach File method executed in           {timer.Elapsed.TotalSeconds} seconds");

  1. foreach循环内,添加代码来检查是否存在具有将integer值附加到filePath变量的文件名部分创建的特定名称的文件。创建文件(确保使用Dispose方法以避免在尝试写入文件时锁定文件)并向新创建的文件写入一些文本:
        string filePath =  $"C:\temp\output\ForEach_Log{integer}.txt"; 
        if (!File.Exists(filePath)) 
        { 
          File.Create(filePath).Dispose(); 
          using (StreamWriter sw = new StreamWriter(filePath, false)) 
          { 
            sw.WriteLine($"{integer}. Log file start:               {DateTime.Now.ToUniversalTime().ToString()}"); 
          } 
        }

  1. 接下来,将这段代码添加到CreateWriteFilesParallelForEach()方法中,该方法基本上执行与CreateWriteFilesForEach()方法相同的功能,但使用Parallel.ForEach循环来创建和写入文件:
        WriteLine($"Start Parallel.ForEach File method"); 
        var timer = Stopwatch.StartNew(); 
        Parallel.ForEach(intCollection, integer => 
        { 

        }); 
        WriteLine($"Parallel.ForEach File method executed in          {timer.Elapsed.TotalSeconds} seconds");

  1. Parallel.ForEach循环内添加稍作修改的文件创建代码:
        string filePath = $"C:\temp\output\ParallelForEach_Log{
          integer}.txt"; 
        if (!File.Exists(filePath)) 
        { 
          File.Create(filePath).Dispose(); 
          using (StreamWriter sw = new StreamWriter(filePath, false)) 
          { 
            sw.WriteLine($"{integer}. Log file start:               {DateTime.Now.ToUniversalTime().ToString()}"); 
          } 
        }

  1. 完成后,您的代码应该如下所示:
        public class Demo 
        { 
          public void CreateWriteFilesForEach(List<string> intCollection) 
          {         
            WriteLine($"Start foreach File method"); 
            var timer = Stopwatch.StartNew(); 
            foreach (string integer in intCollection) 
            { 
              string filePath = $"C:\temp\output\ForEach_Log{integer}.txt"; 
              if (!File.Exists(filePath)) 
              { 
                File.Create(filePath).Dispose(); 
                using (StreamWriter sw = new StreamWriter(filePath, false)) 
                { 
                    sw.WriteLine($"{integer}. Log file start:                     {DateTime.Now.ToUniversalTime().ToString()}"); 
                } 
              } 
            } 
            WriteLine($"foreach File method executed in {
                      timer.Elapsed.TotalSeconds} seconds"); 
          } 

          public void CreateWriteFilesParallelForEach(List<string> 
            intCollection) 
          {         
            WriteLine($"Start Parallel.ForEach File method"); 
            var timer = Stopwatch.StartNew(); 
            Parallel.ForEach(intCollection, integer => 
            { 
              string filePath = $"C:\temp\output\ParallelForEach_Log 
                {integer}.txt"; 
              if (!File.Exists(filePath)) 
              { 
                File.Create(filePath).Dispose(); 
                using (StreamWriter sw = new StreamWriter(filePath, false)) 
                { 
                  sw.WriteLine($"{integer}. Log file start:                     {DateTime.Now.ToUniversalTime().ToString()}"); 
                } 
              }                 
            }); 
            WriteLine($"Parallel.ForEach File method executed in             {timer.Elapsed.TotalSeconds} seconds"); 
          } 
        }

  1. 转到控制台应用程序,稍微修改List<string>对象,并将计数从500增加到1000。然后,调用在Demo类中创建的文件方法:
        List<string> integerList = new List<string>(); 
        for (int i = 0; i <= 1000; i++) 
        { 
          integerList.Add(i.ToString()); 
        } 

        Demo oRecipe = new Demo(); 
        oRecipe.CreateWriteFilesForEach(integerList); 
        oRecipe.CreateWriteFilesParallelForEach(integerList); 
        ReadLine();

  1. 最后,当您准备好时,请确保您有C:tempoutput目录,并且该目录中没有其他文件。运行您的应用程序并查看控制台窗口中的输出。这一次,我们可以看到Parallel.ForEach循环产生了巨大的差异。性能增益是巨大的,并且比标准的foreach循环提高了 47.42%的性能:

它是如何工作的...

从本教程中使用的示例中,很明显使用并行foreach循环应该仔细考虑。如果您处理的数据量相对较小或者事务不是处理密集型的,那么并行foreach循环不会对应用程序的性能产生太大的好处。在某些情况下,标准的foreach循环可能比并行foreach循环快得多。但是,如果您发现您的应用程序在处理大量数据或运行处理器密集型任务时遇到性能问题,请尝试使用并行foreach循环。它可能会让您感到惊讶。

取消并行 foreach 循环

在处理并行foreach循环时,一个明显的问题是如何根据某些条件(例如超时)提前终止循环。事实证明,并行foreach循环相当容易提前终止。

准备工作

我们将创建一个方法,该方法接受一个项目集合,并在并行foreach循环中循环遍历该集合。它还将意识到超时值,如果超过了,将终止循环并退出方法。

如何做...

  1. 首先,在Demo类中创建一个名为CancelParallelForEach()的新方法,它接受两个参数。一个是List<string>的集合,另一个是指定超时值的整数。当超过超时值时,Parallel.ForEach循环必须终止:
        public class Demo 
        { 
          public void CancelParallelForEach(List<string> intCollection, 
            int timeOut) 
          {         

          }     
        }

  1. CancelParallelForEach()方法内,添加一个计时器来跟踪经过的时间。这将向循环发出信号,超过了超时阈值,循环需要退出。创建一个定义状态的Parallel.ForEach方法。在每次迭代中,检查经过的时间是否超过了超时时间,如果超过了,就跳出循环:
        var timer = Stopwatch.StartNew(); 
        Parallel.ForEach(intCollection, (integer, state) => 
        { 
          Thread.Sleep(1000); 
          if (timer.Elapsed.Seconds > timeOut) 
          { 
            WriteLine($"Terminate thread {Thread.CurrentThread
              .ManagedThreadId}. Elapsed time {
              timer.Elapsed.Seconds} seconds"); 
            state.Break(); 
          } 
          WriteLine($"Processing item {integer} on thread           {Thread.CurrentThread.ManagedThreadId}"); 
        });

  1. 在控制台应用程序中,创建List<string>对象,并向其中添加1000个项目。使用超时值为5秒调用CancelParallelForEach()方法:
        List<string> integerList = new List<string>(); 
        for (int i = 0; i <= 1000; i++) 
        { 
          integerList.Add(i.ToString()); 
        } 

        Demo oRecipe = new Demo(); 
        oRecipe.CancelParallelForEach(integerList, 5); 
        WriteLine($"Parallel.ForEach loop terminated"); 
        ReadLine();

  1. 运行您的控制台应用程序并查看输出结果:

工作原理...

您可以从控制台窗口输出中看到,一旦经过的时间超过了超时值,就会通知并行循环在系统尽快的时机停止执行当前迭代之后的迭代。对Parallel.ForEach循环有这种控制,使开发人员能够避免无限循环,并允许用户通过单击按钮或在超时值达到时自动终止应用程序来取消循环操作。

捕获并行 foreach 循环中的错误

使用并行foreach循环时,开发人员可以将循环包装在try...catch语句中。但是需要注意,因为Parallel.ForEach会抛出AggregatedException,其中包含它在多个线程上遇到的异常。

准备工作

我们将创建一个包含一组机器 IP 地址的List<string>对象。Parallel.ForEach循环将检查 IP 地址,看看给定 IP 的另一端的机器是否在线。它通过对 IP 地址进行 ping 来实现这一点。执行Parallel.ForEach循环的方法还将获得所需最小在线机器数量作为整数值。如果未达到所需的最小在线机器数量,就会抛出异常。

如何做...

  1. Demo类中,添加一个名为CheckClientMachinesOnline()的方法,它以List<string> IP 地址集合和指定要在线的最小机器数量的整数作为参数。添加第二个名为MachineReturnedPing()的方法,它将接收一个要 ping 的 IP 地址。对于我们的目的,我们将返回false来模拟一个死机器(对 IP 地址的 ping 超时):
        public class Recipes 
        { 
          public void CheckClientMachinesOnline(List<string> ipAddresses, 
            int minimumLive) 
          {         

          }    

          private bool MachineReturnedPing(string ip)   
          {             
            return false; 
          }  
        }

  1. CheckClientMachinesOnline()方法内部,添加Parallel.ForEach循环,并创建指定并行度的ParallelOptions变量。将所有这些代码包装在try...catch语句中,并捕获AggregateException
        try 
        { 
          int machineCount = ipAddresses.Count();                 
          var options = new ParallelOptions(); 
          options.MaxDegreeOfParallelism = machineCount; 
          int deadMachines = 0; 

          Parallel.ForEach(ipAddresses, options, ip => 
          { 

          }); 
        } 
        catch (AggregateException aex) 
        { 
          WriteLine("An AggregateException has occurred"); 
          throw; 
        }

  1. Parallel.ForEach循环内,编写代码来检查机器是否在线,调用MachineReturnedPing()方法。在我们的示例中,这个方法总是返回false。您会注意到,我们通过Interlocked.Increment方法跟踪离线机器的数量。这只是一种在Parallel.ForEach循环的线程之间递增变量的方法:
        if (MachineReturnedPing(ip)) 
        { 

        } 
        else 
        {                         
          if (machineCount - Interlocked.Increment(ref deadMachines) 
              < minimumLive) 
          { 
            WriteLine($"Machines to check = {machineCount}"); 
            WriteLine($"Dead machines = {deadMachines}"); 
            WriteLine($"Minimum machines required = {minimumLive}"); 
            WriteLine($"Live Machines = {machineCount - deadMachines}"); 
            throw new Exception($"Minimum machines requirement of 
              {minimumLive} not met"); 
          } 
        }

  1. 如果你已经正确添加了所有的代码,你的Demo类将如下所示:
        public class Demo 
        { 
          public void CheckClientMachinesOnline(List<string> ipAddresses, 
            int minimumLive) 
          {         
            try 
            { 
              int machineCount = ipAddresses.Count();                 
              var options = new ParallelOptions(); 
              options.MaxDegreeOfParallelism = machineCount; 
              int deadMachines = 0; 

              Parallel.ForEach(ipAddresses, options, ip => 
              { 
                if (MachineReturnedPing(ip)) 
                { 

                } 
                else 
                {                         
                  if (machineCount - Interlocked.Increment(
                      ref deadMachines) < minimumLive) 
                  { 
                    WriteLine($"Machines to check = {machineCount}");                            
                    WriteLine($"Dead machines = {deadMachines}"); 
                    WriteLine($"Minimum machines required = 
                              {minimumLive}"); 
                    WriteLine($"Live Machines = {machineCount - 
                              deadMachines}"); 
                    throw new Exception($"Minimum machines requirement 
                                        of {minimumLive} not met"); 
                  } 
                } 
              }); 
            } 
            catch (AggregateException aex) 
            { 
              WriteLine("An AggregateException has occurred"); 
              throw; 
            } 
          }    

          private bool MachineReturnedPing(string ip) 
          {             
            return false; 
          }  
        }

  1. 在控制台应用程序中,创建List<string>对象来存储一组虚拟 IP 地址。实例化您的Demo类,并调用CheckClientMachinesOnline()方法,将 IP 地址集合和所需在线机器的最小数量传递给它:
        List<string> ipList = new List<string>(); 
        for (int i = 0; i <= 10; i++) 
        { 
          ipList.Add($"10.0.0.{i.ToString()}"); 
        } 

        try 
        { 
          Demo oRecipe = new Demo(); 
          oRecipe.CheckClientMachinesOnline(ipList, 2); 
        } 
        catch (Exception ex) 
        { 
          WriteLine(ex.InnerException.Message); 
        } 
        ReadLine();

  1. 运行应用程序并在控制台窗口中查看输出:

只需注意一点。如果启用了 Just My Code,在某些情况下,Visual Studio 会在引发异常的行上中断。它还可能会说异常未被用户代码处理。您只需按下F5继续。要防止这种情况发生,请取消选中 Tools,Options,Debugging 和 General 下的 Enable Just My Code。

工作原理...

从控制台窗口输出可以看到,未达到所需在线机器的最小数量。应用程序随后抛出了一个异常,并从Parallel.ForEach循环中捕获了它。能够处理这种并行循环中的异常对于通过处理异常来维持应用程序的稳定性至关重要。

我鼓励您尝试一下Parallel.ForEach循环,并深入研究AggregareException类的一些内部方法,以更好地理解它。

调试多个线程

在 Visual Studio 中调试多个线程是棘手的,特别是因为这些线程都在同时运行。幸运的是,作为开发人员,我们有一些可用的工具可以帮助我们更好地了解多线程应用程序中发生的情况。

做好准备

在调试多线程应用程序时,您可以通过转到 Visual Studio 中的 Debug | Windows 来访问各种窗口。

如何做...

  1. 在代码中的某个地方添加断点后,开始调试您的多线程应用程序。您可以通过转到 Visual Studio 中的 Debug | Windows 来访问各种调试窗口:

  1. 您可以访问的第一个窗口是线程窗口。通过转到 Visual Studio 中的 Debug | Windows 或键入Ctrl + DT来访问。在这里,您可以右键单击线程以进行监视和标记。如果您已经为线程命名,您将在名称列中看到这些名称。要为线程命名,请修改之前创建的LockThreadExample()方法。
        public void LockThreadExample()
        {
          Task thread1 = Task.Factory.StartNew(() => ContendedResource(3));
          Task thread2 = Task.Factory.StartNew(() => ContendedResource(5));
          Task thread3 = Task.Factory.StartNew(() => ContendedResource(2)); 

          int threadID = Thread.CurrentThread.ManagedThreadId; 
          Thread.CurrentThread.Name = $"New Thread{threadID}";

          Task.WaitAll(thread1, thread2, thread3); 
          WriteLine("All tasks completed");
        }

您还将能够在调试器中看到当前活动的线程。它将用黄色箭头标记。然后是托管 ID,这是您之前用来创建唯一线程名称的相同 ID。

位置列显示线程当前所在的方法。通过双击位置字段,线程窗口允许您查看线程的堆栈。您还可以冻结和解冻线程。冻结会停止线程执行,而解冻允许冻结的线程继续正常运行。

  1. 通过转到 Debug | Windows 或按住Ctrl + Shift + DK来访问 Tasks 窗口。要查看它的运行情况,请在您的LockThreadExample()方法中的一行上放置一个断点,该行读取Task.WaitAll(thread1, thread2, thread3);。再次调试应用程序,并查看每个线程创建的状态列。任务的状态显示了那一刻的状态,我们可以看到三个线程是 Active、Blocked 和 Scheduled:

  1. 通过转到 Visual Studio 中的 Debug | Windows 或按住Ctrl + D + S键来访问并行堆栈窗口。在这里,您可以看到任务和线程的图形视图。您可以通过在并行堆栈窗口左上角的下拉列表中进行选择来在线程和任务视图之间切换:

  1. 将选择更改为 Tasks 将显示调试会话中的当前任务:

  1. 下一个窗口,毫无疑问是我最喜欢的,就是并行监视窗口。实际上,它与 Visual Studio 中的标准监视窗口完全相同,但它可以监视应用程序中所有线程的值。您可以在并行监视中输入任何有效的 C#表达式,并在调试会话中查看那一刻的值。通过添加几个断点并在并行监视中添加表达式来尝试一下。

它是如何工作的...

能够有效地在 Visual Studio 中使用多线程应用程序的调试工具,可以更轻松地理解应用程序的结构,并帮助您识别可能的错误、瓶颈和关注的领域。

我鼓励你更多地了解可用于调试的各种窗口。

第九章:使用反应式扩展来组合基于事件的程序

本章涉及反应式扩展Rx)。为了理解 Rx,我们将涵盖以下内容:

  • 安装 Rx

  • 事件与可观察对象

  • 使用 LINQ 执行查询

  • 在 Rx 中使用调度程序

  • 调试 lambda 表达式

介绍

在日常处理 C# 应用程序开发中,您经常需要使用异步编程。您可能还需要处理许多数据源。想象一下返回当前汇率的 Web 服务,返回相关数据流的 Twitter 搜索,甚至多台计算机生成的不同事件。Rx 通过 IObserver<T> 接口提供了一个优雅的解决方案。

您使用 IObserver<T> 接口订阅事件。然后,维护 IObserver<T> 接口列表的 IObservable<T> 接口将通知它们状态的变化。实质上,Rx 将多个数据源(社交媒体、RSS 订阅、UI 事件等)粘合在一起生成数据。因此,Rx 将这些数据源汇集在一个接口中。事实上,Rx 可以被认为由三个部分组成:

  • 可观察对象:将所有这些数据流汇集并表示的接口

  • 语言集成查询LINQ):使用 LINQ 查询这些多个数据流的能力

  • 调度程序:使用调度程序参数化并发

许多人心中的疑问可能是为什么开发人员应该使用(或找到使用)Rx。以下是一些 Rx 真正有用的例子。

  • 创建具有自动完成功能的搜索。您不希望代码对搜索区域中输入的每个值执行搜索。Rx 允许您对搜索进行节流。

  • 使应用程序的用户界面更具响应性。

  • 在数据发生变化时得到通知,而不是必须轮询数据以查看变化。想象实时股票价格。

要了解 Rx 的最新信息,您可以查看 github.com/Reactive-Extensions/Rx.NET  GitHub 页面.

安装 Rx

在我们开始探索 Rx 之前,我们需要安装它。最简单的方法是使用 NuGet。

准备工作

在 Rx 的这一章中,我们不会创建一个单独的类。所有的代码都将在控制台应用程序中编写。

如何做...

  1. 创建一个控制台应用程序,然后右键单击解决方案,从上下文菜单中选择“管理解决方案的 NuGet 包...”。

  2. 在随后显示的窗口中,在搜索文本框中键入 System.Reactive 并搜索 NuGet 安装程序:

  1. 在撰写本书时,最新的稳定版本是 3.1.1。如果您有多个项目,请选择要在其上安装 Rx 的项目。鉴于我们只有一个单独的控制台应用程序,只需选择为整个项目安装 Rx。

  2. 接下来显示的屏幕是一个确认对话框,询问您确认对项目的更改。它将显示对每个项目将要进行的更改的预览。如果您对更改满意,请单击“确定”按钮。

  3. 在最后的对话框屏幕上可能会向您呈现许可协议,您需要接受。要继续,请单击“我接受”按钮。

  4. 安装完成后,您将在项目的引用节点下看到 Rx 添加的引用。具体如下:

  • System.Reactive.Core

  • System.Reactive.Interfaces

  • System.Reactive.Linq

  • System.Reactive.PlatformServices

它是如何工作的...

NuGet 绝对是向项目添加附加组件的最简单方式。从添加的引用中可以看出,System.Reactive是主要程序集。要更好地了解System.Reactive,请查看对象浏览器中的程序集。要做到这一点,请双击项目的引用选项中的任何程序集。这将显示对象浏览器:

System.Reactive.Linq包含 Rx 中的所有查询功能。您还会注意到System.Reactive.Concurrency包含所有调度程序。

事件与可观察对象

作为开发人员,我们应该都对事件非常熟悉。自从我们开始编写代码以来,大多数开发人员一直在创建事件。事实上,如果您在窗体上放置了一个按钮控件并双击按钮以创建处理按钮点击的方法,那么您已经创建了一个事件。在.NET 中,我们可以使用event关键字声明事件,通过调用它来发布事件,并通过向事件添加处理程序来订阅该事件。因此,我们有以下操作:

  • 声明

  • 发布

  • 订阅

使用 Rx,我们有一个类似的结构,我们声明一个数据流,将数据发布到该流中,并订阅它。

准备就绪

首先,我们将看看 C#中事件的工作原理。然后,我们将看到使用 Rx 的事件的工作方式,并在此过程中突出显示差异。

如何做...

  1. 在您的控制台应用程序中,添加一个名为DotNet的新类。在这个类中,添加一个名为AvailableDatatype的属性:
        public class DotNet 
        { 
          public string  AvailableDatatype { get; set; } 
        }

  1. 在主程序类中,添加一个名为types的新静态动作事件。基本上,这只是一个委托,将接收一些值;在我们的情况下,是可用的.NET 数据类型:
        class Program 
        { 
          // Static action event 
          static event Action<string> types; 

          static void Main(string[] args) 
          { 

          } 
        }

  1. void Main内,创建一个名为lstTypesList<DotNet>类。在这个列表中,添加几个DotNet类的值。在这里,我们将只添加一些.NET 中的数据类型的硬编码数据:
        List<DotNet> lstTypes = new List<DotNet>(); 
        DotNet blnTypes = new DotNet(); 
        blnTypes.AvailableDatatype = "bool"; 
        lstTypes.Add(blnTypes); 

        DotNet strTypes = new DotNet(); 
        strTypes.AvailableDatatype = "string"; 
        lstTypes.Add(strTypes); 

        DotNet intTypes = new DotNet(); 
        intTypes.AvailableDatatype = "int"; 
        lstTypes.Add(intTypes); 

        DotNet decTypes = new DotNet(); 
        decTypes.AvailableDatatype = "decimal"; 
        lstTypes.Add(decTypes);

  1. 我们的下一个任务是订阅此事件,使用一个简单地将x的值输出到控制台窗口的事件处理程序。然后,每次我们通过lstTypes列表循环时,通过添加types(lstTypes[i].AvailableDatatype);来触发事件:
        types += x => 
        { 
          Console.WriteLine(x); 
        }; 

        for (int i = 0; i <= lstTypes.Count - 1; i++) 
        { 
          types(lstTypes[i].AvailableDatatype); 
        } 

        Console.ReadLine();

实际上,在触发事件之前,我们应该始终检查事件是否为 null。只有在此检查之后,我们才应该触发事件。为简洁起见,我们在触发事件之前没有添加此检查。

  1. 当您将步骤 1 到步骤 4 的所有代码添加到控制台应用程序中时,它应该看起来像这样:
        class Program 
        { 
          // Static action event 
          static event Action<string> types; 

          static void Main(string[] args) 
          { 
            List<DotNet> lstTypes = new List<DotNet>(); 
            DotNet blnTypes = new DotNet(); 
            blnTypes.AvailableDatatype = "bool"; 
            lstTypes.Add(blnTypes); 

            DotNet strTypes = new DotNet(); 
            strTypes.AvailableDatatype = "string"; 
            lstTypes.Add(strTypes); 

            DotNet intTypes = new DotNet(); 
            intTypes.AvailableDatatype = "int"; 
            lstTypes.Add(intTypes); 

          DotNet decTypes = new DotNet(); 
            decTypes.AvailableDatatype = "decimal"; 
            lstTypes.Add(decTypes); 

            types += x => 
            { 
              Console.WriteLine(x); 
            }; 

            for (int i = 0; i <= lstTypes.Count - 1; i++) 
            { 
              types(lstTypes[i].AvailableDatatype); 
            } 

            Console.ReadLine(); 
          } 
        }

  1. 运行应用程序将使用值设置我们的列表,然后触发创建的事件以将列表的值输出到控制台窗口:

  1. 让我们看看使用 Rx 的事件的工作方式。添加一个静态的stringSubject。您可能还需要将System.Reactive.Subjects命名空间添加到您的项目中,因为Subjects位于这个单独的命名空间中:
        class Program 
        { 

            static Subject<string> obsTypes = new Subject<string>(); 

         static void Main(string[] args) 
          { 

          } 
        }

  1. 在创建DotNet列表的代码之后,我们使用+=来连接事件处理程序。这一次,我们将使用Subscribe。这是代码的IObservable部分。添加完这个之后,使用OnNext关键字触发事件。这是代码的IObserver部分。因此,当我们循环遍历我们的列表时,我们将调用OnNext来将值输出到订阅的IObservable接口:
        // IObservable 
        obsTypes.Subscribe(x => 
        { 
          Console.WriteLine(x); 
        }); 

        // IObserver 
        for (int i = 0; i <= lstTypes.Count - 1; i++) 
        { 
          obsTypes.OnNext(lstTypes[i].AvailableDatatype); 
        } 

        Console.ReadLine();

  1. 当您完成添加所有代码后,您的应用程序应该看起来像这样:
        class Program 
        {      
          static Subject<string> obsTypes = new Subject<string>(); 

          static void Main(string[] args) 
          { 
            List<DotNet> lstTypes = new List<DotNet>(); 
            DotNet blnTypes = new DotNet(); 
            blnTypes.AvailableDatatype = "bool"; 
            lstTypes.Add(blnTypes); 

            DotNet strTypes = new DotNet(); 
            strTypes.AvailableDatatype = "string"; 
            lstTypes.Add(strTypes); 

            DotNet intTypes = new DotNet(); 
            intTypes.AvailableDatatype = "int"; 
            lstTypes.Add(intTypes); 

            DotNet decTypes = new DotNet(); 
            decTypes.AvailableDatatype = "decimal"; 
            lstTypes.Add(decTypes); 

            // IObservable 
            obsTypes.Subscribe(x => 
            { 
              Console.WriteLine(x); 
            }); 

            // IObserver 
            for (int i = 0; i <= lstTypes.Count - 1; i++) 
            { 
              obsTypes.OnNext(lstTypes[i].AvailableDatatype); 
            } 

            Console.ReadLine(); 
          } 
        }

  1. 运行应用程序时,您将看到与之前相同的项目输出到控制台窗口。

它是如何工作的...

在 Rx 中,我们可以使用Subject关键字声明事件流。因此,我们有一个事件源,我们可以使用OnNext发布到该事件源。为了在控制台窗口中看到这些值,我们使用Subscribe订阅了事件流。

Rx 允许您拥有仅为发布者或仅为订阅者的对象。这是因为IObservableIObserver接口实际上是分开的。另外,请注意,在 Rx 中,observables 可以作为参数传递,作为结果返回,并存储在变量中,这使它们成为一流。

Rx 还允许您指定事件流已完成或发生错误。这确实使 Rx 与.NET 中的事件有所不同。另外,重要的是要注意,在项目中包括System.Reactive.Linq命名空间允许开发人员对Subject类型编写查询,因为SubjectIObservable接口:

这是 Rx 与.NET 中的事件有所不同的另一个功能。

使用 LINQ 执行查询

Rx 允许开发人员使用IObservable接口,该接口表示同步数据流,以使用 LINQ 编写查询。简而言之,Rx 可以被认为由三个部分组成:

  • Observables:将所有这些数据流汇集并表示的接口

  • 语言集成查询LINQ):使用 LINQ 查询这些多个数据流的能力

  • 调度程序:使用调度程序参数化并发

在本示例中,我们将更详细地查看 Rx 的 LINQ 功能。

准备就绪

由于 observables 只是数据流,我们可以使用 LINQ 对它们进行查询。在以下示例中,我们将根据 LINQ 查询将文本输出到屏幕上。

如何做...

  1. 首先向解决方案添加一个新的 Windows 表单项目。

  2. 将项目命名为winformRx,然后单击“确定”按钮:

  3. 在工具箱中,搜索 TextBox 控件并将其添加到您的表单中。

  4. 最后,在表单中添加一个标签控件:

  1. 右键单击winformRx项目,然后从上下文菜单中选择“管理 NuGet 包...”。

  2. 在搜索文本框中,输入System.Reactive以搜索 NuGet 包,然后单击“安装”按钮。

  3. Visual Studio 将要求您审查即将对项目进行的更改。单击“确定”按钮。

  4. 在安装开始之前,您可能需要点击“我接受”按钮接受许可协议。

  5. 安装完成后,如果展开项目的引用,您应该会看到新添加的引用winformRx项目:

  6. 最后,右键单击项目,并通过单击上下文菜单中的“设置为启动项目”将winformRx设置为启动项目。

  7. 通过双击 Windows 表单上的任何位置创建表单加载事件处理程序。向此表单添加Observable关键字。您会注意到该关键字立即被下划线标记。这是因为您缺少对System.Reactive的 LINQ 程序集的引用。

  8. 要添加此功能,请按Ctrl + .(句号)以显示可能的建议以解决问题。选择将using System.Reactive.Linq命名空间添加到您的项目。

  9. 继续将以下代码添加到您的表单加载事件中。基本上,您正在使用 LINQ 并告诉编译器您要从称为textBox1的表单上的文本更改事件匹配的事件模式中选择文本。完成后,添加一个订阅变量并告诉它将在表单上的标签label1中输出找到的任何文本:

        private void Form1_Load(object sender, EventArgs e) 
        { 
          var searchTerm = Observable.FromEventPattern<EventArgs>(
            textBox1, "TextChanged").Select(x => ((TextBox)x.Sender).Text); 

          searchTerm.Subscribe(trm => label1.Text = trm); 
        }

当我们向表单添加文本框和标签时,我们将控件名称保留为默认值。但是,如果您更改了默认名称,则需要指定表单上控件的名称而不是textBox1label1

  1. 单击运行按钮以运行应用程序。Windows 表单将显示文本框和标签。

  2. 注意,当您输入时,文本将输出到表单上的标签上:

  1. 让我们通过在 LINQ 语句中添加Where条件来增加一些乐趣。我们将指定text字符串只有在以句号结尾时才能选择文本。这意味着文本只会在每个完整句子之后显示在标签上。正如您所看到的,我们在这里并没有做任何特别的事情。我们只是使用标准的 LINQ 来查询我们的数据流,并将结果返回给我们的searchTerm变量:
        private void Form1_Load(object sender, EventArgs e) 
        { 
          var searchTerm = Observable.FromEventPattern<EventArgs>(
            textBox1, "TextChanged").Select(x => ((TextBox)x.Sender)
            .Text).Where(text => text.EndsWith(".")); 

          searchTerm.Subscribe(trm => label1.Text = trm); 
        }

  1. 运行您的应用程序并开始输入一行文本。您会发现在您输入时标签控件没有输出任何内容,就像在我们添加Where条件之前的上一个示例中一样:

  1. 在文本后加上一个句号并开始添加第二行文本:

  1. 您会发现只有在每个句号之后,输入的文本才会添加到标签上。因此,我们的Where条件完美地发挥作用:

它是如何工作的...

Rx 的 LINQ 方面允许开发人员构建可观察序列。以下是一些示例:

  • Observable.Empty<>: 这将返回一个空的可观察序列

  • Observable.Return<>: 这将返回一个包含单个元素的可观察序列

  • Observable.Throw<>: 这将返回一个以异常终止的可观察序列

  • Observable.Never<>: 这将返回一个持续时间无限的非终止可观察序列

在 Rx 中使用 LINQ 允许开发人员操纵和过滤数据流,以返回他们需要的内容。

在 Rx 中使用调度程序

有时,我们需要在特定时间运行IObservable订阅。想象一下需要在不同地理区域和时区的服务器之间同步事件。您可能还需要从队列中读取数据,同时保留事件发生顺序。另一个例子是执行可能需要一些时间才能完成的某种 I/O 任务。在这些情况下,调度程序非常有用。

准备工作

此外,您可以考虑在 MSDN 上阅读更多关于使用调度程序的内容。请查看msdn.microsoft.com/en-us/library/hh242963(v=vs.103).aspx.

如何做...

  1. 如果您还没有这样做,请创建一个新的 Windows 表单应用程序并将其命名为winformRx。打开表单设计器,在工具箱中搜索 TextBox 控件并将其添加到您的表单中。

  2. 接下来,在您的表单中添加一个标签控件。

  3. 双击您的 Windows 表单设计器以创建 onload 事件处理程序。在此处理程序中,添加一些代码来读取输入到文本框中的文本,并在用户停止输入 5 秒后仅显示该文本。这是使用Throttle关键字实现的。向searchTerm变量添加一个订阅,将文本输入的结果写入标签控件的文本属性:

        private void Form1_Load(object sender, EventArgs e) 
        { 
          var searchTerm = Observable.FromEventPattern<EventArgs>(
            textBox1, "TextChanged").Select(x => ((TextBox)x.Sender)
            .Text).Throttle(TimeSpan.FromMilliseconds(5000)); 

          searchTerm.Subscribe(trm => label1.Text = trm); 
        }

请注意,您可能需要在您的using语句中添加System.Reactive.Linq

  1. 运行您的应用程序并开始在文本框中输入一些文本。立即,我们将收到一个异常。这是一个跨线程违规。当尝试从后台线程更新 UI 时会发生这种情况。Observable接口正在从System.Threading运行一个计时器,这与 UI 不在同一线程上。幸运的是,有一种简单的方法可以克服这个问题。事实证明,UI 线程能力位于不同的程序集中,我们最容易通过包管理器控制台获取:

  1. 导航到视图 | 其他窗口 | 包管理器控制台以访问包管理器控制台。

  2. 输入以下命令:

      PM> Install-Package System.Reactive.Windows.Forms

这将向您的winformRx项目添加 System.Reactive.Windows.Forms.3.1.1。因此,您应该在输出中看到以下内容:成功安装'System.Reactive.Windows.Forms 3.1.1'到 winformRx

请注意,您需要确保在包管理器控制台中将默认项目选择设置为winformRx。如果您没有看到此选项,请调整包管理器控制台屏幕的宽度,直到显示该选项。这样您就可以确保该包已添加到正确的项目中。

  1. 安装完成后,在onload事件处理程序中修改您的代码,并将执行订阅的searchTerm.Subscribe(trm => label1.Text = trm);更改为以下内容:
        searchTerm.ObserveOn(new ControlScheduler(this)).Subscribe(trm => label1.Text = trm);

您会注意到我们在这里使用了ObserveOn方法。这基本上告诉编译器的是new ControlScheduler(this)中的this关键字实际上是指我们的 Windows 表单。因此,ControlScheduler将使用 Windows 表单计时器来创建更新我们的 UI 的间隔。消息发生在正确的线程上,我们不再有跨线程违规。

  1. 如果您还没有将System.Reactive.Concurrency命名空间添加到您的项目中,Visual Studio 将用波浪线下划线标出代码中的ControlScheduler行。按下Ctrl + .(句号)将允许您添加缺少的命名空间。

  2. 这意味着System.Reactive.Concurrency包含一个可以与 Windows 表单控件通信的调度程序,以便进行调度。再次运行应用程序,并开始在文本框中输入一些文本:

  1. 在我们停止输入大约 5 秒钟后,节流条件得到满足,文本被输出到我们的标签上:

工作原理...

我们需要记住的是,从我们创建的代码中,有ObserveOnSubscribe。您不应该混淆这两者。在大多数情况下,处理调度程序时,您将使用ObserveOnObserveOn方法允许您参数化OnNextOnCompletedOnError消息的运行位置。而Subscribe,我们参数化实际的订阅和取消订阅代码的运行位置。

我们还需要记住,Rx 默认使用线程计时器(System.Threading.Timer),这就是为什么我们之前遇到跨线程违规的原因。不过,正如您所看到的,我们使用调度程序来参数化使用哪个计时器。调度程序执行此操作的方式是通过公开三个组件。它们如下:

  • 调度程序执行某些操作的能力

  • 执行操作或工作的顺序

  • 允许调度程序具有时间概念的时钟

使用时钟的重要性在于它允许开发人员在远程计算机上使用定时器;例如(在您和他们之间可能存在时间差的地方),告诉他们在特定时间执行某个操作。

调试 lambda 表达式

自 Visual Studio 2015 以来,调试 lambda 表达式的能力一直存在。这是我们最喜欢的 IDE 功能的一个很棒的补充。它允许我们实时检查 lambda 表达式的结果并修改表达式以测试不同的场景。

准备就绪

我们将创建一个非常基本的 lambda 表达式,并在监视窗口中更改它以产生不同的值。

如何做...

  1. 创建一个控制台应用程序,并在控制台应用程序中添加一个名为LambdaExample的类。在这个类中添加一个名为FavThings的属性:
        public class LambdaExample
        {
          public string FavThings { get; set; }
        }

  1. 在控制台应用程序中,创建一个List<LambdaExample>对象,并将一些您喜欢的事物添加到此列表中:
        List<LambdaExample> MyFavoriteThings = new List<LambdaExample>();
        LambdaExample thing1 = new LambdaExample();
        thing1.FavThings = "Ice-cream";
        MyFavoriteThings.Add(thing1);

        LambdaExample thing2 = new LambdaExample();
        thing2.FavThings = "Summer Rain";
        MyFavoriteThings.Add(thing2);

        LambdaExample thing3 = new LambdaExample();
        thing3.FavThings = "Sunday morning snooze";
        MyFavoriteThings.Add(thing3);

  1. 然后,创建一个表达式,仅返回以字符串"Sum"开头的事物。在这里,我们显然希望看到Summer Rain作为结果:
        var filteredStuff = MyFavoriteThings.Where(feature =>         feature.FavThings.StartsWith("Sum"));

  1. 在表达式上设置断点并运行应用程序。当代码在断点处停止时,您可以复制 lambda 表达式:

  1. 将 lambda 表达式MyFavoriteThings.Where(feature => feature.FavThings.StartsWith("Sum"))粘贴到监视窗口中,并将StartsWith方法中的字符串从Sum更改为Ice。您会看到结果已经改变,现在显示一个Ice-cream字符串:

请注意,如果您正在使用 Visual Studio 2017 RC,调试 lambda 表达式可能不起作用。您可能会收到从表达式评估器中的内部错误到包含 lambda 表达式的消息的任何内容。

工作原理是这样的…

通过这种方式,我们能够轻松地更改和调试 lambda 表达式。这在 Visual Studio 2015 之前的旧版本中是不可能的。显然,在处理这些表达式时,了解这个技巧非常重要。

另一个要注意的重点是,您可以在 Visual Studio 2017 的 Immediate 窗口中执行相同的操作,以及从 lambda 表达式中固定变量。

第十章:探索.NET Core 1.1

本章将探讨.NET Core 1.1。我们将看看.NET Core 是什么,以及您可以用它做什么。我们将重点关注:

  • 在 Mac 上创建一个简单的.NET Core 应用程序并运行它

  • 创建您的第一个 ASP.NET Core 应用程序

  • 发布您的 ASP.NET Core 应用程序

介绍

最近.NET Core 引起了很多关注。有很多文章解释了.NET Core 是什么以及它的作用。简而言之,.NET Core 允许您创建在 Windows、Linux 和 macOS 上运行的跨平台应用程序。它通过利用一个.NET 标准库来实现,该库以完全相同的代码针对所有这些平台。因此,您可以使用您熟悉的语言和工具来创建应用程序。它支持 C#、VB 和 F#,甚至允许使用泛型、异步支持和 LINQ 等构造。有关.NET Core 的更多信息和文档,请访问www.microsoft.com/net/core

在 Mac 上创建一个简单的.NET Core 应用程序并运行它

我们将看看如何在 Windows 上使用 Visual Studio 2017 创建一个应用程序,然后在 Mac 上运行该应用程序。以前这种应用程序开发是不可能的,因为您无法在 Mac 上运行为 Windows 编译的代码。.NET Core 改变了这一切。

准备工作

您需要访问 Mac 才能运行您创建的应用程序。我使用的是 Mac mini(2012 年底)配备 2.5 GHz Intel Core i5 CPU,运行 macOS Sierra,内存为 4GB。

为了在 Mac 上使用您的.NET Core 应用程序,您需要做一些准备工作:

  1. 我们需要安装 Homebrew,用于获取最新版本的 OpenSSL。通过在 Spotlight 搜索中键入Terminal来打开 Mac 上的终端:

也可以通过转到www.microsoft.com/net/core#macos 在 Mac 上执行以下步骤。

  1. 将以下内容粘贴到终端提示符处,然后按Enter
        /usr/bin/ruby -e "$(curl -fsSL         https://raw.githubusercontent.com/Homebrew/install/master/install)"

  1. 如果终端要求您输入密码,请输入密码并按Enter。您在输入时将看不到任何内容。这是正常的。只需输入密码并按Enter继续。

安装 Homebrew 的要求是 Intel CPU、OS X 10.10 或更高版本、Xcode 的命令行工具CLT)以及用于安装的 Bourne 兼容 shell,如 bash 或 zsh。因此终端非常适合。

根据您的互联网连接速度以及是否已安装 Xcode 的 CLT,安装 Homebrew 的过程可能需要一些时间才能完成。完成后,终端应如下所示:

输入brew help将显示一些有用的命令:

在终端中依次运行以下命令:

  • brew update

  • brew install openssl

  • mkdir -p /usr/local/lib

  • ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/

  • ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/

然后我们需要安装.NET Code SDK。从 URL www.microsoft.com/net/core#macos 点击下载.NET Core SDK 按钮。下载完成后,点击下载的.pkg文件。点击继续按钮安装.NET Core 1.1.0 SDK:

如何做...

  1. 我们将在 Visual Studio 2017 中创建一个.NET Core 控制台应用程序。在 Visual C#模板下,选择.NET Core 和一个 Console App (.NET Core)项目:

  1. 创建控制台应用程序时,代码如下:
        using System;

        class Program
        {
          static void Main(string[] args)
          {
            Console.WriteLine("Hello World!");
          }
        }

  1. 修改代码如下:
        static void Main(string[] args)
        {
          Console.WriteLine("I can run on Windows, Linux and macOS");
          GetSystemInfo();
          Console.ReadLine();
        }

        private static void GetSystemInfo()
        {
          var osInfo = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
          Console.WriteLine($"Current OS is: {osInfo}");
        }

  1. 方法GetSystemInfo()只是返回当前操作系统,控制台应用程序当前运行的操作系统。我的应用程序的csproj文件如下:
        <Project ToolsVersion="15.0"           >
          <Import Project="$(MSBuildExtensionsPath)$(MSBuildToolsVersion)
            Microsoft.Common.props" />
            <PropertyGroup>
              <OutputType>Exe</OutputType>
              <TargetFramework>netcoreapp1.1</TargetFramework>
            </PropertyGroup>
            <ItemGroup>
              <Compile Include="***.cs" />
              <EmbeddedResource Include="***.resx" />
            </ItemGroup>
            <ItemGroup>
              <PackageReference Include="Microsoft.NETCore.App">
                <Version>1.1.0</Version>
              </PackageReference>
              <PackageReference Include="Microsoft.NET.Sdk">
                <Version>1.0.0-alpha-20161104-2</Version>
                <PrivateAssets>All</PrivateAssets>
              </PackageReference>
            </ItemGroup>
          <Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" />
        </Project>

<version>被定义为1.1.0

如果你仍在运行 Visual Studio 2017 RC,最好检查你安装的 NuGet 包,看看是否有.NET Core 版本从.NET Core 1.0 到.NET Core 1.1 的更新。

它是如何工作的...

按下F5来运行你的控制台应用程序。你会看到操作系统显示在输出中:

转到你的控制台应用程序的bin文件夹,并将文件复制到 Mac 桌面上的一个文件夹中。将该文件夹命名为consoleApp。在终端中,导航到复制文件的文件夹。你可以通过输入命令cd ./Desktop来做到这一点,然后输入ls来列出你的桌面的内容。检查你创建的文件夹是否被列出,如果是的话,在终端中输入cd ./consoleApp。再次通过输入ls来列出consoleApp文件夹的内容。在我的情况下,DLL 被称为NetCoreConsole.dll。要运行你之前编写的代码,输入dotnet NetCoreConsole.dll并按Enter

你可以看到代码正在运行,并在终端中输出文本。

如果你在安装了.NET Core SDK 后尝试运行dotnet命令时出现command not found的错误,请尝试以下操作。在终端中输入以下内容并按 Enter 键:ln -s /usr/local/share/dotnet/dotnet /usr/local/bin/,这将添加一个符号链接。这之后运行dotnet命令应该可以正常工作。

创建你的第一个 ASP.NET Core 应用程序

让我们来看看如何构建你的第一个 ASP.NET Core 应用程序。在这个教程中,我们将只创建一个非常基本的 ASP.NET Core 应用程序,并简要讨论Startup类。关于这个主题的进一步阅读是必要的,不包括在这个对 ASP.NET Core 的简要介绍中。

准备工作

首先在 Visual Studio 2017 中创建一个新项目。在 Visual C#下,选择.NET Core 节点,然后点击 ASP.NET Core Web Application.... 点击 OK:

然后你将看到项目模板选择。你可以选择创建一个空应用程序,一个 Web API(允许你创建基于 HTTP 的 API),或者一个完整的 Web 应用程序。选择空模板,确保在云中主机未被选中,然后点击 OK:

注意模板窗口允许你启用 Docker 支持。Docker 允许你在包含完整文件系统和运行应用程序所需的其他所有内容的容器中开发应用程序。这意味着你的软件无论在什么环境中都会始终以相同的方式运行。有关 Docker 的更多信息,请访问www.docker.com

当你创建了 ASP.NET Core 应用程序后,你的解决方案资源管理器将如下所示:

如果你正在运行 Visual Studio 2017 RC,你需要点击工具,NuGet 包管理器,管理解决方案的 NuGet 包...,看看是否有.NET Core 的更新。如果你使用的是.NET Core 1.01,那么应该可以通过 NuGet 获得.NET Core 1.1 的更新。让 NuGet 为你更新项目的依赖关系。在这样做之后,你必须浏览www.microsoft.com/net/download/core#/current,确保你已经在所有下载选项下选择了当前选项。下载当前的.NET Core SDK 安装程序并安装它。

此时,你可以按下Ctrl + F5来启动而不是调试,并启动你的 ASP.NET Core 应用程序。这将启动 IIS Express,这是 ASP.NET Core 应用程序的默认主机。它现在所做的唯一的事情就是显示文本 Hello World!。你已经成功创建并运行了一个 ASP.NET Core 应用程序。不要关闭你的浏览器。保持它打开:

请注意浏览器 URL 中的端口号 25608 是一个随机选择的端口。你看到的端口号很可能与书中的不同。

如何做...

  1. 在您的解决方案资源管理器中右键单击解决方案,然后单击在文件资源管理器中打开文件夹。您会注意到有一个名为src的文件夹。点击进入这个文件夹,然后点击其中的AspNetCore子文件夹:

  1. 比较 Visual Studio 中AspNetCore文件夹和解决方案资源管理器中的内容将向您展示它们几乎相同。这是因为在 ASP.NET Core 中,Windows 文件系统确定了 Visual Studio 中的解决方案:

  1. 在 Windows 文件资源管理器中,右键单击Startup.cs文件并在记事本中编辑。您将在记事本中看到以下代码:
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Threading.Tasks;
        using Microsoft.AspNetCore.Builder;
        using Microsoft.AspNetCore.Hosting;
        using Microsoft.AspNetCore.Http;
        using Microsoft.Extensions.DependencyInjection;
        using Microsoft.Extensions.Logging;

        namespace AspNetCore
        {
          public class Startup
          {
            // This method gets called by the runtime. Use this method 
               to add services to the container.
            // For more information on how to configure your application, 
               visit https://go.microsoft.com/fwlink/?LinkID=398940
            public void ConfigureServices(IServiceCollection services)
            {
            }

            // This method gets called by the runtime. Use this method 
               to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, 
              IHostingEnvironment env, ILoggerFactory loggerFactory)
            {
              loggerFactory.AddConsole();

              if (env.IsDevelopment())
              {
                app.UseDeveloperExceptionPage();
              }

              app.Run(async (context) =>
              {
                await context.Response.WriteAsync("Hello World!");
              });
            }
          }
        }

  1. 仍然在记事本中,编辑读取await context.Response.WriteAsync("Hello World!");的行,并将其更改为await context.Response.WriteAsync($"The date is {DateTime.Now.ToString("dd MMM yyyy")}");。在记事本中保存文件,然后转到浏览器并刷新。您会看到更改已在浏览器中显示,而无需我在 Visual Studio 中进行任何编辑。这是因为(如前所述)Visual Studio 使用文件系统来确定项目结构,ASP.NET Core 检测到对Startup.cs文件的更改,并自动在运行时重新编译它:

  1. 更详细地查看解决方案资源管理器,我想要强调项目中的一些文件。wwwroot文件夹将代表托管时网站的根目录。您将在这里放置静态文件,如图像、JavaScript 和 CSS 样式表文件。另一个感兴趣的文件是Startup.cs文件,它基本上取代了Global.asax文件。在Startup.cs文件中,您可以编写在 ASP.NET Core 应用程序启动时执行的代码:

工作原理

Startup.cs文件包含Startup类。ASP.NET Core 需要一个Startup类,并且默认情况下将查找此类。按照惯例,Startup类称为Startup,但如果您愿意,也可以将其命名为其他名称。如果需要重命名它,则还需要确保修改Program.cs文件,以便WebHostBuilder()指定正确的类名用于.UseStartup

public static void Main(string[] args)
{
   var host = new WebHostBuilder()
       .UseKestrel()
       .UseContentRoot(Directory.GetCurrentDirectory())
       .UseIISIntegration()
       .UseStartup<Startup>()
       .Build();

   host.Run();
}

回到Startup.cs文件中的Startup类,当您查看此类时,您将看到两种方法。这些方法是Configure()ConfigureServices()。从Configure()方法的注释中可以看出,它用于配置 HTTP 请求管道。基本上,应用程序在此处处理传入的请求,而我们的应用程序目前所做的就是为每个传入的请求显示当前日期。ConfigureServices()方法在Configure()之前调用,是可选的。它的显式目的是添加应用程序所需的任何服务。ASP.NET Core 原生支持依赖注入。这意味着我可以通过将服务注入到Startup类中的方法中来利用服务。有关 DI 的更多信息,请确保阅读docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection

发布您的 ASP.NET Core 应用程序

发布 ASP.NET Core 应用程序非常简单。我们将通过命令提示符(以管理员身份运行)发布应用程序,然后将 ASP.NET Core 应用程序发布到 Windows 服务器上的 IIS。

做好准备

您需要设置 IIS 才能执行此操作。启动“程序和功能”,然后单击“程序和功能”表单左侧的“打开或关闭 Windows 功能”。确保选择了 Internet 信息服务。选择 IIS 后,单击“确定”以打开该功能:

您还需要确保已安装了.NET Core Windows 服务器托管包,它将在 IIS 和 Kestrel 服务器之间创建反向代理。

在撰写本文时,.NET Core Windows Server Hosting 包可在以下链接找到:

docs.microsoft.com/en-us/aspnet/core/publishing/iis#install-the-net-core-windows-server-hosting-bundle

安装.NET Core Windows Server Hosting 包后,您需要重新启动 IIS:

以管理员身份打开命令提示符,输入iisreset,然后按Enter。这将停止然后启动 IIS:

如何操作...

  1. 通过以管理员身份运行命令提示符来打开命令提示符。在命令提示符中,转到项目的src\AspNetCore目录。确保您的计算机C:\驱动器的temp文件夹中有一个名为publish的文件夹,然后输入以下命令,按Enter。这将构建和发布您的项目:
        dotnet publish --output "c:temppublish" --configuration release

根据您的 ASP.NET Core 应用程序的名称,您的src文件夹下的文件夹名称将与我的不同。

  1. 应用程序发布后,您将在输出文件夹中看到发布文件以及它们的所有依赖项:

  1. 回到命令提示符,输入dotnet AspNetCore.dll来运行应用程序。请注意,如果您的 ASP.NET Core 应用程序名称不同,您将运行的 DLL 将与书中的示例不同。

现在,您可以打开浏览器,输入http://localhost:5000。这将为您显示 ASP.NET Core 应用程序:

  1. 您可以通过将发布文件复制到文件夹并在终端中输入dotnet AspNetCore.dll来在 macOS 上执行相同的操作:

然后在 Mac 上的 Safari 中,输入http://localhost:5000,然后按Enter。这将在 Safari 中加载站点:

虽然我刚刚展示了在 macOS 上运行 Safari 作为替代方案,但 ASP.NET Core 应用程序也可以在 Linux 上运行。

  1. 将应用程序发布到 IIS 也很容易。在 Visual Studio 中,右键单击解决方案资源管理器中的项目,然后从上下文菜单中单击“发布...”:

  1. 然后,您需要选择一个发布目标。有几个选项可供选择,但在本示例中,您需要选择“文件系统”选项,然后单击“确定”:

  1. 在发布屏幕中,您可以通过单击“目标位置”路径旁边的“设置...”来修改其他设置。在这里,您需要选择以发布模式进行发布。最后,单击“发布”按钮。

  1. 应用程序发布后,Visual Studio 将在输出窗口中显示结果以及您选择的发布位置:

  1. 在浏览器中,如果输入http://localhost,您将看到 IIS 的默认页面。这意味着 IIS 已经设置好了:

  1. 在 Windows 资源管理器中,浏览到C:\inetpub\wwwroot,并创建一个名为netcore的新文件夹。将 ASP.NET Core 应用程序的发布文件复制到您创建的新文件夹中。在 IIS 中,通过右键单击Sites文件夹并选择添加网站来添加一个新网站。为网站命名,并选择在物理路径设置中复制发布文件的路径。最后,将端口更改为86,因为端口80被默认网站使用,然后单击“确定”:

  1. 您将在 IIS 的 Sites 文件夹中看到已添加您的网站。在 IIS 管理器右侧面板的“浏览网站”标题下,单击“浏览*.86 (http)”:

  1. 这将在您的默认浏览器中启动 ASP.NET Core 应用程序:

操作原理...

在 Windows 上创建一个 ASP.NET Core 应用程序可以让我们在 Windows、macOS 和 Linux 上运行该应用程序。在 Windows 命令提示符或 macOS 终端中,可以轻松地通过dotnet命令独立运行它。这就是.NET Core 对应用程序开发未来如此强大的原因。您可以使用您习惯的 IDE 来开发跨平台的应用程序。关于.NET Core 还有很多需要了解的内容,您真的需要深入了解概念并了解它的能力。

第十一章:ASP.NET Core 上的 MVC 框架

本章将探讨使用 MVC 框架创建 ASP.NET Core 应用程序。上一章向您介绍了 ASP.NET Core,并且我们从本章所需的基础知识开始。如果您对 ASP.NET Core 不熟悉,请看看第十章,探索.NET Core 1.1提供了什么。我们将会看到:

  • 包括中间件及其有用之处

  • 创建控制器并使用路由

  • 呈现视图

介绍

MVC 框架的命名是根据其遵循的 MVC 设计模式而来的。MVC 代表Model-View-Controller。HTTP 请求被发送到一个控制器,然后映射到Controller类中的一个方法。在该方法内,控制器决定如何处理 HTTP 请求。然后构造一个对控制器和请求不可知的模型。模型将包含控制器需要的所有信息的逻辑。然后使用视图来显示模型中包含的信息,以构建一个 HTML 页面,该页面将在 HTTP 响应中发送回请求的客户端。

MVC 框架允许我们通过让框架的每个组件专注于一个特定的事物来分离逻辑:

  • 控制器接收 HTTP 请求并构建模型

  • 模型包含我们请求的数据并将其发送到视图

  • 视图然后从模型中包含的数据创建 HTML 页面

包括中间件及其有用之处

这个教程将向您展示如何在 ASP.NET Core 应用程序中设置中间件。ASP.NET 中间件定义了我们的应用程序如何响应接收到的任何 HTTP 请求。它还有助于控制我们的应用程序如何响应用户身份验证或错误。它还可以执行有关传入请求的日志操作。

准备工作

我们需要修改Startup类的Configure()方法中包含的代码。在 ASP.NET Core 应用程序中设置中间件就是在这里。在第十章,探索.NET Core 1.1中,我们看到我们的Configure()方法已经包含了两个中间件。第一个是一个中间件,当捕获到未处理的异常时,将显示开发人员异常页面。代码如下所示:

if (env.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}

这将显示任何错误消息,对于调试应用程序很有用。通常,此页面将包含诸如堆栈跟踪之类的信息。仅在应用程序处于开发模式时才安装。当您首次创建 ASP.NET Core 应用程序时,它处于开发模式。

第二个中间件是app.Run(),并且将始终存在于您的应用程序中。在第十章,探索.NET Core 1.1中,它将始终响应当前日期。将中间件视为门卫。所有进入应用程序的 HTTP 请求都必须通过您的中间件。

还要知道,您添加中间件的顺序很重要。在app.Run()中间件中,我们执行了context.Response.WriteAsync()。之后添加的任何中间件都不会被执行,因为处理管道在app.Run()中终止。随着我们的学习,这一点将变得更加清晰。

如何做...

  1. 您当前的 ASP.NET Core 应用程序应包含一个如下所示的Configure()方法:
        public void Configure(IApplicationBuilder app, 
          IHostingEnvironment env, 
          ILoggerFactory loggerFactory)
        {
          loggerFactory.AddConsole();

          if (env.IsDevelopment())
          {
            app.UseDeveloperExceptionPage();
          }

         app.Run(async (context) =>
         {
           await context.Response.WriteAsync($"The date is 
             {DateTime.Now.ToString("dd MMM yyyy")}");
         });
       }

  1. 从调试菜单中,单击“开始调试”或按Ctrl + F5。您将看到日期显示如下:

  1. 返回您的代码,并告诉您的应用程序显示欢迎页面中间件。您可以通过在app.Run()之前添加app.UseWelcomePage();来实现这一点。您的代码需要如下所示:
        if (env.IsDevelopment())
        {
          app.UseDeveloperExceptionPage();
        }

        app.UseWelcomePage();

        app.Run(async (context) =>
        {
          await context.Response.WriteAsync($"The date is 
            {DateTime.Now.ToString("dd MMM yyyy")}"); 
        });

  1. 保存您的Startup.cs文件并刷新您的浏览器。

  1. 现在你再也看不到屏幕上显示的日期了。这是因为欢迎页面是终止中间件,任何 HTTP 请求都不会通过它。继续修改欢迎页面中间件如下:
        app.UseWelcomePage("/hello");

  1. 如果你保存文件并刷新浏览器,你会再次在浏览器中看到日期显示。发生了什么?嗯,你刚刚告诉欢迎页面中间件只响应/hello页面的请求。

  2. 在浏览器中更改 URL 如下http://localhost:25860/hello,然后按Enter。欢迎页面再次显示。

  3. 让我们来看看UseDeveloperExceptionPage()中间件。修改app.Run()如下:

        app.Run(async (context) =>
        {
          throw new Exception("Error in app.Run()");
          await context.Response.WriteAsync($"The date is 
            {DateTime.Now.ToString("dd MMM yyyy")}"); 
        });

  1. 保存你的更改并刷新浏览器。你会看到浏览器现在显示了一个开发人员会发现非常有用的页面。它显示了堆栈信息、传入的查询、任何 cookie 以及头信息。它甚至告诉我们异常发生的行数(在Startup.cs文件的第 36 行)。UseDeveloperExceptionPage()中间件允许请求通过它传递到较低的中间件。如果发生异常,这将允许UseDeveloperExceptionPage()中间件执行其工作。正如前面提到的,中间件的放置很重要。如果我们将UseDeveloperExceptionPage()中间件放在页面的末尾,它将无法捕获任何未处理的异常。因此,在你的Configure()方法的顶部放置这个中间件是一个好主意:

  1. 让我们进一步探讨这个概念。当我们处于生产环境时,通常不希望用户看到异常页面。假设他们需要被引导到一个友好的错误页面。首先在你的应用程序的 wwwroot 中添加一个静态 HTML 页面。右键单击 wwwroot,然后从上下文菜单中选择添加、新项目:

wwwroot 是你可以提供静态页面的地方,比如 JavaScript 文件、CSS 文件、图片或静态 HTML 页面。

  1. 选择一个 HTML 页面,命名为friendlyError.html,然后点击添加。

  1. 修改friendlyError.html的 HTML 如下:
        <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8" />
            <title>Friendly Error</title>
          </head>
          <body>
            Something went wrong. Support has been notified.
          </body>
        </html>

  1. 接下来我们需要向我们的应用程序添加一个 NuGet 包,以便我们可以提供静态文件。在NuGet 包管理器中,搜索 Microsoft.AspNetCore.StaticFiles 并将其添加到应用程序中。

  2. 现在,我们需要稍微修改代码,模拟它在生产环境中运行。我们通过设置IHostingEnvironment接口的EnvironmaneName属性来实现这一点:env.EnvironmentName = EnvironmentName.Production;

  3. 然后我们需要在if (env.IsDevelopment())条件下添加一个else语句,并编写调用我们自定义静态错误页面的代码。在这里,我们将friendlyError.html文件添加到我们的DefaultFileNames()集合中,并告诉我们的应用程序我们希望在生产环境中的任何异常中使用此错误文件。最后,我们需要调用UseStaticFiles()方法告诉我们的应用程序使用静态文件。完成后,你的代码应该如下所示:

        env.EnvironmentName = EnvironmentName.Production;
        if (env.IsDevelopment())
        {
          app.UseDeveloperExceptionPage();
        }
        else
        {
          DefaultFilesOptions options = new DefaultFilesOptions();
          options.DefaultFileNames.Add("friendlyError.html");
          app.UseDefaultFiles(options);

          app.UseExceptionHandler("/friendlyError"); 
        }

        app.UseStaticFiles();

它是如何工作的...

再次按Ctrl + F5重新启动 IIS Express 并启动我们的应用程序。你会看到我们的自定义错误页面已经显示在浏览器中:

实际上,我们可能会使用控制器来做这种事情。我想在这里说明的是添加自定义默认页面的用法,并在生产环境中发生异常时显示该页面。

正如你所看到的,ASP.NET Core 中的中间件非常有用。关于这个主题有很多文档,我鼓励你在这个主题上进行进一步阅读。从微软文档开始docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware

创建控制器并使用路由

在 MVC 框架内,控制器、模型和视图需要共同工作,形成 HTTP 请求和响应循环。然而,基本的起点是根据接收到的 HTTP 请求调用正确的控制器。如果没有这样做,我们建立在 MVC 框架上的应用程序将无法工作。在 MVC 框架中,调用正确的控制器以处理 HTTP 请求的过程称为路由。

准备工作

我们可以通过查看应用程序中间件中包含的路由信息来将 HTTP 请求路由到正确的控制器。然后,中间件使用这些路由信息来查看 HTTP 请求是否需要发送到控制器。中间件将查看传入的 URL,并将其与我们提供的配置信息进行匹配。我们可以在Startup类中使用两种路由方法之一来定义这些路由信息,即:

  • 基于约定的路由

  • 基于属性的路由

本教程将探讨这些路由方法。在我们开始之前,我们需要将 ASP.NET MVC NuGet 包添加到我们的应用程序中。您现在应该对向应用程序添加 NuGet 包相当熟悉。在 NuGet 包管理器中,浏览并安装Microsoft.AspNetCore.MvcNuGet 包。这将为我们的应用程序提供新的中间件,其中之一是app.UseMvc();。这用于将 HTTP 请求映射到我们的控制器中的一个方法。修改您的Configure()方法中的代码如下:

loggerFactory.AddConsole();

if (env.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}
else
{
   DefaultFilesOptions options = new DefaultFilesOptions();
   options.DefaultFileNames.Add("friendlyError.html");
   app.UseDefaultFiles(options);

   app.UseExceptionHandler("/friendlyError"); 
}

app.UseStaticFiles();
app.UseMvc();

接下来,我们需要注册 MVC 框架所需的 MVC 服务。在ConfigureServices()中添加以下内容:

public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
}

完成后,我们已经设置了 MVC 的基本功能。

如何做...

  1. 在应用程序中添加一个名为Controllers的新文件夹:

  1. Controllers文件夹中,添加一个名为StudentController的新类。在StudentController中,添加一个名为Find()的方法。完成后,您的类将如下所示:
        public class StudentController
        {
          public string Find()
          {
            return "Found students";
          }
        }

  1. 回到Startup类,在其中添加一个名为FindController()private void方法,该方法接受一个IRouteBuilder类型的参数。确保还将using Microsoft.AspNetCore.Routing;命名空间添加到您的类中。您的方法应如下所示:
        private void FindController(IRouteBuilder route)
        {

        }

  1. Configure()方法中,将app.UseMvc();更改为app.UseMvc(FindController);

  2. 现在,我们需要告诉我们的应用程序如何查看 URL 以确定要调用哪个控制器。我们将在这里使用基于约定的路由,它使用我们定义的模板来确定要调用哪个控制器。考虑以下模板{controller}/{action}。然后,我们的应用程序将使用此模板来拆分 URL,并确定 URL 的哪一部分是控制器部分,URL 的哪一部分是操作部分。使用我们的StudentController类,方法Find()是模板所指的操作。因此,当应用程序接收到一个带有 URL/Student/Find的传入 HTTP 请求时,它将知道要查找StudentController类,并转到该控制器中的Find()方法。

我们不需要将 URL 明确命名为/StudentController/Find,因为 MVC 框架会根据约定,自动将模板中的{controller}部分中的单词Student应用Controller,以识别要查找的控制器的名称。

  1. 将路由映射添加到FindController()方法中。这告诉应用程序模板名称为默认,并且模板需要在 URL 中查找{controller}/{action}模板。您的代码现在应如下所示:
        private void FindController(IRouteBuilder route)
        {
          route.MapRoute("Default", "{controller}/{action}");
        }

  1. 将所有内容放在一起,您的Startup类将如下所示:
        public void ConfigureServices(IServiceCollection services)
        {
          services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, 
          IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
          loggerFactory.AddConsole();

          if (env.IsDevelopment())
         {
           app.UseDeveloperExceptionPage();
         }
         else
         {
           DefaultFilesOptions options = new DefaultFilesOptions();
           options.DefaultFileNames.Add("friendlyError.html");
           app.UseDefaultFiles(options);

           app.UseExceptionHandler("/friendlyError"); 
         }

         app.UseStaticFiles();
         app.UseMvc(FindController);
       }

       private void FindController(IRouteBuilder route)
       {
         route.MapRoute("Default", "{controller}/{action}");
       }

  1. 保存您的代码并在浏览器中的 URL 末尾输入以下内容:/student/find。我的 URL 如下,但您的可能会有所不同,因为端口号很可能与我的不同:http://localhost:25860/student/find。在浏览器中输入这个将把传入的 HTTP 请求路由到正确的控制器。

  1. 然而,如果 URL 格式不正确或找不到控制器,我们应该怎么办呢?这就是我们可以向我们的模板添加默认值的地方。删除 URL 中的/student/find部分并输入。现在您应该在浏览器中看到错误 404。这是因为应用程序无法根据我们的 URL 找到控制器。在我们的Controllers文件夹中添加另一个类。将此类命名为ErrorController。然后,在此控制器内创建一个名为Support()的方法。您的代码应如下所示:
        public class ErrorController
        {
          public string Support()
          {
            return "Content not found. Contact Support";
          }
        }

  1. 回到Startup类,在FindController()方法中修改模板。它应如下所示:
        route.MapRoute("Default", "{controller=Error}/{action=Support}");

  1. 这样做的作用是告诉我们的应用程序,如果找不到控制器,它应默认到ErrorController类并执行该类中的Support()方法。保存您的代码并刷新浏览器,以查看应用程序默认到ErrorController

  1. 正如您所看到的,ASP.NET MVC 中的路由非常灵活。前面列出的步骤讨论了我们所谓的基于约定的路由。还有另一种称为基于属性的路由的路由方法,它在我们的控制器上使用属性。转到ErrorController类并向类添加using Microsoft.AspNetCore.Mvc;命名空间。然后,在类名上添加属性[Route("Error")],在方法上添加属性[Route("Support")]。您的代码应如下所示:
        [Route("Error")]
        public class ErrorController
        {
          [Route("Support")]
          public string Support()
          {
            return "Content not found. Contact Support";
          }
        }

  1. Startup类中的FindController()方法中,注释掉route.MapRoute("Default", "{controller=Error}/{action=Support}");这一行。在浏览器中,在 URL 末尾添加文本/Error/Support并输入。您会看到应用程序根据ErrorController类中定义的属性正确匹配ErrorController

工作原理...

MVC 框架内的路由是一种非常灵活的方法,可以根据 HTTP 请求访问特定的控制器。如果您需要对访问的控制器有更多控制权,则基于属性的路由可能比基于约定的路由更合适。也就是说,在使用基于属性的路由时,您可以做一些额外的事情。看看在使用基于属性的路由时作为开发人员可用的内容。

渲染视图

到目前为止,我们一直在使用普通的 C#类作为控制器,但更常见的是让您的控制器从 MVC 框架提供的Controller基类继承。这使开发人员能够从他们的控制器中返回复杂的对象,例如我们的学生。这些复杂的返回类型以实现IActionResult接口的结果返回。因此,我们可以返回 JSON、XML,甚至 HTML 以返回给客户端。接下来,我们将看一下这个用法以及创建视图。

准备工作

打开StudentController类并修改它以包含基于属性的路由。确保在StudentController类中添加using Microsoft.AspNetCore.Mvc;命名空间。还要从Controller基类继承。

[Route("Student")]
public class StudentController : Controller
{
   [Route("Find")]
   public string Find()
   {
      return "Found students";
   }
}

然后,在您的项目中添加一个名为Models的文件夹。在Models文件夹中,添加一个名为Student的类,因为我们的应用程序将返回学生信息。这将是一个简单的类,其中包含学生编号、名和姓的属性。您的Student类应如下所示:

public class Student
{
   public int StudentNumber { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

回到StudentController,我们想要实例化我们的Student模型并给它一些数据。然后,将Find()方法的返回类型从string更改为IActionResult。同时,将using AspNetCore.Models;命名空间添加到你的StudentController类中。

注意,如果你的项目不叫AspNetCore,你的命名空间会相应地改变:

using [projectname].Models;

你的代码现在应该如下所示:

[Route("Find")]
public IActionResult Find()
{
   var studentModel = new Student
   {
      StudentNumber = 123
      , FirstName = "Dirk"
      , LastName = 'Strauss"
   };
   return View(studentModel);
}

最终,我们希望从我们的StudentController返回一个视图结果。我们现在已经准备好进行下一步了。

操作步骤...

  1. 在你的项目中添加一个名为Views的新文件夹。在该文件夹内,再添加一个名为Student的文件夹。在Student文件夹内,通过右键单击Student文件夹并从上下文菜单中选择“新建项...”来添加一个新项。在“添加新项”对话框中搜索 MVC 视图页面模板,并将其命名为Find.cshtml

  1. 你应该开始注意到Views文件夹、子文件夹和视图遵循非常特定的命名约定。这是因为 MVC 框架遵循非常特定的约定,当你查看StudentController时,这个约定就会变得清晰。Views文件夹包括ViewsStudentFind,而StudentController包含类名中的Student和一个名为Find()的方法。

你也可以在Views文件夹中创建一个Shared文件夹。这是你放置所有控制器共享的视图的地方,控制器会默认在Shared文件夹中查找。

  1. 回到Find.cshtml Razor 视图,删除当前存在的代码,并用以下代码替换:
        <html >
          <head>
            <title></title>
          </head>
          <body>
          </body>
        </html>

你也可以使用 HTML 代码片段。输入html并按两次Tab键,将 HTML 代码的样板插入到 Find 视图中。

  1. 使用 Razor 视图的关键在于你可以直接在Find.cshtml文件中编写 C#表达式。然而,在这之前,我们需要设置我们将要引入视图的模型类型。我们使用以下指令来实现:@model AspNetCore.Models.Student。现在我们可以在 Razor 视图中直接引用我们的Student模型,并且拥有完整的智能感知支持。这是通过使用大写的M来实现的@Model。看一下 Razor 视图的变化:
        @model AspNetCore.Models.Student
        <html >
          <head>
            <title>Student</title>
          </head>
          <body>
            <div>
              <h1>Student Information</h1>
              <strong>Student number:</strong>@Model.StudentNumber<br />
              <strong>First name: </strong>@Model.FirstName<br />
              <strong>First name: </strong>@Model.LastName<br />
            </div>
          </body>
        </html>

工作原理...

保存你的代码并刷新你的浏览器。你的 URL 应该是http://localhost:[your port number]/student/find,这样才能正常工作。

HTTP 请求被路由到StudentController,然后填充并返回包含我们需要的数据的Student模型,并将其发送到 Find Razor 视图。这就是 MVC 框架的本质。当涉及到 MVC 框架和 ASP.NET Core 时,还有很多内容需要涵盖,但本章只涉及这些主题的基本介绍。

作为开发人员,我们不断面临着跟上最新技术的挑战。我们渴望学习更多,变得更加优秀。你正在阅读这本书本身就是对这一点的证明。然而,就本章而言,.NET Core 和 MVC 框架是绝对需要更多学习的领域。在一章中不可能涵盖所有内容。开发人员可以找到各种在线资源。我发现微软虚拟学院mva.microsoft.com是学习新技术的最佳(免费)资源之一。微软专家提供免费的微软培训。

希望这足以引起你的兴趣,并鼓励你进一步研究这些主题。

第十二章:选择和使用源代码控制策略

源代码控制是每个开发人员工具包的重要组成部分。无论您是业余爱好者还是专业程序员,当您离开办公桌回家时,您最好确保您的代码是安全的。在本章中,我们将讨论选择和使用源代码控制策略。我们将讨论一些主题,比如:

  • 设置 Visual Studio 帐户管理并确定哪种源代码控制解决方案最适合您

  • 设置 Visual Studio GitHub 集成,首次检入代码,以及检入更改

  • 使用 GitHub 作为团队合作,处理和解决代码冲突

介绍

在我的职业生涯中,我使用过 Visual SourceSafe、SVN、VSTS、Bitbucket 和 GitHub。重要的不是你如何对待它,而是你保持你的源代码安全和版本化。当我开始使用源代码控制时,我所在的公司使用了 Visual SourceSafe。如果您对这个软件不熟悉,可以搜索一下。你会看到一些包含“讨厌”、“不愉快”、“糟糕”和“微软的源代码破坏系统”的结果。你懂的。

我们有一个员工离开了他独占的文件,之后他辞职并移民到另一个国家。我开始怀疑公司强制使用 SourceSafe 的政策是否是他移民的原因。但开玩笑的,这给我们带来了无尽的问题。在一个大型项目上使用 SourceSafe,可能会导致灾难。然而,如今,开发人员有很好的选择。

显而易见的两个是 Microsoft Team Services 和 GitHub。它们都有免费的层级,但使用其中一个而不是另一个的决定完全取决于您的独特情况。

设置 Visual Studio 帐户管理并确定哪种源代码控制解决方案最适合您

Visual Studio 允许开发人员创建帐户并登录。如果您经常在不同的机器上工作(比如工作和家用 PC),那么这将特别有益,因为 Visual Studio 将自动在您登录的机器之间同步您的设置。

准备工作

本教程将假设您刚刚在您的计算机上安装了 Visual Studio 2017。无论您安装的是试用版还是授权版的 Visual Studio 2017 都无所谓。

如何做...

  1. 安装完成后,打开 Visual Studio。

  2. 在 Visual Studio 的右上方,您会看到一个“登录”链接:

  1. 单击“登录”链接,您将被允许在此输入您的电子邮件地址。我发现直接使用我的 Outlook 电子邮件地址很有用。在我看来,这是最好的网络电子邮件之一。

请注意,我之所以推荐 Outlook 并不是因为其他原因,而是因为我真的认为它是一个很棒的产品。我还有一个 Gmail 帐户和一个 iCloud 电子邮件帐户。

  1. 添加完您的电子邮件帐户后,Visual Studio 将重定向您到登录页面。

  2. 因为我已经有一个 Outlook 帐户,所以 Visual Studio 只允许我使用它登录。但是,如果您需要创建一个帐户,可以在“登录到 Visual Studio”表单上的注册链接上这样做:

  1. Visual Studio 现在将重定向您到一个注册页面,您可以在那里创建一个帐户:

  1. 创建完您的帐户后,您将被提示返回 Visual Studio 进行登录。登录后,Visual Studio 将在 IDE 的右上角显示您的详细信息:

  1. 单击您的帐户名称旁边的向下箭头,您可以查看您的帐户设置....

  1. 这将向您显示您的帐户摘要,您可以在其中进一步个性化您的帐户:

它是如何工作的...

源代码控制的选择是每个开发人员都有强烈意见的话题。不幸的是,如果您为老板工作,这个决定可能甚至不取决于您。许多公司已经按照他们喜欢的方式设置了他们的源代码控制系统,您需要遵守公司的程序。这就是现实。然而,作为独立开发人员,了解可用的选项是很好的。

所有优秀的开发人员也应该在自己的时间里编写代码。您不仅在工作时才是开发人员。我们吃饭、呼吸、睡觉,生活中都离不开代码。这是我们是谁以及我们是什么的一部分。我会说,为了成为更好的开发人员,您必须在自己的时间里玩弄代码。开始一个小项目,召集一些朋友,决定一起编写一些软件。这不仅会让您们都变得更好,而且您们会互相学到很多东西。

如果您是一名远程开发人员,不需要每天通勤到办公室工作,您仍然可以与开发人员社区联系。开发人员有很多资源可用,开发人员社区也乐意围绕新手提供帮助。如果您不致力于保护您的代码,开始一个独立或小项目是没有意义的。而要做到这一点,您也不必花一分钱。Visual Studio Online(现在称为团队服务)和 GitHub 为开发人员提供了一个绝佳的平台来保护您的代码。

让我们首先看看团队服务。可以通过将浏览器指向www.visualstudio.com/team-services/来找到该网站。

在这里,您将看到微软为开发人员提供了使用团队服务的绝佳机会。最多可免费使用五个用户。这意味着您和您的伙伴可以共同致力于下一个大项目,同时确保您的代码保持安全。注册非常简单,只需点击“免费开始”链接:

有关定价信息,请访问以下链接:

www.visualstudio.com/team-services/pricing/

第二个优秀的选择是 GitHub。它在免费提供方面略有不同,要求开发人员在免费账户上使用公共存储库。如果您不介意您的代码基本上是开源的,那么 GitHub 是一个很好的选择。不过,使用 GitHub,您可以拥有无限的合作者和公共存储库:

有关定价信息,请访问以下链接:

github.com/pricing

源代码控制的选择基本上取决于您的代码的开放性。如果您可以让其他开发人员看到并下载您的代码,那么 GitHub 是一个很好的选择。如果您需要您的代码保持私密,并且只在特定人员之间共享,那么付费的 GitHub 账户会更适合您。如果您还不想花钱,那么团队服务将是您最好的选择。

设置 Visual Studio GitHub 集成,首次提交代码,以及提交更改

多年来,GitHub 一直是一股强大的力量。有开发人员对它赞不绝口。事实上,使用 Apple 的 Xcode IDE 时,它是默认选项。无论出于何种原因,您决定使用 GitHub,可以放心,您和您的代码都在安全的手中。

做好准备

以下步骤将假定您已经注册了 GitHub 账户,并且已启用了双因素身份验证。如果您还没有注册 GitHub 账户,可以访问github.com/注册一个新账户。要在 GitHub 账户上启用双因素身份验证(我个人强烈建议这样做),请执行以下操作:

  1. 点击个人资料图片旁边的向下箭头,然后选择设置:

  1. 从下一个网页左侧出现的个人设置菜单中,选择安全性:

  1. 安全页面的第一部分将是您的双因素身份验证状态。要开始设置它,请单击“设置双因素身份验证”按钮。

  2. 然后,您将看到什么是双因素身份验证的简要概述,并可以选择使用应用程序进行设置(我推荐的)或使用短信进行设置。使用应用程序是最简单的方法,如果您有智能手机或平板电脑,可以从适用的应用商店下载身份验证器应用程序。然后,按照 GitHub 给出的提示完成双因素身份验证设置。

  3. 完成设置后,您的双因素身份验证将被打开。

如何做到的...

  1. 将 GitHub 扩展添加到 Visual Studio 很容易,只需从以下链接下载 visx 并安装:visualstudio.github.com/downloads/GitHub.VisualStudio.vsix

  2. 假设您有要添加到 GitHub 的现有应用程序,那么将其添加到新存储库的过程非常简单。我只是创建了一个仅包含模板代码的控制台应用程序,但您可以将任何项目类型和大小添加到 GitHub。

  3. 在 Visual Studio 2017 的“视图”菜单中,选择“Team Explorer”选项。

  4. 在托管服务提供程序部分,您将看到两个选项。现在,我们将选择 GitHub,并且,因为我们已经有一个帐户,我们将单击“连接”...

  1. 现在,您将看到 GitHub 登录页面。如果您没有现有的 GitHub 帐户,您也可以从这里注册:

  1. 因为我在 GitHub 帐户上设置了双因素身份验证,所以我被提示使用我的身份验证器应用程序输入生成的身份验证代码并进行身份验证:

  1. 认证后,您将返回到“管理连接”屏幕。如果您的项目未显示在本地 Git 存储库下,可以添加它:

  1. 接下来,您将要单击主页图标,即 Team Explorer 窗口顶部的小房子图标。从主屏幕,单击“同步”按钮:

  1. 这将向您显示发布窗口。在 GitHub 下,单击“发布到 GitHub”按钮。这将把您的项目发布到 GitHub 的新存储库中。

请记住,如果您使用的是免费的 GitHub,那么您的所有存储库都是公开的。如果您正在编写不能公开的代码(不是开源的),那么请注册一个包括私人存储库的付费 GitHub 帐户。

  1. GitHub 随后会提示您添加此发布的详细信息。因为您之前连接到了 GitHub,所以您的用户名将已在下拉菜单中选择。准备好后,单击“发布”:

  1. 项目发布到 GitHub 后,您将自动返回到主屏幕:

  1. 查看您的 GitHub 帐户在线,您将看到项目已添加:

  1. 接下来,让我们去对GitHubDemo应用程序进行一些更改。只需添加一个新类到您的项目中。我称我的为NewClass.cs,但您可以随意命名。

  2. 您会注意到,一旦对项目进行更改,解决方案将用红色勾标记更改的项目。您的类将用绿色加号标记:

  1. 要将更改添加到 GitHub 存储库,您可以选择两种方法。第一种选择是转到 Team Explorer - 主页窗口,然后单击“更改”按钮。

  2. 第二种(我认为更方便的)选择是在“解决方案资源管理器”中右键单击解决方案,然后从上下文菜单中单击“提交...”菜单项。

  3. 第一次执行提交时,GitHub 可能会要求您提供用户信息。

  4. 在允许提交更改之前,您必须填写所需的提交消息。在真实的团队项目中,在提交消息中尽可能详细地描述。考虑使用任务项代码(或积压代码)来唯一标识所添加的代码。这将在未来的某个时候为您(或其他开发人员)节省时间,我保证:

  1. 需要注意的一件重要事情是,如果单击“提交所有”按钮旁边的向下箭头,您将有三个提交选项可供选择。提交所有按钮将仅记录您在本地机器上进行的更改。换句话说,更改不会反映在远程存储库中。提交所有并推送按钮将记录本地机器上的更改,并将这些更改推送到您的远程 GitHub 存储库。提交所有并同步按钮将记录本地机器上的更改,然后将从远程存储库中拉取任何更改,最后进行推送。如果您正在团队中工作,您将希望这样做。但是,对于本教程,我将只进行提交所有并推送,因为我是唯一在这个存储库上工作的开发人员:

  1. 当提交完成后,团队资源管理器 - 同步窗口将通知您提交成功:

  1. 转到 GitHub 在线,您将看到新推送的更改反映在您的 GitHub 存储库中,以及提交消息:

  1. GitHub 是任何开发人员的绝佳源代码控制解决方案。考虑创建一个开源项目。它比您想象的更有益。

如今,越来越多的潜在雇主在考虑开发人员职位申请者时会审查他们的 GitHub 存储库。请记住这一点,因为 GitHub 存储库本身就是一份简历。

它是如何工作的...

免费的 GitHub 帐户允许您创建公共存储库。这意味着任何人都可以从 GitHub 搜索、查看和克隆您的项目到他们自己的桌面。这是 GitHub 的核心理念。这显然是独立开发人员和不想花钱的公司的关键因素。公司可以承受比独立开发人员更多的费用,但我认为一些公司更喜欢自己动手,而不是使用云中托管的服务提供商。这意味着他们更喜欢通过在自己的公司服务器上设置源代码控制系统来保持对源代码控制的控制。对于独立开发人员来说,GitHub 作为一个选择是一个很棒的解决方案。对于那些需要私有存储库的人来说,费用也不是一个障碍。

使用 GitHub 作为团队合作,处理和解决代码冲突

在团队中工作时,GitHub 和 Team Services 真的发挥了作用。协作努力的效果非常强大。不过,有时可能会有些挑战。让我们看看如何使用 GitHub 在团队设置中工作。

准备工作

我们将使用已经检入 GitHub 的现有GitHubDemo应用程序。假设一个新的开发人员(我们称之为约翰)加入了团队。在您允许他将代码推送到您的分支之前,您需要将他添加为合作者。要做到这一点,请登录 GitHub,然后单击GitHubDemo存储库中的设置选项卡。单击左侧菜单中的合作者。

然后,您可以通过输入他们的 GitHub 用户名、全名或电子邮件地址来搜索要添加的合作者:

完成后,单击“添加合作者”按钮将约翰添加为项目的合作者:

约翰将收到一封电子邮件,并首先需要回复您的合作邀请。

如何做...

  1. 约翰开始设置他的 Visual Studio 环境,包括通过单击菜单中的团队并单击管理连接来连接到 GitHub....

  2. 他用电子邮件地址和密码登录 GitHub。

请注意,如果您刚刚注册 GitHub,您需要单击发送到注册时指定的电子邮件地址的验证电子邮件。如果未验证您的电子邮件地址,您将无法从 Visual Studio 登录。

  1. 连接后,约翰看到他的 GitHub 详细信息已加载:

  2. 他现在想要在 GitHub 上工作GitHubDemo应用程序,并通过名称搜索在 GitHub 上找到它:

  1. 他现在从克隆或下载按钮的“使用 HTTPS”文本框中复制 URL:

  1. 回到 Visual Studio,约翰展开本地 Git 存储库并单击克隆。他将复制的 URL 粘贴到 Git 存储库路径,并指定代码应克隆到他的硬盘上的位置。然后单击克隆:

  1. 当代码被克隆时,它将在约翰之前指定的文件夹路径中。

  2. 是时候对代码进行一些更改了。他像往常一样在 Visual Studio 中打开项目。约翰决定在NewClass类上工作,并添加一个返回倒计时整数的新函数:

  1. 代码更改完成后,约翰准备提交他刚刚添加到GitHubDemo项目的代码。

  2. 添加提交消息后,然后单击“提交所有”和“同步”。

一个重要的事情要注意的是,如果您单击“提交所有”按钮旁边的向下箭头,您将有三个提交选项可供选择。此按钮将仅记录您在本地计算机上进行的更改。换句话说,更改不会反映在远程存储库中。 “提交所有并推送”按钮将记录本地计算机上的更改,并将这些更改推送到远程 GitHub 存储库。 “提交所有并同步”按钮将记录本地计算机上的更改,然后将从远程存储库中拉取任何更改,最后将进行推送。

  1. 约翰的更改已提交到 GitHub 存储库:

  1. 在办公室的另一边,我正在处理相同的一小部分代码。唯一的问题是我已经添加了相同的方法,并使用了自己的CountDown逻辑实现:

  1. 我准备好并提交我的更改到 GitHub:

  1. GitHub 立即阻止我这样做。这是因为如果我的代码被推送,John 之前的提交将会丢失。GitHub 在 GitHub 帮助中有关于这个主题的很好的帮助文件help.github.com/articles/dealing-with-non-fast-forward-errors/

输出窗口包含更详细的错误消息:

推送到远程存储库时遇到错误:被拒绝的更新,因为远程包含您本地没有的工作。这通常是由另一个存储库推送到相同的引用引起的。您可能需要在再次推送之前先集成远程更改。

  1. 要解决此问题,请单击“拉取”以获取约翰最新的提交。然后您的代码将处于冲突状态。听起来很糟糕,但实际上并不是。这让您控制决定使用哪些代码。您可以看到拉取显示有冲突的文件,还有约翰添加的传入提交消息:

  1. 要查看冲突,请点击消息弹出窗口中的解决冲突链接:

  1. 然后您将看到解决冲突屏幕,列出了冲突的文件。单击文件将其展开为简短摘要和操作选项屏幕。始终明智地单击“比较文件”链接以查看冲突文件之间的差异:

  1. 代码上的差异立即显而易见。从这里开始,你们团队的工作流程取决于你们如何合作。通常,冲突可能会非常复杂,因此与相关开发人员讨论未来的方向总是一个好主意:

  1. 在这种情况下,约翰和我决定他的代码更好、更简洁。因此,决定只需点击“接受远程”并使用约翰的代码。当你点击链接后,需要点击“提交合并”:

  1. 添加提交消息后,你可以将代码推送到仓库。在这种情况下,我只是用约翰的代码替换了我的所有代码,但可能会出现一些情况,你需要使用一些你的代码和另一位开发者的代码。GitHub 允许我们轻松处理这些冲突。

  1. 将代码推送到远程后,GitHub 会通知你代码已成功同步:

它是如何工作的...

GitHub 简化了提交、解决冲突和合并代码的痛苦。毫无疑问,它是任何开发者工具包中的必备工具,也是开发团队的必备工具。即使你不是专业使用它,为自己创建一个仓库也是一个好主意。开始使用它来检查你在下班后工作的宠物项目。将你的知识扩展到日常工作之外,这将使你成为一个更好的开发者。

第十三章:在 Visual Studio 中创建移动应用程序

Visual Studio 是集成开发环境IDEs)的强大工具。毫无疑问。作为开发人员,您可以通过为各种平台创建应用程序来尽情发挥您的多才多艺。其中之一就是移动开发。开发人员开始创建移动应用程序,但不想使用不同的 IDE。使用 Visual Studio,您不必这样做。它将允许您创建 Android 和(现在还有Xamarin)iOS 和 Mac 应用程序。

因此,本章将讨论以下概念:

  • 在您的 Windows PC 和 Mac 上安装 Xamarin 和其他所需组件

  • 在 Visual Studio 中使用 Apache Cordova 创建移动应用程序

  • 使用 Xamarin.Forms 和 Visual Studio for Mac 创建 iOS 应用程序

介绍

如果您还没有听说过 Xamarin,我们鼓励您搜索一下这个工具。传统上,开发人员需要使用XcodeNetBeans来创建 iOS 和 Android 应用程序。对开发人员来说,挑战在于这意味着需要学习一种新的编程语言。例如,如果您创建了一个要部署到 iOS、Android 和 Windows 的应用程序,您需要了解 Objective-C 或 Swift、Java 和.NET 语言。

这也为开发带来了额外的挑战,因为这意味着必须维护多个代码库。如果在应用程序的 Windows 版本中进行更改,还必须对 iOS 和 Android 代码库进行更改。有时公司会为每个平台管理不同的开发团队。您可以想象在多个团队和多个平台上管理变更所涉及的复杂性。如果您正在处理一个庞大的代码库,这一点尤为真实。

Xamarin 通过允许.NET 开发人员使用标准.NET 库在 Visual Studio 中创建 iOS 和 Android 应用程序来解决了这个问题。作为.NET 开发人员,您现在可以使用您已经拥有的技能来完成这个任务。简而言之,您将为您的应用程序创建一个共享库,然后为不同的平台创建不同的外观。第二个选择是使用 Xamarin.Forms 创建一个 Visual Studio 项目并针对所有三个平台。这使得开发人员很容易地针对多个平台进行开发。

在您的 Windows PC 和 Mac 上安装 Xamarin 和其他所需组件

Xamarin 到底是如何工作的?看起来确实像魔术,对吧?我的意思是,在 Visual Studio 中编写 C#并在另一端编译成本地的 iOS、Mac 或 Android 应用程序确实看起来像魔术。许多技术已经投入到让开发人员有能力做到这一点。对于 iOS 和 Mac 应用程序,这个过程有点复杂。如果您想要针对 iOS 或 Mac,需要使用 Mac 来构建您的 iOS 应用程序。有一些服务可以让 Mac 远程测试和编译(例如 MacinCloud,www.macincloud.com/)。然而,这些服务会产生月费。当 Xamarin 编译您的 C#代码时,它会针对 Mono 框架的一个特殊子集进行编译。

Mono 由微软赞助,是.NET Framework 的开源实现。这是基于C#公共语言运行时的 ECMA 标准。有关 Mono 框架的更多信息,请查看www.mono-project.com/

特别是针对 iOS,这个特殊子集包括允许访问 iOS 平台特定功能的库。Xamarin.iOS 编译器将接受您的 C#代码并将其编译成一种称为 ECMA CIL 的中间语言。然后,这个通用中间语言CIL)会再次编译成 iPhone 或 iPad 可以运行的本地 iOS 代码。然后您还可以将其部署到模拟器进行测试。

现在,您可能会想为什么需要 Mac 来编译您的应用程序?为什么不能在 Visual Studio 内部完成所有操作?嗯,这是由于苹果对 iOS 内核生成代码的能力施加了(相当巧妙的)限制。它根本不允许这种情况发生。正如您所知道的(这是极其简化的解释),当您的 C#源代码编译进行测试时,它被编译成中间语言。即时JIT)编译器然后将中间语言编译成适合您所针对的架构的汇编代码。由于 iOS 内核不允许 JIT 编译器进行按需编译,代码是使用提前编译AOT)编译进行静态编译的。

要查看 Xamarin.iOS 的限制,请参阅以下链接:

developer.xamarin.com/guides/ios/advanced_topics/limitations/ 查看 Xamarin.iOS、Xamarin.Mac 和 Xamarin.Android 中可用程序集的列表,请参阅以下支持文档:

developer.xamarin.com/guides/cross-platform/advanced/available-assemblies/.

这背后的技术非常令人印象深刻。难怪微软收购了 Xamarin 并将其作为 Visual Studio 的一部分。为跨平台开发提供开发者这样一系列选择正是微软的目标:赋予开发者创造世界一流应用程序的能力。

准备工作

在本教程中,我们将介绍如何在运行 Visual Studio 2017 的 Windows PC 上安装 Xamarin。Xamarin 可以作为工作负载的一部分在安装 Visual Studio 2017 时安装。现在,让我们假设 Xamarin 尚未安装,并且您需要在安装 Visual Studio 后立即进行安装。转到 Visual Studio 网站www.visualstudio.com/,并下载您安装的 Visual Studio 版本的安装程序。

您还可以在 Visual Studio 2017 的“新建项目”对话框屏幕上运行安装程序。如果您折叠已安装的模板,您将看到一个允许您打开 Visual Studio 安装程序的部分。

您还需要安装 Xcode,这是苹果的开发环境。您可以从 Mac App Store 免费下载。

请注意,您需要有 iTunes 登录才能下载 Xcode 并完成 Mac 的设置。如果您有 Mac,那么您很可能也有 iTunes 登录。

如何操作...

  1. 双击从 Visual Studio 网站下载的安装程序。您将看到显示您的 Visual Studio 2017 版本,并且会出现一个“修改”按钮。点击“修改”按钮:

  1. 这将显示可用的工作负载。在“移动和游戏”部分下,确保选择“使用.NET 进行移动开发”。然后,点击右下角的“修改”按钮:

  1. 如果我们想要使用 Xamarin 来针对 iOS 应用程序,还有第二步需要采取。我们必须在 Mac 上安装所需的软件。在 Mac 上访问 Xamarin 的网站。网址是www.xamarin.com/。点击“产品”下拉菜单,从列表中选择 Xamarin 平台:

  1. 您还可以通过访问www.xamarin.com/platform来访问所需的页面。单击“立即免费下载”按钮将在您的 Mac 上安装一个名为Xamarin Studio Community的东西。您需要知道的是,当在 Mac 上安装时,Xamarin Studio 无法创建 Windows 应用程序。它只允许您在 Mac 上创建 iOS 和 Android 应用程序。除了 Xamarin Studio,您还将获得 Xamarin Mac 代理(以前称为 Xamarin 构建主机)。这是一个必需的组件,以便您可以将您的 PC 链接到 Mac,以构建您的 iOS 应用程序。最后,PC 和 Mac 还必须能够通过网络相互连接(稍后会详细介绍)。

  2. 在 Mac 上下载安装程序后,安装过程很简单。您会注意到在安装屏幕上有一些选项可供选择:Xamarin.Android、Xamarin.iOS、Xamarin.Mac 和 Xamarin Workbooks & Inspector。如果您想要以 Android 作为平台,您将安装 Xamarin.Android。要针对 iOS(iPhone 或 iPad),您需要选择 Xamarin.iOS。要创建完全本机的 Mac 应用程序,您必须选择 Xamarin.Mac。最后,Xamarin Workbooks & Inspector 为开发人员提供了一个与应用程序调试集成的交互式 C#控制台,以帮助开发人员检查运行中的应用程序。目前,我们只对 Xamarin.iOS 感兴趣。只需按照屏幕提示完成安装。根据您的选择,安装程序将下载所需的依赖项并将其安装在您的 Mac 上。根据您的互联网连接,您可能想去喝杯咖啡:

  1. 最后,如果您尚未从 Mac App Store 安装 Xcode,请在继续之前立即这样做:

它是如何工作的...

我们之前安装 Xamarin 时所采取的步骤将使我们能够在开发跨平台时针对 Mac、iOS 和 Android(如果我们选择了 Xamarin.Android)平台进行开发。以前(在 Visual Studio 2015 之前),开发人员必须学习一个新的集成开发环境,以便提升自己的技能,以创建其他平台的应用程序。就我个人而言,我发现 Xcode(用于创建本机 iOS 和 Mac 应用程序的苹果开发人员集成开发环境)有点学习曲线。这不是因为它太复杂,而是因为它显然与我在 Visual Studio 中习惯的方式不同。如果您真的想学习另一种编程语言,并且想要选择 Xcode 的路线,请看看 Swift。这是一种出色的语言,我发现它比 Objective-C 更容易与 C#相关联。

然而,如果您宁愿坚持您所知道并且熟悉的内容,那么 Xamarin 是您开发跨平台应用程序的最佳选择。您也不必去购买 MacBook 来编译您的应用程序。当您想要开始为 iOS 和 Mac 开发时,Mac mini 已经足够了。这是对您的开发工具集的一种投资,将使您受益匪浅。作为开发人员,您还可以选择云选项(例如 MacinCloud)。使用 Xamarin,您可以坚持使用 C#并在您熟悉的环境中开发。

开发人员还有第三种最终选择,这是我们将在本章的最后一个配方中进行讨论的。本配方中的步骤是用于在 Windows PC 上创建应用程序并在 Mac 或 MacinCloud 解决方案上编译它们的情况。

使用 Apache Cordova 创建移动应用程序

使用 Apache Cordova 创建移动应用程序一点也不复杂。如果您熟悉 Web 开发,那么这对您来说会感觉非常自然。对于那些以前没有开发过 Web 应用程序的人来说,这将帮助您熟悉这个过程。这是因为 Cordova 的本质是一个 Web 应用程序。您引用诸如 JS 文件和 CSS 文件之类的文件,并且您可以在浏览器中调试index.html文件。

Cordova 应用程序为您提供了针对 iOS、Android 或 Windows 应用程序的灵活性。这个教程将演示一个简单的应用程序,当用户在应用程序中点击按钮时,它会显示当前日期。

准备工作

您需要在 Visual Studio 2017 安装过程中安装 JavaScript 工作负载。现在,让我们假设您在安装 Visual Studio 2017 时没有安装它,现在需要再次运行安装程序。

您还可以在 Visual Studio 2017 的新项目对话框屏幕中运行安装程序。如果折叠已安装的模板,您将看到一个允许您打开 Visual Studio 安装程序的部分。

转到 Visual Studio 网站www.visualstudio.com/,并下载您安装的 Visual Studio 版本的安装程序。还要注意,您需要在计算机上安装 Google Chrome,以便启动 Cordova 应用程序模拟器。

如何做到这一点...

  1. 双击从 Visual Studio 网站下载的安装程序。这将启动安装程序,并列出安装在您的计算机上的 Visual Studio 2017 版本,并显示一个修改按钮。点击修改按钮:

  1. 从“移动和游戏”组中,选择 JavaScript 工作负载的移动开发。然后,点击修改按钮。根据您的具体要求,可能会安装其他组件,例如Android SDKGoogle Android 模拟器的支持:

  1. Apache Cordova 使用诸如 HTML、CSS 和 JavaScript 之类的 Web 技术来构建可在 Android、iOS 和 Windows 设备上运行的移动应用程序。从 Visual Studio 创建一个新应用程序,并从其他语言模板中选择 JavaScript。然后选择空白应用程序(Apache Cordova)模板。这只是一个使用 Apache Cordova 构建 Android、iOS 和通用 Windows 平台UWP)的空白项目。我只是把我的应用叫做 MyCordovaApp。

  2. 一旦 Visual Studio 创建了您的应用程序,您会注意到它有一个非常特定的文件夹结构:

  • merges:展开merges文件夹,您会注意到有三个名为androidioswindows的子文件夹。开发人员可以使用这些文件夹根据他们正在针对的移动平台提供不同的内容。

  • www:这是您的大部分开发将发生的地方。index.html文件将成为 Cordova 应用程序的主要入口点。当启动您的移动应用程序时,Cordova 将查找这个索引文件并首先加载它。您还会注意到www文件夹下面有子文件夹。把它们想象成一个常规的 Web 应用程序文件夹结构,因为它们确实就是。css子文件夹将包含您需要使用的任何样式表。

您需要在移动应用程序中使用的任何图像都将存储在images子文件夹中。最后,您将在scripts子文件夹中添加任何移动(Web)应用程序使用的 JavaScript 文件。如果展开scripts子文件夹,您会注意到一个名为platformOverrides.js的 JavaScript 文件。这与merges文件夹一起使用,根据您正在针对的移动平台提供特定的 JavaScript 代码。

  • resres文件夹将用于存储可能被不同原生移动应用程序使用的非 Web 应用程序资源。这些资源可以是启动画面、图片、图标、签名证书等等:

您还会注意到几个配置文件。这些是bower.jsonbuild.jsonconfig.xmlpackage.json。虽然我不会详细介绍这些配置文件中的每一个,但我想简要提一下config.xmlpackage.json文件。在撰写本书时,package.json文件目前未被 Cordova 使用。它旨在最终取代config.xml文件。目前,config.xml文件包含特定于您的移动应用程序的设置。双击此文件以查看 Cordova 应用程序的自定义编辑器。自定义编辑器通过提供一个标准的 Windows 表单,避免了直接编辑 XML 文件的复杂性,您可以在其中输入特定于应用程序的设置。作为开发人员,您可以使用的设置包括应用程序名称、作者名称、应用程序描述、设备方向、插件配置等等。

非常重要的是,不要删除config.xml文件。这样做将破坏您的解决方案,Cordova SDK 将无法构建和部署您的移动应用程序。

  1. 此时,您可以从调试下拉菜单中选择一个设备并运行您的移动应用程序。如果您必须选择在浏览器中模拟 - Nexus 7(平板电脑),Visual Studio 将启动 Google Chrome 并显示默认的 Cordova 应用程序。这是每个 Cordova 应用程序的默认设置,实际上并不包含任何功能。它只是让您知道您的 Cordova 应用程序已经正确启动。不过有趣的是,您会看到一个新的选项卡在 Visual Studio 中打开,同时您的模拟器被启动。它被称为 Cordova 插件模拟,并默认为地理位置插件。这允许开发人员与插件进行交互,并在应用程序在模拟器中运行时触发特定事件。向您的 Cordova 应用程序添加新插件将在 Cordova 插件模拟中公开额外的窗格:

  1. 接下来,将 jQuery.mobile NuGet 包添加到您的解决方案中。NuGet 将会向您的解决方案安装 jQuery.1.8.0 和 jquery.mobile.1.4.5。在撰写本书时,建议不将 jQuery.1.8.0 升级到最新版本,因为存在兼容性原因:

  1. 在您的解决方案中,NuGet 将向您的项目的Scripts文件夹添加几个 JS 文件。将所有这些 JS 文件拖到您的www/scripts文件夹中。对于项目的Content文件夹也是一样。将所有 CSS 文件和images子文件夹拖到www/css文件夹中:

  1. 返回并打开您的index.html文件。您将在<body></body>标签之间看到以下内容:
        <div class="app">
          <h1>Apache Cordova</h1>
          <div id="deviceready" class="blink">
            <p class="event listening">Connecting to Device</p>
            <p class="event received">Device is Ready</p>
          </div>
        </div>

这是模板添加的默认样板代码,我们将不使用它。将其替换为以下代码,并在其他脚本引用的底部部分添加<script src="img/jquery-1.8.0.min.js"></script><script src="img/jquery.mobile-1.4.5.min.js"></script>

请注意,您的 JS 文件版本可能与之前引用的版本不同。

完成后,您的<body></body>部分应如下所示:

        <body>
          <div role="main" class="ui-content">
            <form>
              <label id="current-date">The date is:</label>
              <button id="get-date-btn" data-role="button" 
                data-icon="search">
                Get Current Date</button>
            </form>
          </div>
          <script src="img/jquery-1.8.0.min.js"></script>
          <script src="img/jquery.mobile-1.4.5.min.js"></script>
          <script src="img/cordova.js"></script>
          <script type="text/javascript" src="img/cordova.js"></script>
          <script type="text/javascript" src=
            "scripts/platformOverrides.js"></script>
          <script type="text/javascript" src="img/index.js"></script>
        </body>

  1. 然后,在<head></head>标签之间,添加上述<link rel="stylesheet" href="css/jquery.mobile-1.4.5.min.css" />样式引用,放在现有的<link rel="stylesheet" type="text/css" href="css/index.css">引用之上。

请注意,您的 CSS 文件版本可能与之前引用的版本不同。

完成后,您的代码应该类似于以下内容:

        <head>
          <!--
            Meta references omitted for brevity
          -->
          <link href="css/jquery.mobile-1.4.5.min.css" rel="stylesheet" />
          <link rel="stylesheet" type="text/css" href="css/index.css">
          <title>MyCordovaApp</title>
        </head>

  1. 您的应用程序现在包括所需的 jQuery 库,这将使您的移动应用程序移动和触摸优化。您的移动应用程序现在也对其将显示在的设备具有响应性。现在我们需要为应用程序添加一些基本样式。打开index.html文件中<head></head>部分引用的index.css文件。这应该在www/css/index.css中。用以下代码替换内容。#get-date-btn只是引用我们表单上的按钮,并将字体大小设置为 22 像素。form被设计为在底部包含 1 像素宽的实线边框:
        form {
          border-bottom: 1px solid #ddd;
          padding-bottom: 5px;
        }

        #get-date-btn {
          font-size: 22px;
        }

  1. 现在我们需要为用户点击“获取当前日期”按钮时添加一个点击事件。为此,打开位于www/scripts/index.jsindex.js文件。找到onDeviceReady()方法,并修改代码如下:
        function onDeviceReady() {
          // Handle the Cordova pause and resume events
          document.addEventListener( 'pause', onPause.bind(
            this ), false );
          document.addEventListener( 'resume', onResume.bind(
            this ), false );

          $('#get-date-btn').click(getCurrentDate);
        };

  1. 将此代码视为get-date-btn按钮的事件处理程序。实际上,它正在向按钮添加一个点击监听器,每当用户点击按钮时,它将调用getCurrentDate函数。现在可能是时候提到包含onDeviceReady()函数的(function () { ... })();函数了。这被称为匿名自调用函数,实际上只是您可以将其视为表单加载事件。您会注意到它为onDeviceReady()方法添加了一个事件处理程序。

  2. 最后,将getCurrentDate()函数添加到index.js文件中。

为了本教程的目的,我将保持简单,并将getCurrentDate()函数添加到index.js文件中,因为代码并不是非常复杂。对于更复杂的代码,最好创建一个单独的 JS 文件,并在index.html页面中引用该 JS 文件(在<body></body>部分的底部)以及其他 JS 文件引用。

getCurrentDate()函数并不特别。它只是获取日期并将其格式化为yyyy/MM/dd格式,并在index.html页面的标签中显示它。您的函数应该如下所示:

        function getCurrentDate()
        {
          var d = new Date();
          var day = d.getDate();
          var month = d.getMonth();
          var year = d.getFullYear();
          $('#current-date').text("The date is: " + year + "/"
            + month + "/" + day);
        }

它是如何工作的...

您现在可以开始调试您的应用程序。让我们在 Visual Studio 中选择不同的模拟器。选择在浏览器中模拟 - LG G5 并按F5

Chrome 将启动并显示您的 Cordova 应用程序:

单击“获取当前日期”按钮,当前日期将显示在您刚刚单击的按钮上方:

当您的模拟器打开时,打开您添加了getCurrentDate()函数的index.js文件,并在读取$('#current-date').text("The date is: " + year + "/" + month + "/" + day);的行上设置断点。然后再次单击“获取当前日期”按钮:

您会注意到您的断点被触发,现在您可以逐步检查变量并调试您的应用程序,就像您习惯做的那样。您甚至可以设置条件断点。这简直太棒了。

使用 Cordova 开发应用程序还有很多要学习的。Web 开发人员会发现这个过程很熟悉,并且应该很容易掌握。现在您可以将此应用程序在任何平台上运行,因为它完全跨平台。接下来,您可以尝试使用其中一个可用的 Android 模拟器来运行您的 Cordova 应用程序。尝试一下这个示例,并添加一些更多的功能代码。尝试访问 Web 服务以检索值,或者尝试玩一下样式。

能够使用 Visual Studio 从单个解决方案针对不同的移动设备,使开发人员有自由进行实验,并找到最适合他们和他们开发风格的解决方案。Cordova 站出来为那些不使用 Xamarin 等解决方案的开发人员提供了一个奇妙的解决方案。

使用 Xamarin.Forms 和 Visual Studio for Mac 创建 iOS 应用程序

许多开发人员想要尝试编写 iOS 应用程序。一直以来的一个大缺点是需要学习一种新的编程语言和一个新的集成开发环境。对于一些人来说,这可能不是问题,因为他们想要学习新的东西。但对于许多.NET 开发人员来说,能够坚持使用他们熟悉的集成开发环境和编程语言是非常有力量的。这正是 Xamarin.Forms 和 Visual Studio 所实现的。

请注意,我在这里没有考虑 Xamarin.Android。我纯粹专注于编写原生的 iOS 和 Mac 应用程序。

Xamarin 为.NET 开发人员提供了使用 Visual Studio 编写跨平台应用程序的能力,而无需为每个平台单独创建代码库。因此,您可以为应用程序拥有一个单一的代码库,该代码库将在 Windows、iOS/macOS 和 Android 上运行。如果您想要开始开发原生的 iOS/macOS 应用程序,您基本上有四个可行的选择(在我看来)。它们如下:

  • 购买一台 Mac 并自学 Xcode、Swift 和/或 Objective-C。

  • 购买一台 Mac 并安装 Parallels,在其中您可以安装 Windows、Visual Studio 和其他基于 Windows 的软件(Mac 不仅仅用于开发)。您可以在我几年前创建的Developer Community YouTube 频道上观看一个视频(www.youtube.com/developercommunity)。在那个视频中,我向您展示了如何使用 Parallels 在 Mac 上安装 Visual Studio 2013。

  • 购买一台 Mac 并下载Visual Studio for Mac(目前仍处于预览阶段),然后在 Mac 上安装该软件(Mac 专门用于开发 Android 和 iOS/macOS 应用程序)。

  • 购买一台 Mac 并使用它来编译在运行 Visual Studio 的 Windows PC 上开发的 iOS/macOS 应用程序。如果您需要创建仍然可以针对基于 Windows 的平台以及 Android 和 iOS/macOS 的应用程序,那么可以这样做。

如果您要使用Visual Studio for Mac和 Xamarin.Forms,那么您将无法在 macOS 上创建 Xamarin.Forms 项目,因为这些项目无法在 macOS 上构建。还要注意的是,我没有在这里考虑 MacinCloud,因为在开发过程中的某个阶段,我认为拥有一台实体的苹果 Mac 设备是非常有益的。

从前面列出的要点可以清楚地看出,您需要一台 Mac。虽然在 Windows PC 上安装 Visual Studio 并在本地网络上连接到 Xamarin Mac 代理是完全可能的,但当您需要尝试远程访问 Mac 时(例如从您的工作办公室),这可能会有些不便。理论上,这应该是可能的,但您需要做一些工作才能使这一切正常运行。首先,您可能需要在路由器上添加某种端口转发,以允许远程连接到您的 Mac。您还需要为您的 Mac 分配一个静态 IP 地址(甚至为您的路由器购买一个静态 IP 地址),这样,如果在您远程工作时发生断电重启,您仍然能够访问您的 Mac 进行 Visual Studio 构建。

在 Mac 上安装 Parallels 非常方便,当您需要使用其他基于 Windows 的软件时,它将非常有用。如果您(像我一样)将 Mac 专门用于开发目的,那么 Parallels 可能不是一个可行的解决方案。这就留下了Visual Studio for Mac,如果您只计划开发 iOS/macOS 和 Android 应用程序,那么这是一个很好的选择。

要下载 Visual Studio for Mac,请前往developer.xamarin.com/visual-studio-mac/并单击下载链接。安装过程与本章第一个配方中的安装过程有些类似。不同之处在于实际的 Visual Studio 应用程序将安装在 Mac 上,而不是在同一网络上的 Windows PC 上。

准备工作

下载 Visual Studio for Mac 后,开始安装过程。这与第一个配方中概述的过程非常相似。完成可能需要一些时间,所以再次,去喝杯咖啡。使用 Visual Studio for Mac 创建应用程序对于从 Visual Studio for Windows 转到.NET 开发人员来说是一种熟悉的体验。

Visual Studio for Mac 的核心是用于重构和智能感知的 Roslyn 编译器。构建引擎是 MSBuild,调试器引擎与 Xamarin 和.NET Core 应用程序相同。Xamarin 开发和 Visual Studio for Mac 的软件要求如下:

  • 您需要运行 OS X El Capitan(10.11)或 macOS Sierra 的 Mac。

  • 需要 iOS 10 SDK,该 SDK 随 Xcode 8 一起提供。只要您拥有有效的 iTunes 帐户,就可以免费下载 Xcode。

  • Visual Studio for Mac 需要.NET Core,可以按照www.microsoft.com/net/core#macos中概述的步骤进行下载。您必须完成列出的所有步骤,以确保.NET Core 正确安装。当您在那里时,请注意观看 Kendra Havens 的一些 Channel 9 视频,了解如何开始使用.NET Core,网址是channel9.msdn.com/。顺便说一句,还可以看看 Channel 9 上其他精彩的内容。

  • 如果您计划将应用程序提交到 Apple 应用商店,则需要购买开发者许可证,目前价格为每年 99 美元。但是,您可以在不购买开发者许可证的情况下开发您的应用程序。

请注意,如果您计划在 Xamarin Studio 旁边安装 Visual Studio for Mac,则需要知道 Visual Studio for Mac 需要 Mono 4.8。安装 Xamarin Studio 将会将 Mono 降级到旧版本。为了解决这个问题,您需要在 Xamarin Studio 更新屏幕上选择退出 Mono 4.6 的选择。

有了这个相当详细的要求清单,让我们准备好创建一个 iOS 应用程序。

如何做...

  1. 启动 Visual Studio for Mac,并使用您的 Microsoft 帐户详细信息登录。您会注意到“入门”部分,其中列出了许多有用的文章,帮助开发人员开始使用 Visual Studio for Mac:

  1. 接下来,点击“新建项目...”,并在多平台应用程序模板中的 Xamarin.Forms 组中选择 Forms App 项目。然后,点击“下一步”:

  1. 然后,我们需要为我们的应用程序命名和添加组织标识符。我只是将我的应用程序命名为HelloWorld,然后在“目标平台”下只选择了 iOS。点击“下一步”继续:

  1. 最后,决定是否要配置项目以使用 Git 进行版本控制和 Xamarin Test Cloud。当您配置好所需的内容后,点击“创建”:

  1. 创建项目后,您会注意到可以通过单击“调试”按钮旁边的向下箭头来选择要模拟的设备:

  1. 这将列出不同的模拟器可供您使用,以及连接到您的 Mac 的任何设备(在本例中是我的 iPhone):

  1. 点击“运行”按钮将启动所选设备的模拟器,并显示创建 Xamarin.Forms iOS 应用程序时为您创建的默认应用程序:

  1. 模拟器中的应用程序是完全可用的,您可以与其交互以了解模拟器的工作原理。如前所述,如果您的 Mac 上连接了 iOS 设备,甚至可以在设备上启动应用程序进行测试。例如,点击“关于”选项卡将显示“关于”页面:

  1. 在 Visual Studio for Mac 中点击停止按钮,返回到您的解决方案。展开ViewModelsViews文件夹。您会看到一个非常熟悉的结构:

  1. ViewModels文件夹中,打开AboutViewModel.cs文件。在构造函数AboutViewModel()中,您将看到以下代码:
        public AboutViewModel()
        {
          Title = "About";
          OpenWebCommand = new Command(() => Device.OpenUri(new 
            Uri("https://xamarin.com/platform")));
        }

  1. 现在,为了说明 C#的使用,将此处的代码更改为以下代码清单的样子。您注意到了第一行代码吗?var titleText =后面的部分是一个插值字符串$"Hello World - The date is {DateTime.Now.ToString("MMMM dd yyyy")}";。插值字符串是在 C# 6.0 中引入的。点击播放按钮在模拟器中启动应用程序:
        public AboutViewModel()
        {
          var titleText = $"Hello World - The date is {
            DateTime.Now.ToString("MMMM dd yyyy")}";
          Title = titleText;
          OpenWebCommand = new Command(() => Device.OpenUri(new 
            Uri("https://xamarin.com/platform")));
        }

  1. 现在,再次点击“关于”选项卡,查看标题。标题已更改为显示“Hello World”和当前日期:

工作原理...

好吧,我将首先承认,我们编写的代码并没有什么了不起的。实际上,我们基本上是在现有的应用程序上进行了一些修改,只是修改了一点代码来显示“Hello World”和当前日期。然而,需要记住的一件事是,我们编写了 C#代码并将其编译为本机 iOS 应用程序。

还有很多东西要学习。我们甚至还没有涉及使用 Visual Studio for Mac、Xamarin.Forms 和跨平台 C#应用程序现在提供的所有内容。Xamarin 有非常好的文档,将在您使用 Xamarin 开发应用程序的新途径时为您提供帮助。一个很好的案例研究是 Tasky 案例研究,可以在developer.xamarin.com/guides/cross-platform/application_fundamentals/building_cross_platform_applications/case_study-tasky/找到。这将让您对使用 Xamarin 开发跨平台应用程序涉及的内容有一个很好的了解。

为什么不试着再玩一下我们刚刚创建的应用程序呢?看看有什么可能性,以及在处理数据库逻辑和读取用户输入方面有什么不同。Visual Studio for Mac 为开发人员打开了一个新世界,使得开发本机 iOS 应用程序比以往任何时候都更容易。

第十四章:在 Visual Studio 中编写安全代码和调试

在本章中,我们将看一些例子,作为开发人员在调试代码时更高效的方式。我们还将看看如何编写安全的代码。编写安全的代码可能是一个挑战,但请考虑以下内容:如果您的代码安全的一部分涉及确保密码安全存储,为什么要在项目之间一遍又一遍地编写代码?只需编写一次代码,然后在创建的每个新项目中实施它。我们将要看的概念如下:

  • 正确加密和存储密码

  • 在代码中使用 SecureString

  • 保护 App.config/web.config 的敏感部分

  • 防止 SQL 注入攻击

  • 使用 IntelliTrace、诊断工具和历史调试

  • 设置条件断点

  • 使用 PerfTips 识别代码中的瓶颈

介绍

许多开发人员经常忽视的一点是编写安全的代码。开发期限和其他与项目相关的压力会导致开发人员将交付代码置于正确方式之上。你们中的许多人可能不同意我,但相信我,我已经听到“我们没有预算”这样的借口太多次了。这通常是在开发预算已由其他利益相关者确定且未经开发人员咨询时发生的。

考虑这样一种情况,顾问告诉开发人员他们已经向客户出售了一个系统。现在需要开发该系统。此外,开发人员被告知他们有x小时来完成开发。给开发人员提供了一份概述需求的文件,并允许开发人员开始,并在规定的时间内完成开发。

这种情况是许多开发人员面临的现实。你可能认为这种情况不可能存在,或者你正在阅读这篇文章,并将这种情况视为你公司目前的工作流程。无论情况如何,这是今天软件开发中发生的事情。

那么,开发人员如何应对项目自杀(我将这些项目称为这样,因为像这样处理的项目很少成功)?首先要创建可重用的代码。考虑一下你经常重复的流程是否值得编写可重用的 DLL。你知道你可以创建 Visual Studio 模板吗?如果你有一个标准的项目结构,可以从中创建一个模板,并在每个新项目中重用它,从而加快交付速度并减少错误。

项目模板的一些考虑因素是数据库层、安全层、常见验证代码(此数据表是否包含任何数据?)、常见扩展方法等等。

正确加密和存储密码

我经常看到的一件事是密码存储不当。仅仅因为密码存储在服务器上的数据库中,并不意味着它是安全的。那么,密码存储不当是什么样子呢?

存储不当的安全密码不再安全。上一张截图中的密码是实际用户密码。在登录屏幕上输入第一个密码^tj_Y4$g1!8LkD将使用户访问系统。密码应该安全地存储在数据库中。实际上,您需要使用盐加密密码。您应该能够加密用户的密码,但永远不要解密它。

那么,你如何解密密码以匹配用户在登录屏幕上输入的密码?嗯,你不会。你总是对用户在登录屏幕上输入的密码进行哈希处理。如果它与存储在数据库中的他们真实密码的哈希匹配,你就允许他们访问系统。

做好准备

本食谱中的 SQL 表仅用于说明,不是由食谱中的代码编写的。可以在伴随本书源代码的“_ 数据库脚本”文件夹中找到数据库。

如何做…

  1. 最简单的方法是创建一个控制台应用程序,然后通过右键单击解决方案,选择“添加”,然后从上下文菜单中选择“新建项目”来添加一个新的类库。

  2. 从“添加新项目”对话框屏幕中,从已安装的模板中选择“类库”,并将您的类命名为Chapter15

  3. 您的新类库将添加到解决方案中,并具有默认名称Class1.cs,我们将其重命名为Recipes.cs以正确区分代码。但是,如果您觉得更合理,可以将类重命名为任何您喜欢的名称。

  4. 要重命名您的类,只需在“解决方案资源管理器”中单击类名,然后从上下文菜单中选择“重命名”。

  5. Visual Studio 将要求您确认对项目中代码元素 Class1 的所有引用的重命名。只需单击“是”。

  6. 以下类将添加到您的Chapter15库项目中:

        namespace Chapter15 
        { 
          public static class Recipes 
          { 

          } 
        }

  1. 在您的类中添加以下using语句:
        using System.Security.Cryptography;

  1. 接下来,您需要向类中添加两个属性。这些属性将存储盐和哈希值。通常,您将这些值与用户名一起写入数据库,但是,为了本示例的目的,我们将它们简单地添加到静态属性中。还要向类中添加两个方法,分别称为RegisterUser()ValidateLogin()。这两个方法都以usernamepassword变量作为参数:
        public static class Recipes 
        { 
          public static string saltValue { get; set; } 
          public static string hashValue { get; set; } 

          public static void RegisterUser(string password, string 
            username) 
          { 

          } 

          public static void ValidateLogin(string password, 
            string username) 
          {                   

          } 
        }

  1. RegisterUser()方法开始,我们做了一些事情。列出方法中的步骤:

1. 我们使用RNGCryptoServiceProvider生成一个真正随机的、密码学强的盐值。

2. 将盐添加到密码中,并使用SHA256对加盐的密码进行哈希。

在密码之前或之后添加盐都无所谓。只需记住每次都要保持一致。

3. 将盐值和哈希值与用户名一起存储在数据库中。

为了减少代码量,我实际上没有添加代码将哈希和盐值写入数据库。我只是将它们添加到之前创建的属性中。在实际情况下,您应该始终将这些值写入数据库。

这是在应用程序中处理用户密码的一种非常安全的方式:

        public static void RegisterUser(string password, string  username) 
        { 
          // Create a truly random salt using RNGCryptoServiceProvider. 
          RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider(); 
          byte[] salt = new byte[32]; 
          csprng.GetBytes(salt); 

          // Get the salt value 
          saltValue = Convert.ToBase64String(salt); 
          // Salt the password 
          byte[] saltedPassword = Encoding.UTF8.GetBytes(
            saltValue + password); 

          // Hash the salted password using SHA256 
          SHA256Managed hashstring = new SHA256Managed(); 
          byte[] hash = hashstring.ComputeHash(saltedPassword); 

          // Save both the salt and the hash in the user's database record. 
          saltValue = Convert.ToBase64String(salt); 
          hashValue = Convert.ToBase64String(hash);             
        }

  1. 我们需要创建的下一个方法是ValidateLogin()方法。在这里,我们首先获取用户名并验证。如果用户输入的用户名不正确,请不要告诉他们。这会提醒试图破坏系统的人,他们输入了错误的用户名,并且一旦他们收到错误的密码通知,他们就知道用户名是正确的。此方法中的步骤如下:

  2. 从数据库中获取输入的用户名的盐和哈希值。

  3. 使用从数据库中读取的盐对用户在登录屏幕上输入的密码进行加盐。

  4. 使用用户注册时相同的哈希算法对加盐的密码进行哈希。

  5. 将从数据库中读取的哈希值与方法中生成的哈希值进行比较。如果两个哈希值匹配,则密码被正确输入并且用户被验证。

请注意,我们从未从数据库中解密密码。如果您的代码解密用户密码并匹配输入的密码,您需要重新考虑并重写密码逻辑。系统永远不应该能够解密用户密码。

        public static void ValidateLogin(string password, string username) 
        {             
          // Read the user's salt value from the database 
          string saltValueFromDB = saltValue; 

          // Read the user's hash value from the database 
          string hashValueFromDB = hashValue; 

          byte[] saltedPassword = Encoding.UTF8.GetBytes(
            saltValueFromDB + password); 

          // Hash the salted password using SHA256 
          SHA256Managed hashstring = new SHA256Managed(); 
          byte[] hash = hashstring.ComputeHash(saltedPassword); 

          string hashToCompare = Convert.ToBase64String(hash); 

          if (hashValueFromDB.Equals(hashToCompare)) 
            Console.WriteLine("User Validated.");             
          else 
            Console.WriteLine("Login credentials incorrect. User not 
              validated.");             
        }

  1. 要测试代码,请在CodeSamples项目中添加对Chapter15类的引用。

  2. 因为我们创建了一个静态类,您可以将新的using static添加到您的Program.cs文件中:

        using static Chapter15.Recipes;

  1. 通过调用RegisterUser()方法并传递usernamepassword变量来测试代码。之后,调用ValidateLogin()方法并查看密码是否与哈希值匹配。这在真实的生产系统中显然不会同时发生:
        string username = "dirk.strauss"; 
        string password = "^tj_Y4$g1!8LkD"; 
        RegisterUser(password, username); 

        ValidateLogin(password, username); 
        Console.ReadLine();

  1. 当您调试代码时,您将看到用户已被验证:

  1. 最后,稍微修改代码,并将password变量设置为其他内容。这将模仿用户输入错误的密码:
        string username = "dirk.strauss"; 
        string password = "^tj_Y4$g1!8LkD"; 
        RegisterUser(password, username); 

        password = "WrongPassword"; 
        ValidateLogin(password, username); 
        Console.ReadLine();

  1. 当您调试应用程序时,您会发现用户未经过验证:

它是如何工作的...

在代码中我们从未解密密码。事实上,密码从未存储在任何地方。我们总是使用密码的哈希值。以下是从这个示例中得出的重要要点:

  • 永远不要使用Random类来生成您的盐。始终使用RNGCryptoServiceProvider类。

  • 永远不要在代码中重复使用相同的盐。因此,不要创建一个包含您的盐的常量,并将其用于为系统中的所有密码加盐。

  • 如果密码不匹配,永远不要告诉用户密码不正确。同样,永远不要告诉用户他们输入了错误的用户名。这可以防止发现其中一个登录凭据正确后,有人试图破坏系统。相反,如果用户名或密码输入不正确,请通知用户他们的登录凭据不正确。这可能意味着用户名或密码(或两者)输入不正确。

  • 您无法从数据库中存储的哈希或盐中获取密码。因此,如果数据库遭到破坏,其中存储的密码数据不会受到威胁。用户密码的加密是一个单向操作,意味着它永远无法被解密。同样重要的是,即使源代码被人恶意窃取,您也无法使用该代码来解密数据库中的加密数据。

  • 将上述方法与强密码策略结合起来(因为即使在 2016 年,仍然有用户认为使用'l3tm31n'作为密码就足够了),您将得到一个非常好的密码加密例程。

当我们查看用户访问表时,存储用户凭据的正确方式应该是这样的:

盐和哈希存储在用户名旁边,并且是安全的,因为它们无法被解密以暴露实际密码。

如果您在互联网上注册服务,并且他们通过电子邮件或短信向您发送确认并以纯文本显示您的密码,那么您应该认真考虑关闭您的帐户。如果系统可以读取您的密码并以纯文本形式发送给您,其他人也可以。永远不要在所有登录中使用相同的密码。

在代码中使用 SecureString

保护应用程序免受恶意攻击并不是一件容易的事。这是在编写安全代码和最小化错误(黑客通常利用的)之间不断斗争,以及黑客编写越来越复杂的方法来破坏系统和网络。我个人认为高等学府需要教授 IT 学生两件事:

  • 如何使用和集成流行的 ERP 系统

  • 适当的软件安全原则

事实上,我认为安全编程 101 不应该只是给定 IT 课程中的一个模块或主题,而应该是一个完整的课程。它需要以应有的严肃和尊重对待,并且最好由一个真正可以黑客系统或网络的人来教授。

白帽黑客教授学生如何破坏系统,利用易受攻击的代码,并渗透网络,将对未来软件开发人员的编程方式产生重大影响。开发人员需要知道在进行防御性编程时不应该做什么。有可能其中一些学生最终会成为黑帽黑客,但无论他们是否参加了关于黑客安全编程的课程,他们都会这样做。

准备就绪

代码可能在某些地方看起来有点奇怪。这是因为SecureString正在使用非托管内存存储敏感信息。请放心,SecureString在.NET Framework 中得到了很好的支持和使用,可以从创建连接到数据库时使用的SqlCredential对象的实例化中看出:

如何做...

  1. 首先,向解决方案添加一个新的 Windows 表单项目。

  2. 将项目命名为winformSecure并点击“确定”按钮。

  3. 在工具箱中,搜索文本框控件并将其添加到您的表单中。

  4. 最后,向您的表单添加一个按钮控件。您可以调整此表单的大小,使其看起来更像登录表单:

  1. 选择 Windows 表单上的文本框控件,在属性面板中打开并点击事件按钮(看起来像闪电)。在键组中,双击 KeyPress 事件以在代码后台创建处理程序:

为您创建的代码是文本框控件的 KeyPress 事件处理程序。每当用户在键盘上按键时,这将触发。

        private void textBox1_KeyPress(object sender,  KeyPressEventArgs e) 
        { 

        }

  1. 回到属性面板,展开行为组,并将 UseSystemPasswordChar 的值更改为True

  1. 在代码后台,添加以下using语句:
        using System.Runtime.InteropServices;

  1. SecureString变量作为全局变量添加到您的 Windows 表单中:
        SecureString secure = new SecureString();

  1. 然后,在KeyPress事件中,每次用户按键时将KeyChar值附加到SecureString变量中。您可能希望添加代码来忽略某些按键,但这超出了本教程的范围:
        private void textBox1_KeyPress(object sender,  KeyPressEventArgs e) 
        { 
          secure.AppendChar(e.KeyChar); 
        }

  1. 然后,在登录按钮的事件处理程序中,添加以下代码以从SecureString对象中读取值。在这里,我们正在处理非托管内存和非托管代码:
        private void btnLogin_Click(object sender, EventArgs e) 
        { 
          IntPtr unmanagedPtr = IntPtr.Zero; 

          try 
          { 
            if (secure == null) 
            throw new ArgumentNullException("Password not defined");        
            unmanagedPtr = Marshal.SecureStringToGlobalAllocUnicode(
              secure);
            MessageBox.Show($"SecureString password to validate is 
                            {Marshal.PtrToStringUni(unmanagedPtr)}"); 
          } 
          catch(Exception ex) 
          { 
            MessageBox.Show(ex.Message); 
          } 
          finally 
          { 
            Marshal.ZeroFreeGlobalAllocUnicode(unmanagedPtr); 
            secure.Dispose(); 
          } 
        }

  1. 运行您的 Windows 表单应用程序并输入密码:

  1. 然后点击登录按钮。然后您将看到您输入的密码显示在消息框中:

它是如何工作的...

对许多开发人员来说,使用System.String存储密码等敏感信息几乎成了一种习惯。这种方法的问题在于System.String是不可变的。这意味着System.String在内存中创建的对象无法更改。如果修改变量,内存中将创建一个新对象。您也无法确定System.String创建的对象在垃圾回收期间何时从内存中删除。相反,使用SecureString对象,您将加密敏感信息,并在不再需要该对象时将其从内存中删除。SecureString在非托管内存中加密和解密您的敏感数据。

现在,我需要明确一件事。SecureString绝不是绝对安全的。如果您的系统中存在一个旨在破坏SecureString操作的病毒,使用它并没有太大帮助(无论如何,请务必使用适当的防病毒软件)。在代码执行过程中,您的密码(或敏感信息)的字符串表示可能是可见的。其次,如果黑客以某种方式找到了检查您的堆或记录您的按键的方法,密码可能是可见的。然而,使用SecureString可以使黑客的这个窗口机会变得更小。机会窗口变小是因为攻击向量(黑客的入口点)减少了,从而减少了攻击面(黑客的所有攻击点的总和)。

底线是:SecureString是有其存在的理由的。作为一个关心安全的软件开发人员,您应该使用SecureString

保护 App.config/web.config 的敏感部分

作为开发人员,你无疑会处理诸如密码之类的敏感信息。在开发过程中如何处理这些信息非常重要。在过去,我曾收到客户的实时数据库副本用于测试。这确实对你的客户构成了非常真实的安全风险。

通常,我们会将设置保存在web.config文件中(在使用 Web 应用程序时)。但是,在这个例子中,我将演示一个使用App.config文件的控制台应用程序。相同的逻辑也可以应用于web.config文件。

准备工作

创建控制台应用程序是演示这个方法的最快方式。然而,如果你想使用 Web 应用程序(并保护web.config文件)进行跟随,你也可以这样做。

如何做...

  1. 在控制台应用程序中,找到App.config文件。这个文件包含了敏感数据。

  2. 如果你打开App.config文件,你会看到,在appSettings标签中,添加了一个名为Secret的键。这些信息可能本来就不应该在App.config中。问题在于它可能被提交到你的源代码控制中。想象一下在 GitHub 上?

        <?xml version="1.0" encoding="utf-8"?> 
        <configuration> 
          <startup>  
            <supportedRuntime version="v4.0" sku=".NETFramework,
             Version=v4.6.1"/> 
          </startup> 
          <appSettings> 
            <add key="name" value="Dirk"/> 
            <add key="lastname" value="Strauss"/>  
            <add key="Secret" value="letMeIn"/> 
          </appSettings> 
        </configuration>

  1. 为了克服这个漏洞,我们需要将敏感数据从App.config文件中移出到另一个文件中。为此,我们指定一个包含我们想要从App.config文件中移除的敏感数据的文件路径。
        <appSettings file="C:\temp\secret\secret.config">:

你可能会想为什么不简单地加密这些信息。嗯,这是肯定的。这个值以明文形式存在的原因只是为了演示一个概念。在现实世界的情况下,你可能会加密这个值。然而,你不希望这些敏感信息以任何形式存在于服务器的代码库中,即使它被加密了。要保险起见,将其移出你的解决方案。

  1. 当你添加了安全文件的路径后,删除包含敏感信息的键:

  1. 导航到你在App.config文件属性中指定的路径。创建你的secret.config文件并打开它进行编辑:

  1. 在这个文件中,重复appSettings部分并添加Secret键。现在发生的是,当你的控制台应用程序运行时,它会读取你解决方案中的appSettings部分,并找到对秘密文件的引用。然后它会寻找秘密文件,并将其与你解决方案中的App.config合并:

  1. 为了看到这个合并是如何工作的,添加一个引用到你的控制台应用程序。

  2. 搜索并添加System.Configuration到你的引用中:

  1. 当你添加了引用后,你的解决方案引用将列出 System.Configuration。

  2. 在你的Program.cs文件顶部,添加以下using语句:

        using System.Configuration;

  1. 添加以下代码来从你的App.config文件中读取Secret键设置。只是这一次,它将读取合并后的文件,由你的App.configsecret.config文件组成:
        string sSecret =  ConfigurationManager.AppSettings["Secret"]; 
        Console.WriteLine(sSecret); 
        Console.ReadLine();

  1. 运行你的控制台应用程序,你会看到敏感数据已经从secret.config文件中读取,并在运行时与App.config文件合并:

它是如何工作的...

我需要在这里指出的是,这种技术也适用于web.config文件。如果你需要从配置文件中删除敏感信息,将其移动到另一个文件中,这样就不会被包含在你的源代码控制检入或部署中。

防止 SQL 注入攻击

SQL 注入攻击是一个非常真实的问题。有太多的应用程序仍然使自己容易受到这种攻击。如果你开发 Web 应用程序或网站,你应该对不良的数据库操作保持警惕。易受攻击的内联 SQL 会使数据库容易受到 SQL 注入攻击。SQL 注入攻击是指攻击者通过 Web 表单输入框修改 SQL 语句,以产生与最初意图不同的结果。这通常是在 Web 应用程序应该访问数据库以验证用户登录的表单上尝试的。通过不对用户输入进行消毒,你会使你的数据容易受到这种攻击的利用。

减轻 SQL 注入攻击的可接受解决方案是创建一个带参数的存储过程,并从代码中调用它。

准备工作

在继续本示例之前,你需要在你的 SQL Server 中创建CookbookDB数据库。你可以在附带源代码的_database scripts文件夹中找到脚本。

如何做...

  1. 在这个示例中,我使用的是 SQL Server 2012。如果你使用的是较旧版本的 SQL Server,概念是一样的。在创建了CookbookDB数据库之后,你会看到Tables文件夹下有一个名为UserDisplayData的表:

  1. UserDisplayData表只是用来说明使用带参数的存储过程进行查询的概念。在生产数据库中,它不会有任何真正的好处,因为它只返回一个屏幕名称:

  1. 我们需要创建一个存储过程来选择这个表中特定 ID(用户 ID)的数据。点击Programmability节点以展开它:

  1. 接下来,右键单击Stored Procedures节点,从上下文菜单中选择New Stored Procedure...

  1. SQL Server 会为你创建以下存储过程模板。这个模板包括一个你可以对特定存储过程进行注释的部分,以及一个你可能需要添加参数的部分,显然还有一个你需要添加实际 SQL 语句的部分:
        SET ANSI_NULLS ON 
        GO 
        SET QUOTED_IDENTIFIER ON 
        GO 
        -- ============================================= 
        -- Author:          <Author,,Name> 
        -- Create date:      <Create Date,,> 
        -- Description:      <Description,,> 
        -- ============================================= 
        CREATE PROCEDURE <Procedure_Name, sysname, ProcedureName>  
            -- Add the parameters for the stored procedure here 
            <@Param1, sysname, @p1> <Datatype_For_Param1, , int> =                  <Default_Value_For_Param1, , 0>,  
            <@Param2, sysname, @p2> <Datatype_For_Param2, , int> =              <Default_Value_For_Param2, , 0> 
        AS 
        BEGIN 
        -- SET NOCOUNT ON added to prevent extra result sets      from 
        -- interfering with SELECT statements. 
        SET NOCOUNT ON; 

        -- Insert statements for procedure here 
        SELECT <@Param1, sysname, @p1>, <@Param2, sysname, @p2> 
        END 
        GO

  1. 给存储过程取一个合适的名字,描述存储过程的动作或意图:
        CREATE PROCEDURE cb_ReadCurrentUserDisplayData

有很多人在他们的存储过程中加入前缀,我就是其中之一。我喜欢把我的存储过程分组。因此,我以[prefix][tablename_or_module][stored_procedure_action]的格式命名我的存储过程。话虽如此,我通常避免使用sp_作为存储过程的前缀。关于为什么这样做是一个坏主意,互联网上有很多不同的观点。一般认为,在性能方面,使用sp_作为存储过程前缀会有影响,因为它被用作主数据库中的存储过程前缀。对于这个示例,我只是简单地给存储过程取了一个简单的名字。

  1. 为这个存储过程定义一个参数。通过这样做,你告诉数据库,当调用这个存储过程时,它将传递一个整数类型的值,存储在一个名为@userID的参数中:
        @userID INT

  1. 现在定义要由该存储过程使用的 SQL 语句。我们将只执行一个简单的SELECT语句:
        SELECT 
          Firstname, Lastname, Displayname 
        FROM 
          dbo.UserDisplayData 
        WHERE 
          ID = @userID

您会注意到我的SELECT语句包含特定的列名,而不是SELECT * FROM。使用SELECT *被认为是不良实践。通常情况下,您不希望从表中返回所有列值。如果您需要所有列值,最好明确列出列名,而不是获取所有列。使用SELECT *会返回不必要的列,并增加服务器的开销。这在更大的事情中确实会有所不同,特别是当数据库开始有很多流量时。不得不为大表的列名输入而感到期待是绝对不会发生的事情。但是,您可以使用以下技巧来使您轻松地将列名添加到您的 SQL SELECT语句中。您可以右键单击数据库表,然后选择Script Table As来创建多个 SQL 语句之一。其次,您可以展开Table节点并展开要为其编写语句的表。然后,您将看到一个名为Columns的节点。将Columns节点拖放到查询编辑器中。这将为您在查询编辑器中插入所有列名。

  1. 当您完成向存储过程添加代码后,它将如下所示:

  1. 要创建存储过程,您需要单击“执行”按钮。确保在单击“执行”按钮时选择了正确的数据库:

  1. 然后存储过程将在 SQL Server 的Stored Procedures节点下创建:

  1. 我们现在已经完成了这项任务的一半。是时候构建我们将在应用程序中使用来查询数据库的代码了。我们将直接将此代码添加到控制台应用程序的Program.cs文件中。虽然这段代码不被认为是最佳实践(硬编码服务器凭据),但它仅仅用来说明从 C#调用参数化存储过程的概念。

  2. 首先,在您的控制台应用程序顶部添加以下using语句:

        using System.Data.SqlClient;

  1. 然后添加变量以包含我们登录服务器所需的凭据:
        int intUserID = 1; 
        int cmdTimeout = 15; 
        string server = "DIRK"; 
        string db = "CookbookDB"; 
        string uid = "dirk"; 
        string password = "uR^GP2ABG19@!R";

  1. 我们现在使用SecureString来存储密码,并将其添加到SqlCredential对象中:
        SecureString secpw = new SecureString(); 
        if (password.Length > 0) 
        { 
          foreach (var c in password.ToCharArray()) secpw.AppendChar(c); 
        } 
        secpw.MakeReadOnly(); 

        string dbConn = $"Data Source={server};Initial Catalog={db};"; 
        SqlCredential cred = new SqlCredential(uid, secpw);

有关SecureString的更多信息,请参阅本章的在代码中使用 SecureString配方。

  1. 我们现在在using语句中创建一个SqlConnection对象。这确保了当using语句移出范围时,SQL 连接将被关闭:
        using (SqlConnection conn = new SqlConnection(dbConn,  cred)) 
        {                 
          try 
          { 

          } 
          catch (Exception ex) 
          { 
            Console.WriteLine(ex.Message); 
          } 
        } 
        Console.ReadLine();

  1. try内,添加以下代码以打开连接字符串并创建一个SqlCommand对象,该对象将打开的连接和存储过程的名称作为参数。您可以使用创建实际 SQL 参数的快捷方法来传递给存储过程:
        cmd.Parameters.Add("userID", SqlDbType.Int).Value = intUserID;

因为我只是向存储过程传递了一个整数类型的参数,所以我没有为这个参数定义长度:

然而,如果您需要定义VarChar(MAX)类型的参数,您需要通过添加-1来定义参数类型的大小。例如,假设您需要在数据库中存储学生的文章;则代码将如下所示:

        cmd.Parameters.Add("essay", SqlDbType.VarChar, -1).Value = 
          essayValue;

  1. 在将参数及其值添加到SqlCommand对象后,我们指定超时值,执行SqlDataReader并将其加载到DataTable中。然后将该值输出到控制台应用程序:
        conn.Open(); 
        SqlCommand cmd = new SqlCommand("cb_ReadCurrentUserDisplayData", 
          conn); 
        cmd.CommandType = CommandType.StoredProcedure; 
        cmd.Parameters.Add("userID", SqlDbType.Int).Value = intUserID; 
        cmd.CommandTimeout = cmdTimeout; 
        var returnData = cmd.ExecuteReader(); 
        var dtData = new DataTable(); 
        dtData.Load(returnData); 

        if (dtData.Rows.Count != 0) 
          Console.WriteLine(dtData.Rows[0]["Displayname"]);

  1. 在将所有代码添加到控制台应用程序后,正确的完成代码将如下所示:
        int intUserID = 1; 
        int cmdTimeout = 15; 
        string server = "DIRK"; 
        string db = "CookbookDB"; 
        string uid = "dirk"; 
        string password = "uR^GP2ABG19@!R"; 
        SecureString secpw = new SecureString(); 
        if (password.Length > 0) 
        { 
          foreach (var c in password.ToCharArray())
            secpw.AppendChar(c); 
        } 
        secpw.MakeReadOnly(); 

        string dbConn = $"Data Source={server};Initial Catalog={db};"; 

        SqlCredential cred = new SqlCredential(uid, secpw); 
        using (SqlConnection conn = new SqlConnection(dbConn, cred)) 
        {                 
          try 
          { 
            conn.Open(); 
            SqlCommand cmd = new SqlCommand(
              "cb_ReadCurrentUserDisplayData", conn); 
            cmd.CommandType = CommandType.StoredProcedure; 
            cmd.Parameters.Add("userID", SqlDbType.Int).Value = intUserID; 
            cmd.CommandTimeout = cmdTimeout; 
            var returnData = cmd.ExecuteReader(); 
            var dtData = new DataTable(); 
            dtData.Load(returnData); 
            if (dtData.Rows.Count != 0) 
              Console.WriteLine(dtData.Rows[0]["Displayname"]);  
          } 
          catch (Exception ex) 
          { 
            Console.WriteLine(ex.Message); 
          } 
        } 
        Console.ReadLine();

  1. 运行您的控制台应用程序,您将看到显示名称输出到屏幕上:

它是如何工作的...

通过创建参数化的 SQL 查询,编译器在运行 SQL 语句之前正确地替换参数。这将防止恶意数据改变您的 SQL 语句以获得恶意结果。这是因为SqlCommand对象不会直接将参数值插入语句中。

总之,使用参数化存储过程意味着不再有“小鲍比表”。

使用 IntelliTrace、诊断工具和历史调试

老式的臭虫已经成为软件开发人员和工程师 140 多年来的祸根。是的,你没看错。事实上,正是托马斯·爱迪生在 19 世纪 70 年代末创造了“臭虫”这个词。它出现在他的许多笔记中,例如他描述白炽灯仍然有许多“臭虫”的笔记中。

他为调试自己的发明所付出的努力是相当传奇的。考虑到一个已经年过六旬的人每周工作 112 小时的真正勇气和决心。他和他的七人团队(人们普遍错误地认为只有六个人,因为第七个成员没有出现在团队照片中)在为期 5 周的工作中几乎没有睡眠,因此被称为失眠小队。

如今,由于技术的进步,软件开发人员在使用调试工具(包括 Visual Studio 内外)时有着广泛的选择。那么,调试真的很重要吗?当然很重要。这是我们作为软件开发人员所做的一部分。如果我们不调试,嗯,这里有一些例子:

  • 2004 年,英国的电子数据系统EDS)子支持系统向近 200 万人过度支付,同时向近 100 万人支付不足,并导致数十亿美元的未收取子支持费。EDS 与其依赖的另一个系统之间的不兼容性导致纳税人损失,并对许多单身父母的生活产生负面影响。

  • 2012 年发布的苹果地图就足够说明问题了。虽然对许多人来说令人困惑,但当我在陌生的城市或地区时,我仍然发现自己使用谷歌地图进行导航。

  • Therac-25 放射治疗机使用电子来瞄准患者的肿瘤。不幸的是,软件中的竞争条件导致该机器向几名患者输送致命的过量辐射。

在互联网上可以找到许多影响数百万人生活的软件错误的例子。我们不仅仅谈论一般的错误。有时,我们面临看似不可逾越的问题。知道如何使用一些可用的工具是稳定应用程序和完全无法使用的应用程序之间的区别。

准备工作

请注意,IntelliTrace 仅在 Visual Studio 的企业版中可用。请参阅www.visualstudio.com/vs/compare/链接,了解 Visual Studio 各个版本之间的比较。IntelliTrace 并不是 Visual Studio 中的新功能。它已经随着时间的推移(自 Visual Studio 2010 以来)发展成为我们今天所拥有的功能。

如何做到...

  1. 首先,转到“工具”,“选项”。

  2. 展开 IntelliTrace 节点,单击“常规”。确保已选中“启用 IntelliTrace”。还要确保选择了 IntelliTrace 事件和调用信息选项。单击“确定”:

  1. Recipes.cs文件中,您可能需要添加以下using语句:
        using System.Diagnostics; 
        using System.Reflection; 
        using System.IO;

  1. Recipes类中添加一个名为ErrorInception()的方法。还要添加代码来读取基本路径,并假设有一个名为log的文件夹。不要在硬盘上创建这个文件夹。我们希望抛出一个异常。最后,添加另一个名为LogException()的方法,什么也不做:
        public static void ErrorInception() 
        { 
          string basepath = Path.GetDirectoryName(
            Assembly.GetEntryAssembly().Location); 
          var full = Path.Combine(basepath, "log"); 
        } 

        private static void LogException(string message) 
        { 

        }

  1. 在确定完整路径后,将以下代码添加到您的ErrorInception()方法中。在这里,我们尝试打开日志文件。这就是异常将发生的地方:
        try 
        { 
          for (int i = 0; i <= 3; i++) 
          { 
            // do work 
            File.Open($"{full}log.txt", FileMode.Append); 
          } 
        } 
        catch (Exception ex) 
        { 
          StackTrace st = new StackTrace(); 
          StackFrame sf = st.GetFrame(0); 
          MethodBase currentMethodName = sf.GetMethod(); 
          ex.Data.Add("Date", DateTime.Now); 
          LogException(ex.Message); 
        }

  1. 当您添加了所有代码后,您的代码应该看起来像这样:
        public static void ErrorInception() 
        { 
          string basepath = Path.GetDirectoryName(
            Assembly.GetEntryAssembly().Location); 
          var full = Path.Combine(basepath, "log"); 

          try 
          { 
            for (int i = 0; i <= 3; i++) 
            { 
              // do work 
              File.Open($"{full}log.txt", FileMode.Append); 
            } 
          } 
          catch (Exception ex) 
          { 
            StackTrace st = new StackTrace(); 
            StackFrame sf = st.GetFrame(0); 
            MethodBase currentMethodName = sf.GetMethod(); 
            ex.Data.Add("Date", DateTime.Now); 
            LogException(ex.Message); 
          } 
        } 

        private static void LogException(string message) 
        { 

        }

  1. Program.cs文件中,调用ErrorInception()方法。在那之后,进行Console.ReadLine(),这样我们的控制台应用程序将在那里暂停。不要在代码的任何地方添加断点:
        ErrorInception(); 
        Console.ReadLine();

  1. 开始调试您的应用程序。异常被抛出,应用程序继续运行,这在更复杂的应用程序中经常发生。在这一点上,您期望日志文件被附加上应用程序的虚构数据,但什么也没有发生。就在这时,您停止应用程序,并开始在代码中随意添加断点。我说随意,因为您可能不知道错误的确切位置。如果您的代码文件包含几千行代码,这一点尤其正确。现在有了 IntelliTrace 和历史调试,您只需点击“全部中断”按钮:

  1. 您的应用程序现在基本上暂停了。如果您没有看到诊断工具窗口,请按住Ctrl + Alt + F2

  2. Visual Studio 现在显示诊断工具窗口。立即,您可以看到在事件部分的红色菱形图标指示了问题。在底部的事件选项卡中,您可以点击异常:

  1. 这样做会扩展异常详细信息,您可以看到日志文件未找到。然而,Visual Studio 通过历史调试更进一步:

  1. 您将在异常详细信息底部看到一个名为“激活历史调试”的链接。点击此链接。这允许您在代码编辑器中看到导致此异常的实际代码行。它还允许您查看本地窗口、调用堆栈和其他窗口中应用程序状态的历史记录。现在您可以在代码编辑器中看到导致异常的具体代码行。在本地窗口中,您还可以看到应用程序用于查找日志文件的路径。这种调试体验非常强大,可以让开发人员直接找到错误的源头。这将提高生产力并改善代码:

它是如何工作的...

那么这里的要点是什么?如果您只记住一件事,请记住这一点。一旦您的系统的用户因为错误而失去了对该系统能力和潜力的信心,那种信心几乎不可能重新获得。即使您从错误和其他问题中复活了您的系统,制作出了一个无瑕疵的产品,您的用户也不会轻易改变主意。这是因为在他们的心目中,系统是有错误的。

我曾经接手过一部分由一位即将离开公司的资深开发人员开发的系统。她有一个出色的规格说明和一个向客户展示的精美原型。唯一的问题是,她在系统的第一阶段实施后不久就离开了公司。当出现错误时,客户自然会要求她的帮助。

告诉客户,负责与客户建立关系的开发人员已经离开公司,并不能增强信心。在这个特定项目中,只有一个开发人员参与是第一个错误。

其次,第二阶段即将由我来开发,我也是唯一被分配给这个客户的开发人员。这必须在修复第一阶段的错误的同时完成。所以,我在开发系统的新功能的同时修复错误。幸运的是,这一次我有一个名叫罗里·谢尔顿的出色项目经理作为我的搭档。我们一起被抛入深渊,罗里在管理客户期望方面做得非常出色,同时对客户完全透明地表明我们面临的挑战。

不幸的是,用户已经对提供的系统感到幻灭,并不信任这个软件。这种信任从未完全恢复。如果我们在 2007 年就有 IntelliTrace 和历史调试,我肯定能够追踪到对我来说陌生的代码库中的问题。

始终调试你的软件。当你找不到更多的错误时,再次调试。然后把系统交给我妈妈(爱你妈妈)。作为系统的开发者,你知道应该点击哪些按钮,输入哪些数据,以及事情需要以什么顺序发生。我妈妈不知道,我可以向你保证,一个对系统不熟悉的用户比你煮一杯新鲜咖啡还要快地破坏它。

Visual Studio 为开发人员提供了非常强大和功能丰富的调试工具。好好利用它们。

设置条件断点

条件断点是调试时的另一个隐藏宝石。这允许你指定一个或多个条件。当满足其中一个条件时,代码将在断点处停止。使用条件断点非常简单。

准备工作

你不需要特别准备任何东西来使用这个方法。

如何做...

  1. 在你的Program.cs文件中添加以下代码。我们只是创建了一个整数列表并循环遍历该列表:
        List<int> myList = new List<int>() { 1, 4, 6, 9, 11 }; 
        foreach(int num in myList) 
        { 
          Console.WriteLine(num); 
        } 
        Console.ReadLine();

  1. 接下来,在循环内的Console.WriteLine(num)代码上设置一个断点:

  1. 右键单击断点,然后从上下文菜单中选择条件...:

  1. 现在你会看到 Visual Studio 打开了一个断点设置窗口。在这里,我们指定断点只有在num的值为9时才会被触发。你可以添加多个条件并指定不同的条件。条件逻辑非常灵活:

  1. 调试你的控制台应用程序。你会看到当断点被触发时,num的值是9

它的工作原理...

条件在每次循环中都会被评估。当条件为真时,断点将被触发。在这个示例中,条件断点的真正好处有点失去了,因为这是一个非常小的列表。不过请考虑一下。你正在绑定一个数据网格。网格上的项目根据项目的状态给定特定的图标。你的网格包含数百个项目,因为这是一个分层网格。你确定了绑定到网格的项目的主要 ID。然后将此主要 ID 传递给其他代码逻辑来确定状态,从而确定显示的图标。

通过数百个循环按下F10进行调试并不高效。使用条件断点,你可以指定主要 ID 的值,并且只有在循环达到该值时才会中断。然后你可以直接找到显示不正确的项目。

使用 PerfTips 来识别代码中的瓶颈

PerfTips 绝对是我最喜欢的 Visual Studio 功能之一。解释它们的作用并不能充分展现它们的价值。你必须亲眼看到它们的效果。

准备工作

不要将 PerfTips 与 CodeLens 混淆。它是 Visual Studio 中与 CodeLens 分开的一个选项。

如何做...

  1. PerfTips 默认是启用的。但是以防你没有看到任何 PerfTips,转到工具 | 选项,并展开调试节点。在常规下,到设置页面的底部,你会看到一个名为在调试时显示经过时间 PerfTip 的选项。确保选中此选项:

  1. 我们将创建一些模拟长时间运行任务的简单方法。为此,我们将让线程休眠几秒钟。在Recipes.cs文件中添加以下代码:
        public static void RunFastTask() 
        { 
          RunLongerTask(); 
        } 

        private static void RunLongerTask() 
        { 
          Thread.Sleep(3000); 
          BottleNeck(); 
        } 

        private static void BottleNeck() 
        { 
          Thread.Sleep(8000); 
        }

  1. 在你的控制台应用程序中,调用静态方法RunFastTask()并在这行代码上设置一个断点:
        RunFastTask(); 
        Thread.Sleep(1000);

  1. 开始调试你的控制台应用程序。你的断点将停在RunFastTask()方法上。按F10跳过这个方法:

  1. 您会注意到 11 秒后,下一行将被突出显示,并显示 PerfTip。PerfTip 显示了上一行代码执行所花费的时间。因此,现在位于Thread.Sleep上的调试器显示RunFastTask()方法花费了 11 秒才完成。该任务显然并不是很快:

  1. 进入RunFastTask()方法后,您可以设置更多断点,并逐个跳过它们,以找到导致最长延迟的方法。正如您所看到的,PerfTips 可以让开发人员快速轻松地识别代码中的瓶颈。

工作原理...

市场上有许多工具可以做到这一点,甚至更多,允许开发人员查看各种代码指标。然而,PerfTips 可以让您在正常调试任务中逐步查看代码时即时查看问题。在我看来,这是一个必不可少的调试工具。

第十五章:在 Azure Service Fabric 上创建微服务

本章涉及微服务和Azure Service Fabric的激动人心的世界。在本章中,我们将介绍以下内容:

  • 下载和安装 Service Fabric

  • 使用无状态 actor 服务创建 Service Fabric 应用程序

  • 使用 Service Fabric Explorer

介绍

传统上,开发人员以单片方式编写应用程序。这意味着一个单一的可执行文件通过类等组件进行分解。单片应用程序需要大量的测试,由于单片应用程序的庞大,部署是繁琐的。即使您可能有多个开发团队,他们都需要对整个应用程序有扎实的了解。

微服务是一种旨在解决单片应用程序和传统应用程序开发方式所带来问题的技术。使用微服务,您可以将应用程序分解为可以独立运行的较小部分(服务),而不依赖于任何其他服务。这些较小的服务可以是无状态或有状态的,并且在功能规模上也更小,使它们更容易开发、测试和部署。您还可以独立对每个微服务进行版本控制。如果一个微服务的负载比其他微服务更大,您可以仅扩展该服务以满足其所承受的需求。对于单片应用程序,您必须尝试扩展整个应用程序以满足应用程序中的单个组件的需求。

例如,考虑一个流行的在线网络商店的运作方式。它可能包括购物车、购物者个人资料、订单管理、后端登录、库存管理、结算、退货等等。传统上,创建一个单一的 Web 应用程序来提供所有这些服务。使用微服务,您可以将每个服务隔离为独立的、自包含的功能和代码库。您还可以专门组建一个开发团队来处理网络商店的某一部分。如果这个团队负责库存管理微服务,他们将处理它的各个方面。例如,这意味着从编写代码和增强功能到测试和部署的所有工作。

微服务的另一个优点是,它可以轻松隔离您可能遇到的任何故障。最后,您还可以使用任何您想要的技术(C#,Java 和 VB.NET)创建微服务,因为它们是与语言无关的。

Azure Service Fabric 允许您轻松扩展微服务,并增加应用程序的可用性,因为它实现了故障转移。当微服务与 Service Fabric 一起使用时,微服务变得非常强大。将 Azure Service Fabric 视为您的微服务所在的平台即服务PaaS)解决方案。我们将微服务所在的集合称为 Service Fabric 集群。每个微服务都位于一个虚拟机上,这在 Service Fabric 集群中被称为节点。此 Service Fabric 集群可以存在于云中或本地机器上。如果由于任何原因节点不可用,Service Fabric 集群将自动将微服务重新分配到其他节点,以确保应用程序保持可用。

最后,关于有状态和无状态微服务之间的区别。您可以将微服务创建为无状态或有状态。当微服务依赖外部数据存储来持久化数据时,它具有无状态性质。这意味着微服务不会在内部维护其状态。另一方面,有状态微服务通过在其所在的服务器上本地存储来维护自己的状态。可以想象,有状态微服务非常适合金融交易。如果某个节点因某种原因关闭,当故障转移发生时,该交易的状态将被持久化,并在新节点上继续进行。

下载和安装 Service Fabric

在创建和测试 Service Fabric 应用程序之前,您需要在 PC 上安装和设置本地 Service Fabric 集群。本地 Service Fabric 集群是一个完全功能的集群,就像在实际环境中一样。

准备就绪

我们将从 Azure 网站下载并安装Microsoft Azure Service Fabric SDK。这将允许您在本地开发机器上创建本地 Service Fabric 集群。有关更多信息,请参阅docs.microsoft.com/en-us/azure/service-fabric/service-fabric-get-started

Service Fabric 工具现在是 Visual Studio 2017 中 Azure 开发和管理工作负载的一部分。在安装 Visual Studio 2017 时启用此工作负载。您还需要启用 ASP.NET 和 Web 开发工作负载:

请注意,如果您不再拥有 Visual Studio 的原始安装程序,并且在安装过程中没有启用 Azure 开发和管理工作负载,您仍然可以启用它。下载您拥有的 Visual Studio 2017 版本的 Web 平台安装程序并单击它。这将启动安装程序,但将允许您修改现有的 Visual Studio 2017 安装。您还可以从 Visual Studio 2017 的“新项目”对话框屏幕中运行安装程序。如果您折叠已安装的模板,您将看到一个允许您打开 Visual Studio 安装程序的部分。

除此之外,您还可以使用上述链接中的 Web 平台安装程序安装 Microsoft Azure Service Fabric SDK。它将读取安装 Microsoft Azure Service Fabric SDK。为了获得最佳的安装体验,建议使用 Internet Explorer 或 Edge 浏览器启动 Web 平台安装程序。

如何操作...

  1. 从 Microsoft Azure 网站下载 Microsoft Azure Service Fabric SDK,并通过 Service Fabric 学习路径访问其他资源,例如文档,从azure.microsoft.com/en-us/documentation/learning-paths/service-fabric/。单击 WPI 启动程序后,您应该看到以下屏幕:

  1. 在安装开始之前,您需要接受许可条款。

  2. 然后,Web 平台安装程序开始下载 Microsoft Azure Service Fabric Runtime。允许此过程完成。

  3. 下载完成后,安装过程将开始:

  1. 安装完成后,将安装以下产品,这也可以从以下屏幕截图中看出:
  • Microsoft Visual C++ 2012 SP1 可再发行包

  • Microsoft Azure Service Fabric Runtime

  • Microsoft Azure Service Fabric SDK

您的安装可能与屏幕截图不同,具体取决于您特定的预安装组件。

  1. 下一个任务是以管理员身份打开 PowerShell。在 Windows 10 开始菜单中,键入单词 PowerShell,搜索将立即返回桌面应用程序作为结果。右键单击桌面应用程序,然后从上下文菜单中选择以管理员身份运行:

  1. 一旦 Windows PowerShell 打开,运行 Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force -Scope CurrentUser 命令。原因是 Service Fabric 使用 PowerShell 脚本来创建本地开发集群。它也用于部署 Visual Studio 开发的应用程序。运行此命令可以防止 Windows 阻止这些脚本:

  1. 接下来,创建本地 Service Fabric 集群。输入 & "$ENV:ProgramFiles\Microsoft SDKs\Service Fabric\ClusterSetup\DevClusterSetup.ps1" 命令。

这将创建所需的本地集群来托管 Service Fabric 应用程序:

B06434_17_07

  1. 集群创建后,PowerShell 将启动服务:

  1. 该过程可能需要几分钟。请确保让它完成:

  1. 一旦命名服务准备就绪,您可以关闭 PowerShell:

  1. 要查看创建的集群,可以在本地机器上导航到 http://localhost:19080/Explorer

这将为您提供集群的健康和状态的快照。它还将显示集群中运行的任何应用程序:

工作原理...

正如您所看到的,Service Fabric 集群对于在 Visual Studio 中创建和运行应用程序至关重要。这将允许我们在将应用程序发布到云之前直接在本地机器上测试应用程序。正如前面提到的,这不是 Service Fabric 集群的简化版本。它与您在其中安装 Service Fabric 应用程序的任何一台机器上安装的版本完全相同。

使用无状态 Actor 服务创建 Service Fabric 应用程序

作为本章介绍的一部分,我们看了有状态和无状态微服务之间的区别。然后,可用的 Service Fabric 应用程序模板进一步分为可靠服务(有状态/无状态)和可靠 Actor。何时使用哪一个将取决于您的应用程序的具体业务需求。

简单来说,如果您想创建一个应该向您的应用程序的许多用户公开的服务,可靠的服务可能是一个很好的选择。想象一下,一个服务公开了最新的汇率,可以被许多用户或应用程序同时使用。

再次回顾本章的介绍,我们使用了在线网店和购物车的例子。对于每个购买商品的客户,可靠 Actor 可能是一个很好的选择,因此您可以有一个购物车 Actor。Service Fabric 框架中的可靠 Actor 基于虚拟 Actor 模式。请查看 research.microsoft.com/en-us/projects/orleans/ 上关于虚拟 Actor 模式的文章。

为了向您展示使用无状态 Actor 服务创建微服务有多容易,我们将使用 Visual Studio 将服务发布到 Service Fabric 集群,并从控制台(客户端)应用程序调用该服务作为示例。

做好准备

要完成此步骤,您必须确保已在本地机器上安装了 Service Fabric 集群。您还需要确保已安装了 Visual Studio 2017 中的 Azure 开发和管理工作负载。在安装 Visual Studio 2017 时启用此工作负载。如果您没有在 Visual Studio 2017 的安装中安装该工作负载,可以通过单击 Visual Studio 2017 的 Web 平台安装程序并维护安装来执行此操作。

如何做...

  1. 在 Visual Studio 中,通过转到“文件”|“新建”|“项目”来创建一个新项目。

  2. 从 Visual C#节点展开节点,直到看到 Cloud 节点。当您点击它时,您会看到 Visual Studio 现在列出了一个新的 Service Fabric 应用程序模板。选择 Service Fabric 应用程序模板,将其命名为sfApp,然后单击“确定”:

  1. 接下来,从弹出的服务模板窗口中选择 Actor Service。我们只是称之为UtilitiesActor

  1. 创建解决方案后,您会注意到它由三个项目组成:
  • sfApp

  • UtilitiesActor

  • UtilitiesActor.Interfaces

  1. 我们将首先修改UtilitiesActor.Interfaces项目中的IUtilitiesActor接口。该接口将简单要求UtilitiesActor实现一个名为ValidateEmailAsync的方法,该方法以电子邮件地址作为参数,并返回一个布尔值,指示它是否是有效的电子邮件地址:
        namespace UtilitiesActor.Interfaces 
        { 
          public interface IUtilitiesActor : IActor 
          { 
            Task<bool> ValidateEmailAsync(string emailToValidate); 
          } 
        }

  1. 接下来,打开您的UtilitiesActor项目,并查看UtilitiesActor.cs类。查找大约在第 22 行左右的内部类定义internal class UtilitiesActor:Actor,IUtilitiesActorIUtilitiesActor接口名称将被下划线标记,因为它没有实现接口成员ValidateEmailAsync()

  2. 使用Ctrl + .(句号),实现接口。删除所有其他不必要的默认代码(如果有)。

  3. 为您插入的实现接口代码应如下所示。目前,它只包含NotImplementedException。我们将在这里实现验证电子邮件地址的代码:

        namespace UtilitiesActor 
        { 
          internal class UtilitiesActor : StatelessActor, IUtilitiesActor 
          { 
            public UtilitiesActor(ActorService actorService, 
              ActorId actorId) : base(actorService, actorId)
            {
            }
            public async Task<bool> ValidateEmailAsync(string 
              emailToValidate) 
            { 
              throw new NotImplementedException(); 
            }         
          } 
        }

  1. 我们将使用正则表达式来验证通过参数传递给此方法的电子邮件地址。正则表达式非常强大。然而,在我多年的编程生涯中,我从未编写过自己的表达式。这些可以在互联网上轻松找到,并且您可以为自己的项目创建一个实用程序类(或扩展方法类)以重用。您可以利用经常使用的正则表达式和其他代码。

最后,您会注意到ActorEventSource代码。这只是为了创建Windows 事件跟踪ETW)事件,以帮助您从 Visual Studio 的诊断事件窗口中查看应用程序中发生的情况。要打开诊断事件窗口,请转到“视图”,选择“其他窗口”,然后单击“诊断事件”:

        public async Task<bool> ValidateEmailAsync(string emailToValidate)
        {
          ActorEventSource.Current.ActorMessage(this, "Email Validation");
          return await Task.FromResult(Regex.IsMatch(emailToValidate, 
          @"A(?:[a-z0-9!#$%&'*+/=?^_&grave;{|}~-]+(?:.[
          a-z0-9!#$%&'*+/=?^_&grave;{|}~-]+) *@(?:a-z0-9?.)+a-z0-9?)
          Z", RegexOptions.IgnoreCase));
        }

  1. 确保添加对System.Text.RegularExpressions命名空间的引用。如果没有引用,您将无法使用正则表达式。如果在代码中添加了正则表达式而没有添加引用,Visual Studio 将在Regex方法下显示红色波浪线。

  2. 使用Ctrl + .(句号),将using语句添加到您的项目。这将使正则表达式命名空间生效。

  3. 现在我们已经创建了接口,并添加了该接口的实现,现在是时候添加一个客户端应用程序进行测试了。右键单击解决方案,然后添加一个新项目。

  4. 最简单的方法是添加一个简单的控制台应用程序。将您的客户端应用程序命名为sfApp.Client,然后单击“确定”按钮。

  5. 将控制台应用程序添加到解决方案后,您的解决方案应如下所示:

  1. 现在,您需要向客户端应用程序添加引用。右键单击sfApp.Client项目中的References节点,然后从上下文菜单中选择添加引用。

  2. 首先要做的是向UtilitiesActor.Interfaces项目添加引用。

  3. 您还需要添加对几个 Service Fabric 动态链接库DLLs)的引用。当您创建 Service Fabric 应用程序时,它应该已经在项目文件夹结构中添加了一个名为packages的文件夹。浏览到此文件夹,并从中添加所需的 Service Fabric DLL。添加所需的 DLL 后,您的项目应如下所示:

  1. 在您的控制台应用程序的Program.cs文件中,您需要将以下代码添加到Main方法中:
        namespace sfApp.Client 
        { 
          class Program 
          { 
            static void Main(string[] args)
            {
              var actProxy = ActorProxy.Create<IUtilitiesActor>
                (ActorId.CreateRandom(), "fabric:/sfApp");
              WriteLine("Utilities Actor {0} - Valid Email?:{1}", 
              actProxy.GetActorId(), actProxy.ValidateEmailAsync(
              "validemail@gmail.com").Result);
              WriteLine("Utilities Actor {0} - Valid Email?:{1}", 
              actProxy.GetActorId(), actProxy.ValidateEmailAsync(
              "invalid@email@gmail.com").Result);
              ReadLine();
            } 
          }   
        }

确保将以下using语句添加到您的控制台应用程序中:

        using Microsoft.ServiceFabric.Actors;
        using Microsoft.ServiceFabric.Actors.Client;
        using UtilitiesActor.Interfaces;
        using static System.Console;

我们所做的就是为我们的 actor 创建一个代理,并将电子邮件验证的输出写入控制台窗口。您的客户端应用程序现在已经准备就绪。

它是如何工作的...

然而,在运行客户端应用程序之前,我们需要先发布我们的服务。在解决方案资源管理器中,右键单击sfApp服务,然后从上下文菜单中单击“发布...”:

现在将显示发布 Service Fabric 应用程序窗口。单击连接端点文本框旁边的“选择...”按钮。选择本地集群作为您的连接端点,然后单击“确定”。将目标配置文件和应用程序参数文件更改为Local.1Node.xml。完成后,单击“发布”按钮:

如果您导航到http://localhost:19080/Explorer,您会注意到您创建的服务已发布到本地的 Service Fabric 集群:

现在您已经准备好运行您的客户端应用程序。右键单击sfApp.Client项目,然后从上下文菜单中选择“调试”和“启动新实例”。控制台应用程序调用validate方法来检查电子邮件地址,并将结果显示在控制台窗口中。结果如预期的那样:

如果在尝试运行控制台应用程序时收到System.BadImageFormatException,请检查控制台应用程序的目标平台。您可能已经将控制台应用程序编译为 Any CPU,而解决方案中的其他项目则以 x64 为目标。从配置管理器中修改这一点,并使控制台应用程序也以 x64 为目标。

但是,在创建 actor ID 时,我们可以更具体。在先前的代码清单中,我们使用CreateRandom()方法生成了一个ActorId。现在我们可以给它一个特定的名称。修改您的代理代码,创建一个新的ActorId实例,并给它任何字符串值。在下面的代码清单中,我只是称呼我的为Utilities

var actProxy = ActorProxy.Create<IUtilitiesActor>(new ActorId("Utilities"), "fabric:/sfApp");

ActorId方法可以接受Guidlongstring类型的参数。

当您再次调试您的客户端应用程序时,您会注意到Utilities Actor现在有一个逻辑名称(与创建新的ActorId实例时传递的字符串值相同的名称):

在将您的 Service Fabric 应用程序本地发布之前,这是测试应用程序的完美解决方案。创建小型、独立的微服务允许开发人员在测试、调试和部署高效和健壮的代码方面获得许多好处,您的应用程序可以利用这些好处来确保最大的可用性。

使用 Service Fabric Explorer

还有另一个工具可以用来可视化 Service Fabric 集群。这是一个独立的工具,您可以通过导航到本地安装路径%Program Files%\Microsoft SDKs\Service Fabric\Tools\ServiceFabricExplorer并单击ServiceFabricExplorer.exe来找到。运行应用程序时,它将自动连接到您的本地 Service Fabric 集群。它可以显示有关集群上的应用程序、集群节点、应用程序和节点的健康状态以及集群中应用程序的任何负载的丰富信息。

准备工作

您必须已经在本地计算机上完成了 Service Fabric 的安装,才能使 Service Fabric Explorer 正常工作。如果尚未完成,请按照本章中的下载和安装 Service Fabric配方进行操作。

如何做...

  1. 当您启动 Service Fabric Explorer 时,将出现以下窗口:

  1. 请注意,左侧的树形视图显示了应用程序视图和节点视图:

  1. 右侧窗格将显示有关本地集群的信息。这使您可以轻松地查看本地服务集群的整体健康状况:

  1. 当您扩展应用程序视图时,您会注意到我们的sfApp服务已经发布。进一步扩展它,您会看到sfApp服务已经发布在 Node_3 上。扩展节点视图和 Node_3,以查看该节点上的服务活动:

  1. 为了说明微服务的可扩展性,右键单击 Node_3,并从上下文菜单中选择在节点上激活/停用和停用(删除数据)。然后,单击窗口顶部的刷新按钮以刷新节点和应用程序。

  2. 如果您现在继续扩展应用程序视图并再次查看服务,您会注意到 Service Fabric 集群注意到 Node_3 已被禁用。然后自动将服务推送到一个新的健康节点(在本例中为 Node_2):

  1. Service Fabric Explorer 右侧面板中的本地集群节点视图还报告 Node_3 已禁用。单击节点视图以查看此信息:

工作原理...

Service Fabric Explorer 将允许您查看所选节点的信息,并且您将能够深入了解有关 Service Fabric 集群应用程序的丰富信息。这只是管理员除了浏览器中可用的 Service Fabric Explorer 之外可以使用的另一个实用程序。

有一些激烈的辩论关于开发人员应该如何处理微服务架构。有人认为,当开发人员的目标是应用程序的微服务架构时,需要从单体优先的角度来处理。也就是说,首先编写大型单体应用程序,因为这个过程是熟悉的开发方法。在完成后,计划并将单体应用程序划分为更小的微服务。这里的论点是,创建单体应用程序时,上市时间更快。更快的上市时间意味着更快的投资回报。

另一方面的论点是,从单体开始恰恰是错误的方法。在设计阶段开始考虑如何将应用程序划分为部分才是正确的时间。然而,必须承认,开发团队可能需要了解他们需要构建的系统。另一个让步是,也许最好在创建现有单体的第二个版本时采用微服务方法。单体应用程序根据定义,所有部分都紧密耦合在一起。将这些部分分解为更小的微服务需要多少时间?

无论您决定采取哪种方法,都必须在仔细考虑涉及所有利益相关者的所有事实之后做出决定。不幸的是,没有公式或硬性规则可以帮助您做出决定。关于应用程序架构(单体与微服务)的决定将因项目而异。

第十六章:Azure 和无服务器计算

现在,我敢打赌,有些人来到这一章,问道:“无服务器计算到底是什么意思?”名字很令人困惑,我同意。对我来说毫无意义,但当你理解这个概念时,它有点意义。在这一章中,我们将看看无服务器计算这个术语的含义。我们还将看一下:

  • 创建 Azure 函数

  • 使用 DocRaptor 提供打印功能

  • 使用 AWS 和 S3

  • 使用 AWS 和 S3 创建 C# lambda 函数

介绍

无服务器并不意味着没有服务器,而是你(或应用程序)不知道用于为应用程序提供某些功能的服务器是哪个。因此,无服务器描述了一个依赖于云中的某些第三方应用程序或服务来为应用程序提供一些逻辑或功能的应用程序。

让我们以学生研究门户的例子来说明。学生研究某个主题并在门户中创建相关的文档。然后他们可以加载打印信用到他们的个人资料中,并打印他们需要的保存的文档。在打印一页后,打印信用将从他们的个人资料中扣除。

虽然这是一个非常简单的例子,但我用它来说明无服务器计算的概念。我们可以将应用程序分成各种组件。具体如下:

  1. 登录认证

  2. 购买打印信用

  3. 更新剩余的打印信用

  4. 打印文档

这里可能需要其他未提及的组件,但这不是现实世界。我们只是创建这个假设的应用程序来说明无服务器计算的概念。

当已经有第三方服务提供登录认证时,为什么还要在您的应用程序中编写代码来提供登录认证呢?同样,当有提供打印文档的服务时,为什么还要编写代码来打印文档呢?任何特定的功能,比如购买和加载学生打印信用,都可以使用 Azure 函数来创建。无服务器计算的主题是广泛的,而且还处于起步阶段。还有很多东西要学习和体验。让我们迈出第一步,探索这对开发人员有什么好处。

创建 Azure 函数

为什么选择 Azure Functions?想象一下,您有一个应用程序需要提供一些特定的功能,但当对函数的调用率增加时,它仍然会扩展。这就是 Azure Functions 提供的好处所在。使用 Azure Functions,您只支付函数在特定时间点所需的计算,而且它立即可用。

要开始,请访问azure.microsoft.com/en-us/services/functions并创建一个免费账户。

因为在运行 Azure Functions 时,您只支付实际使用的计算时间,所以您的代码尽可能优化是至关重要的。如果您重构 Azure Function 代码并获得了 40%的代码执行改进,那么您直接节省了 40%的月度费用。您重构和改进代码的越多,您就能节省更多的钱。

准备工作

您需要设置一个 Azure 账户。如果您还没有账户,可以免费设置一个。从 Azure 门户,在左侧菜单中,点击“新建”开始:

在搜索框中,输入“函数应用程序”并点击“Enter”按钮。第一个结果应该是函数应用程序。选择它。

当您选择函数应用程序时,您将看到右侧弹出此屏幕。描述完美地描述了 Azure Functions 的功能。在此表单底部点击“创建”按钮。

现在您看到一个表单,允许您为函数命名并选择资源组和其他设置。完成后,点击“创建”按钮。

如何做...

  1. Azure 创建新的函数应用程序后,您将能够创建 Azure 函数。我们要做的就是创建一个 Azure 函数,每当 GitHub 存储库上发生某些事情时就会触发。单击创建自定义函数链接。

根据 Microsoft Azure 网站,编写 Azure Functions 时支持以下内容:JavaScript、C#、F#以及 Python、PHP、Bash、Batch 和 PowerShell 等脚本选项。

  1. 现在您将看到可以在几个模板之间进行选择。从语言选择中选择 C#,从场景选择中选择 API 和 Webhooks,然后选择 GitHubWebHook-CSharp 模板。Azure 现在会要求您为函数命名。我将我的命名为GithubAzureFunctionWebHook。单击创建按钮创建函数。

  1. 创建函数后,您将看到在线代码编辑器中为您添加了一些默认代码。
        using System.Net;

        public static async Task<HttpResponseMessage> Run
                              (HttpRequestMessage req, TraceWriter log)
        {
          log.Info("C# HTTP trigger function processed a request.");

          // Get request body
          dynamic data = await req.Content.ReadAsAsync<object>();

          // Extract github comment from request body
          string gitHubComment = data?.comment?.body;

          return req.CreateResponse(HttpStatusCode.OK, "From Github:" +
                                    gitHubComment);
        }

  1. return语句之前,添加以下代码行:log.Info($"来自 GitHub 的消息:{gitHubComment}");。这样我们就可以看到从 GitHub 发送的内容。

  2. 您的代码现在应如下所示。请注意,有两个链接可让您获取函数 URL 和 GitHub 秘钥。单击这些链接,然后将每个值复制到记事本中。单击保存并运行按钮。

您的 Azure 函数 URL 应类似于:https://funccredits.azurewebsites.net/api/GithubAzureFunctionWebHook

  1. 转到 GitHub 网站github.com/。如果您没有帐户,请创建一个并创建一个存储库(GitHub 对开源项目免费)。转到您创建的存储库,然后单击设置选项卡。在左侧,您将看到一个名为 Webhooks 的链接。单击该链接。

  1. 现在您将看到右侧有一个名为添加 webhook 的按钮。单击该按钮。

  1. 将之前复制的 Azure Function URL 添加到 Payload URL 字段。将内容类型更改为 application/json,并将之前复制的 GitHub 秘钥添加到秘钥字段。选择 Send me everything,然后单击添加 webhook 按钮。

工作原理...

在您的 GitHub 存储库中,打开一个文件并向其添加评论。单击此提交上的评论按钮。

返回到 Azure Function 并查看日志窗口。此窗口直接位于代码窗口下方。您将看到我们在 GitHub 中发布的评论出现在 Azure Function 的日志输出中。

如果日志窗口中没有显示任何内容,请确保您已单击 Azure Function 的运行按钮。如果一切都失败了,请单击测试窗口底部的运行按钮。

虽然这只是一个非常简单的例子,但 Azure Functions 的实用性应该变得明显。您还会注意到函数具有.csx扩展名。重要的是要注意,无论您选择使用哪种编程语言编写代码,Azure Functions 都共享一些核心概念和组件。归根结底,函数是这里的主要概念。您还有一个包含 JSON 配置数据的function.json文件。您可以通过单击右侧的查看文件链接来查看此文件和其他文件。

单击function.json文件,您将看到 JSON 文件的内容。将disabled属性更改为true将有效地阻止函数在调用时执行。您还会注意到bindings属性。在这里,您可以配置您的 web hook。所有这些设置都可以在 Azure Function 的集成和其他部分中设置。

        {
          "bindings": [
            {
              "type": "httpTrigger",
              "direction": "in",
              "webHookType": "github",
              "name": "req"
            },
            {
              "type": "http",
              "direction": "out",
              "name": "res"
            }
          ],
          "disabled": false
        }

Azure Functions 和向开发人员提供的好处是一个令人兴奋的概念。这是您编程技能中的一个领域,肯定会让您忙碌很多小时,因为您将探索更复杂和复杂的任务。

使用 DocRaptor 提供打印功能

从 Web 应用程序中打印一直是棘手的。如今,由于提供打印功能的众多第三方控件的可用性,这变得更加容易。然而,现实情况是,我遇到过许多项目,在开发时使用了第三方控件来提供打印功能。当时,第三方控件很好,确实满足了他们的需求。

使应用程序具有这种功能意味着购买这些第三方控件的公司很少继续续订他们的许可证。然而,几年后,这将导致 Web 应用程序包含旧的和过时的打印技术。虽然这没有什么问题,但它确实有一些缺点。

开发人员通常被困在维护老化的代码库中,这些代码库被锁定在这个第三方控件中。一旦需求发生变化,你会发现开发人员不得不使代码在第三方控件的限制内工作。或者,他们需要向管理层提出建议,建议更新第三方控件到最新版本。这意味着在打印模块中需要的小改动,结果比任何人的预算都要昂贵。

现实世界:我曾经在一家公司工作,这家公司会让顾问为客户报价某个应用功能的更改。一旦报价得到接受,就交给开发人员在规定的时间和预算内使其工作。这导致开发人员不得不修改代码以使其工作,并满足预算和截止日期,因为缺乏适当的项目管理技能。替换第三方控件几乎是不可能的,因为预算已经在没有开发人员参与的情况下确定了。

我同意有一些开发人员在维护老化的代码库中提供和维护功能方面做得非常好。我也非常喜欢第三方控件和它们提供的功能。开发人员可以选择一些大型的供应商。但问题是:当你只需要打印发票时,为什么要购买一套第三方控件?按照这种逻辑,在许多情况下(包括本例),无服务器更有意义。

准备工作

这个示例将介绍一个名为 DocRaptor 的服务。这项服务并不是免费的,但考虑一下在您的 Web 应用程序中编写和维护提供打印功能的代码的成本。考虑购买第三方控件以提供相同的功能的成本。最终取决于作为开发人员的您选择什么才是最合理的。

创建一个基本的 Web 应用程序,然后转到工具,NuGet 包管理器,包管理器控制台。在控制台中键入以下命令以安装 DocRaptor NuGet 包。

Install-Package DocRaptor

安装了 DocRaptor 后,您可以访问他们的网页(docraptor.com/)阅读一些 API 文档,或者您也可以访问 GitHub 页面(github.com/DocRaptor/docraptor-csharp)获取更多信息。

最好查看本书附带的源代码,以便复制本示例的代码。

如何做到这一点...

  1. 添加一个包含发票详细信息的 aspx 网页。我只是从 DocRaptor 网站的示例中简单地提取并稍作修改。将此页面命名为InvoicePrint.aspx

我已经在名为invoice.css的样式表中包含了 CSS。一定要从本书附带的源代码中获取这个。

有几种方法可以处理这段代码。这并不一定是创建 Web 页面的唯一方法。如果您使用.NET Core MVC,您的方法可能会有所不同。但是,如果您这样做,请记住,这段代码只是为了说明这里的概念。

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="InvoicePrint.aspx.cs" Inherits="Serverless.InvoicePrint" %>

    <!DOCTYPE html>

    <html >
      <head runat="server">
         <title>Invoice</title>
         <meta http-equiv="content-type" content="text/html;
          charset=utf-8"/>
        <link href="css/invoice.css" rel="stylesheet" />
        <script type="text/javascript">
          function ToggleErrorDisplay()
          {
            if ($("#errorDetails").is(":visible")) {
              $("#errorDetails").hide();
            } else {
              $("#errorDetails").show();
            }
          }

          function TogglePrintResult() {
            if ($("#printDetails").is(":visible")) {
              $("#printDetails").hide();
            } else {
              $("#printDetails").show();
            }
          }
        </script>
      </head>
      <body>
        <form runat="server">
          <div id="container"> 
            <div id="main">
              <div id="header">
                <div id="header_info black">The Software Company
                  <span class="black">|</span> (072)-412-5920 
                  <span class="black">|</span> software.com</div>
              </div>
              <h1 class="black" id="quote_name">Invoice INV00015</h1>
              <div id="client" style="float: right">
                <div id="client_header">client:</div>
                <p class="address black">
                  Mr. Wyle E. Coyote
                </p>
              </div>
              <table id="phase_details">
                <thead>
                  <tr>
                    <th class="title">Stock Code</th>
                    <th class="description">Item Description</th>
                    <th class="price">price</th>
                  </tr>
                </thead>
                <tr class="first black">
                  <td>BCR902I45</td>
                  <td>Acme Company Roadrunner Catch'em Kit</td>
                  <td class="price">
                    <div class="price_container">$300</div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>Booster Skates</td>
                  <td class="price">
                    <div class="price_container">$200</div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>Emergency Parachute</td>
                  <td class="price">
                     <div class="price_container">$100</div>
                  </td>
                </tr>
                <tr class="last">
                  <td></td>
                  <td></td>
                  <td></td>
                </tr>
                <tr class="first black">
                  <td>BFT547J78</td>
                  <td>Very Sneaky Trick Seed Kit</td>
                  <td class="price">
                    <div class="price_container">$800</div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>Giant Magnet and Lead Roadrunner Seeds</td>
                  <td class="price">
                    <div class="price_container">$500</div>
                  </td>
                </tr>
                <tr>
                  <td></td>
                  <td>Rollerblades</td>
                  <td class="price">
                    <div class="price_container">$300</div>
                  </td>
                </tr>
                <tr class="last">
                  <td></td>
                  <td></td>
                  <td></td>
                </tr>
              </table>
            </div>
            <div id="total_price">
              <h2>TOTAL: <span class="price black">$1100</span></h2>
            </div>
            <div id="print_link">
              <asp:LinkButton ID="lnkPrintInvoice" runat="server"
                Text="Print this invoice" OnClick="lnkPrintInvoice_Click">
              </asp:LinkButton> 
            </div>
            <div id="errorDetails">
              <asp:Label ID="lblErrorDetails" runat="server">
              </asp:Label>
            </div>
            <div id="printDetails">
              <asp:Label ID="lblPrintDetails" runat="server">
              </asp:Label>
            </div>
          </div>
        </form>

      </body>
    </html>

  1. 我还创建了一个名为invoice.html的发票页面的打印友好版本。

  2. 下一步是为链接按钮创建一个单击事件。将以下代码添加到单击事件。您会注意到,我只是将生成 PDF 文档的路径硬编码为:C:tempinvoiceDownloads。如果您想要输出到不同的路径(或者获取相对于您所在服务器的路径),请确保更改此路径。

        Configuration.Default.Username = "YOUR_API_KEY_HERE";
        DocApi docraptor = new DocApi();

         Doc doc = new Doc(
           Test: true,
           Name: "docraptor-csharp.pdf",
           DocumentType: Doc.DocumentTypeEnum.Pdf,
           DocumentContent: GetInvoiceContent()
        );

        byte[] create_response = docraptor.CreateDoc(doc);
        File.WriteAllBytes(@"C:tempinvoiceDownloadsinvoice.pdf",
                           create_response);

  1. 确保在您的网页中包含以下命名空间:
        using System;
        using System.Web.UI;
        using DocRaptor.Client;
        using DocRaptor.Model;
        using DocRaptor.Api;
        using System.IO;
        using System.Net;
        using System.Text;

  1. 最后,获取名为invoice.html的打印友好页面的 HTML 内容。下面代码中的 URL 在您的机器上会有所不同,因为您的端口号可能不同。
        private string GetInvoiceContent()
        {
          WebRequest req = WebRequest.Create
                              ("http://localhost:37464/invoice.html");
          WebResponse resp = req.GetResponse();
          Stream st = resp.GetResponseStream();
          StreamReader sr = new StreamReader(st, Encoding.ASCII);
          return sr.ReadToEnd();
        }

它是如何工作的...

运行您的 Web 应用程序并查看在 Web 页面上显示的基本发票。确保您已将InvoicePrint.aspx页面设置为 Web 应用程序的起始页面。单击“打印此发票”链接。

您将看到发票已创建在您指定的输出路径中。

单击 PDF 文档以打开发票。

DocRaptor 为开发人员创建 Web 解决方案提供了一个非常有用的服务。如果您需要从您的应用程序创建 PDF 或 Excel 文档,DocRaptor 可以使您的团队受益。本例中使用的测试文档可免费使用,不会从您的月配额中扣除(如果您是付费计划用户)。

从真正无服务器的意义上讲,DocRaptor 为您提供功能,而无需您编写大量额外的代码。它非常容易实现,也非常容易维护。前面的例子非常基本,但您可以传递给 DocRaptor 一个 URL,而不是DocumentContent,以打印您想要的页面。从开发人员的角度来看,他们不关心 DocRaptor 是如何做到的。它只是有效。这就是无服务器计算背后的理念。

开发人员可以轻松、毫不费力地在他们的应用程序中实现解决方案,并在记录时间内使用最少的代码为他们正在开发的应用程序增加了很多价值。随着需求的增加,实施的功能也可以轻松扩展。但是,专业计划会有超额费用。最后,创建几个 PDF 文档可能不会对服务器计算能力产生太大影响。然后考虑到 DocRaptor 被一些大公司使用,这些公司可能每个月生成数千份文档。所有这些文档生成请求都不是由使用 DocRaptor 的客户处理的,而是由 DocRaptor 服务器自己处理的。

然后,您可以开发一个轻量级、简化的 Web 应用程序,随着访问量的增加,不会对您的服务器造成巨大的需求。

使用 AWS 和 S3

没有看到 Amazon Web Services(AWS)这一章就不能算完整。AWS 的主题非常广泛。该平台提供了许多功能。开发人员可以在他们的应用程序中利用这一点,并在他们自己的部分上使用最少的代码提供丰富的功能。AWS 还有非常好的文档,开发人员可以快速查看以迅速掌握。S3 是亚马逊的简单存储服务,允许您在云中存储和检索数据。

我喜欢和我的孩子们一起玩 Minecraft。他们创造的一些东西令人难以置信,尤其是因为我的女儿(以 CupcakeSparkle 的身份玩耍)只有 7 岁,而我的儿子(以 Cheetah 的身份玩耍)只有 4 岁。我的女儿从 5 岁开始玩 Minecraft,可以想象,她已经创造了相当多令人难以置信的结构。Joseph Garrett 绝对是我孩子们最喜欢的 YouTuber,他以 Stampy Cat 的身份玩耍。他们经常(包括与 Squid Nugget 一起建造时间)看他的游戏视频。我们经常举行自己的建造时间比赛,而 Stampy Cat 和他美丽的世界则成为我的孩子们在 Minecraft 中所做的一切的灵感来源。

这是我女儿建造的 Stampy Cat 的图片。

这是我儿子建造的 Squid Nugget 的图片。

因此,我想创建一个地方来上传一些他们的 Minecraft 图片、截图和与我们的 Minecraft 冒险相关的其他文档。为此,我们将使用 S3。

准备工作

本章假设您已经注册了 AWS 账户并使用了免费套餐。有关免费套餐的更多详细信息,请转到aws.amazon.com/free/。不过,我想要强调的一个部分是:

亚马逊网络服务(AWS)免费套餐旨在让您能够亲身体验 AWS 云服务。AWS 免费套餐包括在您注册 AWS 后的 12 个月内提供免费套餐的服务,以及在您的 12 个月 AWS 免费套餐期满后不会自动到期的其他服务提供。

为了注册,您需要提供您的信用卡信息。免费套餐期满后(或者如果您的应用程序超出了使用限制),您将按照按使用量付费的服务费率收费。特别是关于 S3,免费套餐允许 5GB 的存储空间,20,000 个获取请求和 2,000 个放置请求。首先,您需要创建一个 S3 存储桶。从服务选择中,找到存储组,然后点击 S3。

创建您的第一个存储桶。我将其命名为familyvaultdocs并选择了 EU(法兰克福)地区。点击下一步,直到完成存储桶的创建。

创建存储桶后,您可以查看存储桶的权限。为简单起见,我已选择让所有人对对象访问和权限访问具有读取和写入权限。

最后,您还需要为您的应用程序创建访问密钥和秘密密钥。从服务中查找安全、身份和合规性组,然后点击 IAM(身份和访问管理)。添加一个访问类型为程序访问的用户。这将为您提供所需的访问密钥 ID 和秘密访问密钥。

创建了您的存储桶,用户权限设置为所有人,并创建了访问密钥,让我们写一些代码。

如何做...

  1. 我们将创建一个控制台应用程序,将图片上传到之前创建的 S3 存储桶中。首先打开 NuGet 包管理器,并将 AWSSDK NuGet 包添加到您的控制台应用程序中。

您可能值得查看以下链接中的.NET 的 AWS SDKaws.amazon.com/sdk-for-net/。这有助于开发人员快速掌握 SDK。

  1. 接下来,创建一个名为StampysLovelyWorld的类和一个名为SaveStampy()的方法。代码真的没有什么复杂的地方。创建一个指定存储桶区域的客户端对象,创建一个指定要上传的文件、存储桶名称和目录的TransferUtilityUploadRequest对象,最后,通过TransferUtility将文件上传到存储桶。

AWS 的RegionEndpoint枚举为 EU(法兰克福)是EUCentral1。请参考 AWS 区域和端点的以下链接docs.aws.amazon.com/general/latest/gr/rande.html.

实际上,我们可能会枚举文件夹的内容,甚至允许用户选择多个文件。这个类只是为了说明将文件上传到我们的存储桶的概念。正如您将看到的,这段代码真的很简单。

        internal static class StampysLovelyWorld
        { 
          public static void SaveStampy(string fileToSave,
                                        string bucket,
                                        string bucketDirectory,
                                        string bucketFilename)
          {
            IAmazonS3 client = AWSClientFactory.CreateAmazonS3Client
                                        (RegionEndpoint.EUCentral1);

            TransferUtility utility = new TransferUtility(client); 
            TransferUtilityUploadRequest request = new 
                                    TransferUtilityUploadRequest();

            request.BucketName = bucket + "/" + bucketDirectory;
            request.Key = bucketFilename; 
            request.FilePath = fileToSave; 
            utility.Upload(request); 
          }
        }

  1. 在控制台应用程序的static void Main方法中,指定您之前创建的存储桶名称,要在存储桶中创建的文件夹以及您想要在 S3 文件夹中的文件名。将这些与文件的路径一起传递给StampysLovelyWorld类中的SaveStampy()方法。
        static void Main(string[] args)
        {
          string uploadFile = "C:UsersdirkPicturesSaved 
                               PicturesStampyCat.png";
          string S3Bucket = "familyvaultdocs"; 
          string S3Folder = "MinecraftPictures";
          string uploadedFilename = $"{DateTime.Now.ToString("yyyymmdd")}
                                      - StampyCat.png";
          StampysLovelyWorld.SaveStampy(uploadFile, S3Bucket, S3Folder,
                                        uploadedFilename);
          WriteLine("uploaded");
          ReadLine();
        }

  1. 我们需要做的最后一件事是将访问密钥和秘密密钥添加到我们控制台应用程序的 App.config 文件中。只需添加一个<appSettings>部分,并添加此处列出的密钥。您显然会使用之前在 IAM 中生成的访问密钥和秘密密钥。
        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
          <appSettings>
            <add key="AWSProfileName" value="profile1"/>
            <add key="AWSAccessKey" value="AKIAJ6Q2Q77IHJX7STWA"/>
            <add key="AWSSecretKey" value="uFBN6xtuWCSf9zR9WzQKrh1vk
                                           zU2PEuosTTy5qhc"/>
          </appSettings>
          <startup>
            <supportedRuntime version="v4.0" sku=".NETFramework,
                Version=v4.6.2" />
          </startup>
        </configuration>

  1. 运行您的控制台应用程序。文件上传后,您的控制台应用程序将在输出中显示上传的文本。

它是如何工作的...

返回到 AWS 中的familyvaultdocs存储桶,并单击欧盟(法兰克福)区域旁边的刷新图标。您将看到您在代码中指定的MinecraftPictures文件夹。

单击文件夹,您将看到列出的内容。我之前上传了SquidNugget.png图像,但我们在代码示例中上传的StampyCat.png图像已经根据代码中指定的日期前缀。

代码运行并且文件几乎立即被添加。诚然,这些文件并不是很大,但这表明了在 AWS 中添加简单存储服务并将其与.NET 应用程序集成是多么容易。

使用 AWS 创建 C# Lambda 函数

2016 年 12 月 1 日,亚马逊宣布 C#现在是 AWS Lambda 支持的语言。因此,这实际上是最新的消息,开发人员可以尝试在.NET 应用程序中使用 AWS Lambda。AWS Lambda 允许您将代码部署到 AWS,而无需担心代码运行的机器,甚至无需担心需求增加时这些机器的扩展。您的代码将正常工作。这对移动开发人员来说非常棒。直到 12 月,AWS Lambda 只支持 Node.js、Pythos 和 Java。让我们看看如何在 Visual Studio 2017 中使用 C#创建 Lambda 函数。

准备工作

您需要确保已下载并安装了 Visual Studio 2017 的 AWS Toolkit 预览版。在撰写本文时,工具包可以在以下链接找到:aws.amazon.com/blogs/developer/preview-of-the-aws-toolkit-for-visual-studio-2017/

如果您使用的是较早版本的 Visual Studio,请从此链接下载 AWS Toolkit:aws.amazon.com/visualstudio/。该工具包支持 Visual Studio 2015,并允许您下载 Visual Studio 2010-2012 和 Visual Studio 2008 的旧版本。下载并安装工具包后,您就可以创建您的第一个 AWS Lambda 函数了。

如何操作...

  1. 启动 Visual Studio 并创建一个新项目。在 Visual C#模板下,您将看到一个名为 AWS Lambda 的新类型。单击 AWS Lambda 项目(.NET Core)模板。没错,这些是.NET Core 应用程序。

  1. 下一个屏幕将允许我们选择一个蓝图。对于我们的目的,我们将选择一个简单的 S3 函数蓝图,用于响应 S3 事件通知。

  1. 函数已创建,您的 Visual Studio 中的解决方案资源管理器将如下所示。

  1. 添加到Function.cs文件的代码只是一个具有名为FunctionHandler()的方法的类。您还会注意到类顶部的程序集属性如下:[assembly: LambdaSerializerAttribute(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]。这是必需的,并注册了使用Newtonsoft.Json创建我们的类型化类的 Lambda JSON 序列化程序。由于这段代码只是起作用,我不会花太多时间来解释它。
        public async Task<string> FunctionHandler(S3Event evnt,
                                                ILambdaContext context)
        {
          var s3Event = evnt.Records?[0].S3;
          if(s3Event == null)
          {
            return null;
          }

          try
          {
            var response = await this.S3Client.GetObjectMetadataAsync
                           (s3Event.Bucket.Name, s3Event.Object.Key);
            return response.Headers.ContentType;
          }
          catch(Exception e)
          {
            context.Logger.LogLine($"Error getting object
              {s3Event.Object.Key} from bucket {s3Event.Bucket.Name}.
              Make sure they exist and your bucket is in the same
              region as this function.");
            context.Logger.LogLine(e.Message);
            context.Logger.LogLine(e.StackTrace);
            throw;
          }
        }

  1. 现在,您可以直接从 Visual Studio 中发布函数到 AWS。右键单击您创建的项目,从上下文菜单中选择发布到 AWS Lambda....

  1. 现在,您需要完成部署向导。为您的函数命名,如果您没有选择帐户配置文件,请添加一个。

对于您的 AWS Lambda 函数,请确保选择与上一篇文章中创建的 S3 存储桶相同的区域。

  1. 添加账户配置文件非常简单。这是您在 IAM 中配置的帐户。

  1. 单击“下一步”将允许您选择为 S3 和我们的函数提供访问权限的 IAM 角色名称。这是在IAM(身份和访问管理)中配置的。

  1. 单击“上传”将函数上传到 AWS。

  1. 请注意,在这一步可能会遇到几个权限问题。您可能会遇到以下内容:
Error creating Lambda function: User: arn:aws:iam::932141661806:user/S3Lambda is not authorized to perform: lambda:CreateFunction on resource: arn:aws:lambda:eu-central-1:932141661806:function:S3LambdaFunction

实际上,在尝试将函数上传到 AWS 时,您可能会收到几个此类错误。AWS 中的身份和访问管理区域在这里是您的朋友。您应该查看您正在使用的用户(在本例中是 S3Lambda)并审查分配给用户的权限。在这里,错误通知我们,用户 S3Lambda 没有权限在 AWS 上为 S3LambdaFunction 资源创建函数。修改您的权限,然后尝试重新上传。

工作原理...

将函数上传到 AWS 后,在 Visual Studio 中单击“查看”菜单,然后选择 AWS 资源管理器。展开 AWS Lambda 节点将显示我们之前上传的函数。如果在展开节点时看到错误,可能需要为您的用户提供 ListFunctions 权限。展开 AWS 身份和访问管理节点还将显示您配置的用户、组和角色。您可以通过选择一个示例请求并单击“调用”按钮在 Visual Studio 中轻松测试 AWS Lambda 函数。

然而,我们想要做的是将存储文件的 S3 连接到我们的函数以发送事件。单击“事件源”选项卡,然后单击“添加”按钮。选择 Amazon S3 作为源类型,并选择我们在上一篇文章中创建的familyvaultdocs存储桶。完成后,单击“确定”按钮。

运行上一篇文章中的控制台应用程序以将新文件上传到我们的 S3 存储桶将触发我们的 Lambda 函数。我们可以通过查看函数视图中的日志部分来确认这一点。

您还可以从 AWS 资源管理器上传文件。展开 Amazon S3 节点,然后单击“上传文件”按钮到存储桶。

您的文件已上传,并且进度显示在底部的状态窗口中。

虽然这个例子并不太复杂(除了权限设置可能有点复杂),但它确实说明了 AWS Lambda 函数的概念。我们可以使用该函数在触发来自 S3 存储桶中的事件等简单事件时执行一系列操作。开始结合功能,您可以创建一个非常强大的无服务器模块,以支持和增强您的应用程序。

无论您使用 AWS、Azure 还是诸如 DocRaptor(或任何其他第三方服务),无服务器计算都将长存下去,C# Lambda 函数将以一种重大的方式改变开发的面貌。

posted @   绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2020-05-17 HowToDoInJava Spring 教程·翻译完成
点击右上角即可分享
微信分享提示