C--编程秘籍-全-

C# 编程秘籍(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Visual Studio 2015 在创建跨各种平台的世界级应用程序时,为开发者的工具集带来了很多好处。C# 6.0 的新语言特性为开发者提供了更轻松地执行熟悉任务的方法。本书将向您展示 C#的美丽之处,当与 Visual Studio 的力量结合时,您将成为一个非常强大的开发者,能够直面各种编程挑战。

本书中的许多章节将为您提供足够的知识,以启动对所讨论主题的理解。无论您的技能水平如何,当涉及到使用 C#进行编程时,本书为每个人提供了内容。

本书涵盖内容

第一章, C# 6.0 的新特性,为您介绍了 C# 6.0 中可用的新特性。

第二章, 类和泛型,涵盖了类和泛型,它们是现代应用程序的构建块。您将了解它们是什么以及如何更有效地使用它们。

第三章, C#中的面向对象编程,涵盖了面向对象编程(OOP),这是我们以这种方式做事的原因。我们将讨论这一概念的基本原理。

第四章, 使用响应式扩展编写基于事件的程序,帮助您使用 Rx 和利用提供实时数据的力量使您的应用程序更具响应性。

第五章, 在 Azure Service Fabric 上创建微服务,展示了如何摆脱传统的应用程序开发方法。而不是一个单一的庞大应用程序,微服务将应用程序分解成更小的部分,这些部分可以独立运行。

第六章, 使用异步编程使应用响应,涵盖了使用异步编程来确保应用不会因为等待长时间运行的任务而锁定。

第七章, 使用 C#中的并行和多线程进行高性能编程,展示了您如何充分利用当今多核 CPU 提供的性能。

第八章, 代码约定,涉及编写健壮的代码,以验证传递给方法的数据的正确性。我们还将介绍代码约定,它允许开发者编写更好的代码。

第九章, 正则表达式,涵盖了正则表达式,这是一种嵌入.NET 框架的技术,在大多数书籍中经常被忽视。更好地理解它将大大有助于增加您的技能集。

第十章,选择和使用源代码控制策略,深入探讨了在不同情况下,不同的开发者在使用源代码控制时所需的不同策略。

第十一章, 在 Visual Studio 中创建移动应用程序,讨论了您可以使用 Visual Studio 做什么,这使得跨多个平台开发移动应用程序对几乎所有开发者来说都变得触手可及。

第十二章, 在 Visual Studio 中编写安全代码和调试,强调能够编写更安全的代码将使您的应用程序与众不同。能够像老板一样调试将使您脱颖而出。

第十三章, 在 Azure 中创建 Web 应用程序,展示了在 Azure 中创建 Web 应用程序是多么简单。

您需要这本书的内容

要完成本书中的代码示例,您需要 Visual Studio 2015 或更高版本的副本。大多数其他所需组件可以通过 NuGet 安装。

这本书面向的对象

本书面向对 C#编程有基本了解且熟悉 VS 2015 环境的开发者。

部分

在这本书中,您将找到几个频繁出现的标题(准备就绪、如何操作、工作原理、更多信息、相关内容)。

为了清楚地说明如何完成食谱,我们使用以下部分如下:

准备就绪

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

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包含对上一节发生情况的详细解释。

更多信息…

本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。

相关内容

本节提供了对食谱中其他有用信息的链接。

约定

在这本书中,您将找到许多用于区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称的显示方式如下:“添加一个名为CSharpSix的类。向此类添加一个名为FavoriteFeature的属性。”

代码块设置如下:

public class CSharpSix
{
    public string FavoriteFeature { get; set; }
}

任何命令行输入或输出都按照以下方式编写:

PM> Install-Package System.Reactive.Windows.Forms

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“启动 Visual Studio 2015 并点击文件菜单。然后,点击新建并选择项目。”

注意

警告或重要提示会出现在这样的框中。

小贴士

小技巧和技巧看起来像这样。

读者反馈

欢迎读者反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。

下载示例代码

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载

您还可以通过点击 Packt Publishing 网站上书籍网页上的代码文件按钮下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录您的 Packt 账户。

文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/CSharp-Programming-Cookbook。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,网址为github.com/PacktPublishing/。请查看它们!

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/CSharpProgrammingCookbook_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

互联网上版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章:C# 6.0 的新功能

在本章中,我们将介绍以下关于 C# 6.0 新功能的食谱:

  • 创建您的 Visual Studio 项目

  • 字符串插值

  • 空条件运算符

  • 自动实现属性和只读自动属性的初始化器

  • 索引初始化器

  • nameof表达式

  • 表达式体函数和属性

  • 使用static

  • 异常过滤器

  • catchfinally块中使用await运算符

简介

C#作为一种编程语言首次出现在 2000 年。其开发团队由杰出的丹麦软件工程师 Anders Hejlsberg 领导。他是 C#的首席架构师和 TypeScript 的核心开发者。C#编程语言易于使用,本书将涉及于 2015 年 7 月 20 日发布的 C# 6.0。

了解 C# 6.0 中可用的新语言功能不仅会使您成为一个更有效的开发者,还允许您在您创建的软件中实施最新的最佳实践。一个鲜为人知的事实是,在 2000 年 7 月微软的专业开发者大会上发布之前,C#实际上被称为C-like Object Oriented Language),但在发布时改名为 C#。

名称可能已经更改,但 C#仍然是一种学习和使用起来非常酷的语言。本章将向您介绍 C# 6.0 的新功能,并说明如何有效地在日常编程任务中使用这些功能。

创建您的 Visual Studio 项目

您将创建的 Visual Studio 项目将用于添加包含每个食谱中代码示例的类。该项目将是一个简单的控制台应用程序,它将调用静态类来完成食谱代码的展示,并将结果(如果有)输出到控制台窗口。

准备工作

要逐步执行本书中的食谱,您需要一个 Visual Studio 2015 的副本。如果您没有 Visual Studio 2015 的副本,您可以从www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx下载免费的 Visual Studio 2015 Community 版本。

您也可以通过导航到www.visualstudio.com/en-us/products/compare-visual-studio-2015-products-vs.aspx来比较 Visual Studio 2015 的版本。

在您下载并安装 Visual Studio 2015 之后,创建一个新的控制台应用程序,该应用程序将包含本书中展示的食谱。

如何做…

  1. 启动 Visual Studio 2015 并点击文件菜单。然后,点击新建然后选择项目。您也可以使用Ctrl + Shift + N键盘快捷键:如何做…

  2. 新建项目对话框屏幕上,选择控制台应用程序,您可以通过在左侧的树视图中导航到已安装 | 模板 | Visual C# | Windows | 经典桌面来找到它。您可以将您的控制台应用程序命名为CodeSamples如何操作…

    注意

    您会注意到选定的框架是 .NET Framework 4.6.1,这是默认选择的。在创建项目时,请保留此框架的选择。

  3. Visual Studio 现在将创建您的控制台应用程序,我们将使用它来创建本书所需的所有代码示例。

它是如何工作的…

此控制台应用程序将构成本书中食谱的基础。每个食谱都可以单独添加到这个控制台应用程序中。因此,一个食谱可以独立运行,无需创建先前的食谱。您还可以轻松地分离您可能想要添加和实验的任何自定义代码。还建议您通过添加自己的类来与代码进行交互。

字符串插值

字符串插值是一种非常简单且精确的方法,可以将变量值注入到字符串中。一个插值字符串表达式会查看包含的表达式。然后,它将这些表达式替换为表达式的结果的 ToString 表示形式。

准备工作

创建一个新的类来测试您的代码。我们将使用读取特定货币当前汇率的示例来说明如何使用字符串插值将字符串输出到用户界面。

如何操作…

  1. 通过在解决方案上右键单击,选择添加,然后从上下文菜单中选择新建项目来创建一个新的类:如何操作…

  2. 添加新项目对话框屏幕中选择类库从已安装模板,并将您的类命名为 Chapter1如何操作…

  3. 您的新类库将以默认名称 Class1.cs 添加到您的解决方案中,我将它重命名为 Recipes.cs 以便正确区分代码。但是,如果您觉得这样更有意义,您可以将类重命名为您喜欢的任何名称。

  4. 要重命名您的类,只需在解决方案资源管理器中单击类名,然后从上下文菜单中选择重命名如何操作…

  5. Visual Studio 将要求您确认重命名项目中所有对 Class1 代码元素的引用。只需单击如何操作…

  6. 现在创建的类需要使用 static 关键字使其成为静态的。同时,将代码中的类名重命名为 Recipe1StringInterpolation

    namespace Chapter1
    {
        public static class Recipe1StringInterpolation
        {
    
        }
    }
    

    注意

    注意,因此静态类不需要实例化,并且默认情况下将是密封类。这意味着它们不能进一步继承。在实践中,你通常会定义辅助或实用类为静态。这些类将经常被你的应用程序使用,例如解析日期或执行计算。这里使用 static 关键字只是为了在一个可以轻松快速从控制台应用程序中调用的类中说明 C# 6.0 的特定新功能。实际上,静态类可能不适合所有示例。

  7. 在你的类中,添加一个属性来包含基础货币:

    public static string BaseCurrency { get; private set; }
    
  8. 接下来,包括一个返回汇率的方法:

    private static decimal PreformConversion(string toCurrency)
    {
        decimal rate = 0.0m;
    
        if (BaseCurrency.Equals("ZAR"))
        {
            switch (toCurrency)
            {
                case "USD":
                    rate = 16.3040m;
                    break;
                default:
                    rate = 1.0m;
                    break;
            }
        }
    
        return rate;
    }
    
  9. 最后要添加的方法是返回插值字符串表达式的那个方法:

    public static string ReadExchangeRate(string fromCurrencyCode, string toCurrencyCode)
    {
        BaseCurrency = fromCurrencyCode;
        decimal conversion = PreformConversion(toCurrencyCode);
        return $"1 {toCurrencyCode} = {conversion} {fromCurrencyCode} ";
    }
    
  10. 现在,你需要将你创建的类连接到你的控制台应用程序。因此,你需要从控制台应用程序中添加对类的引用。在你的 CodeSamples 项目中,右键单击 引用 并选择 添加引用…如何操作…

  11. 从弹出的 参考 管理器 对话框中选择 Chapter1 解决方案以将其添加为参考。然后,点击 确定 按钮:如何操作…

  12. 在你的 CodeSamples 项目中,双击 Program.cs 文件,并将以下代码添加到 Main 方法中:

    string RandDollarExchangeRate = Chapter1.Recipe1StringInterpolation.ReadExchangeRate("ZAR", "USD");
    Console.WriteLine("The current Rand / Dollar exchange rate is:");
    Console.WriteLine(RandDollarExchangeRate);
    Console.Read();
    
  13. 要查看结果,运行你的应用程序并查看控制台应用程序的输出:如何操作…

  14. 插值字符串表达式输出为 1 USD = 16,3040 ZAR

它是如何工作的...

控制台应用程序通过调用以下代码行将南非兰特和美元的货币代码传递给静态类:Chapter1.Recipe1StringInterpolation.ReadExchangeRate("ZAR", "USD");

这个类是静态的,正如之前提到的,不需要实例化。然后 ReadExchangeRate 方法读取汇率并将其格式化为合适的字符串,使用字符串插值。你会注意到插值字符串表达式被写成 $"1 {toCurrencyCode} = {conversion} {fromCurrencyCode} ";

toCurrencyCodeconversionfromCurrencyCode 变量直接在字符串表达式中表示。这是一个格式化字符串的更简单方法,因为你可以不用 String.Format(在 C# 的早期版本中使用),同样的表达式将被写成 String.Format("1 {0} = {1} {2} ", toCurrencyCode, conversion, fromCurrencyCode);

如你所见,插值字符串表达式更容易阅读和编写。然而,实际上,字符串插值仅仅是语法糖,因为编译器仍然将表达式视为 String.Format。你可能想知道在使用字符串插值时如何表示花括号。为此,你可以在表达式中简单地使用双花括号。如果你需要将汇率表示为 {16,3040},你需要将其表示为 $"{{{conversion}}}";

你也可以在插值字符串表达式中直接格式化字符串。如果你返回 $"The date is {DateTime.Now}"; 表达式,输出将是 The date is 2016/01/10 3:04:48 PM。你可以继续修改表达式,使用冒号后跟的格式来格式化日期。将代码更改为 $"The date is {DateTime.Now : MMMM dd, yyyy}";。输出将被格式化,并产生 The date is January 5, 2016

另一个很好的技巧是,你可以在字符串表达式中表达一个条件。考虑以下代码行,它确定一个年份是否是闰年:

$"The year {DateTime.Now.Year} {(DateTime.IsLeapYear(DateTime.Now.Year) ? " is " : " is not ")} a leap year.";

我们可以将三元 ? 操作符进一步使用。考虑以下代码行:

$"There {(StudentCount > 1 ? "are " : "is ")}{StudentCount} student{(StudentCount > 1 ? "s" : "")} in the list."

由于冒号用于表示格式化,我们必须将表达式的条件部分用括号括起来。字符串插值是表达代码中易于阅读和理解的字符串的一种非常好的方式。

空条件操作符

开发者能做的最糟糕的事情就是在代码中不检查 null。这意味着没有对象的引用,换句话说,存在一个 null。引用类型变量有一个默认值 null。另一方面,值类型不能为 null。在 C# 2 中,开发者被引入了可空类型。为了确保对象不是 null,开发者通常编写一些复杂的 if 语句来检查对象是否为 null。C# 6.0 通过引入空条件操作符使这个过程变得非常简单。

它通过写入 ?. 来表示,被称为问号点操作符。问号紧跟在实例之后,在通过点调用属性之前书写。可以这样理解空条件操作符:如果操作符的左侧是 null,整个表达式就是 null。如果左侧不是 null,则调用属性并成为操作的结果。真正看到空条件操作符的强大之处,就是看到它在实际中的应用。

准备工作

我们将创建另一个类来展示空条件操作符的使用。该方法将调用 Student 类来返回结果列表中的学生数量。在返回学生数量之前,我们将检查 Student 类是否有效。

如何做到这一点...

  1. 创建你的 Visual Studio 项目 菜单中,在最后一个你编写的类下面创建另一个名为 Recipe2NullConditionalOperator 的类:

    public static class Recipe2NullConditionalOperator
    {
    
    }
    
  2. 将名为GetStudents的方法添加到类中,并向其中添加以下代码:

    public static int GetStudents()
    {
        List<Student> students = new List<Student>(); 
        Student st = new Student();
    
        st.FirstName = "Dirk";
        st.LastName = "Strauss";
        st.JobTitle = "";
        st.Age = 19;
        st.StudentNumber = "20323742";
        students.Add(st);
    
        st.FirstName = "Bob";
        st.LastName = "Healey";
        st.JobTitle = "Lab Assistant";
        st.Age = 21;
        st.StudentNumber = "21457896";
        students.Add(st);
    
        return students?.Count() ?? 0;            
    }
    
  3. 接下来,向你的代码中添加一个名为Student的第三个类,具有以下属性:

    public class Student
    {
        public string StudentNumber { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public string JobTitle { get; set; }
    }
    
  4. 我们的Student类将是我们在GetStudents方法中调用的对象。在Program.cs文件中,添加以下代码:

    int StudentCount = Chapter1.Recipe2NullConditionalOperator.GetStudents();
                if (StudentCount >= 1)
                    Console.WriteLine($"There {(StudentCount > 1 ? "are " : "is ")}{StudentCount} student{(StudentCount > 1 ? "s" : "")} in the list.");
                else
                    Console.WriteLine($"There were {StudentCount} students contained in the list.");
                Console.Read();
    
  5. 运行控制台应用程序会导致应用程序告诉我们列表中有两个学生。这是预期的,因为我们向我们的List<Student>类中添加了两个Student对象:如何做到这一点…

  6. 要查看空条件运算符的实际应用,修改你的GetStudents方法中的代码,将students变量设置为空。你的代码应该看起来像这样:

    public static int GetStudents()
    {
        List<Student> students = new List<Student>(); 
        Student st = new Student();
    
        st.FirstName = "Dirk";
        st.LastName = "Strauss";
        st.JobTitle = "";
        st.Age = 19;
        st.StudentNumber = "20323742";
        students.Add(st);
    
        st.FirstName = "Bob";
        st.LastName = "Healey";
        st.JobTitle = "Lab Assistant";
        st.Age = 21;
        st.StudentNumber = "21457896";
        students.Add(st);
    
        students = null;
        return students?.Count() ?? 0;            
    }
    
  7. 再次运行控制台应用程序,看看输出是如何变化的:如何做到这一点…

它是如何工作的…

考虑我们在return语句中使用的代码:

return students?.Count() ?? 0;

我们告诉编译器检查List<Student>类的变量students是否为空。我们通过在students对象后添加?来实现这一点。如果students对象不为空,我们使用点运算符,Count()属性成为语句的结果。

如果students对象为空,则返回零。这种方式检查空值使得所有if(students != null)代码都变得不必要。空值检查似乎淡入背景,使得表达和阅读空值检查变得更容易(更不用说代码更少了)。

如果我们必须将return语句更改为没有空条件运算符的常规Count()方法,我们会看到一个ArgumentNullException was unhandled错误:

return students.Count();

students对象上调用Count()而不使用空条件运算符会破坏代码。空条件运算符是 C#语言的激动人心的补充,因为它使得编写检查空值的代码变得更加容易。代码越少,代码质量越好。

自动实现的属性和只读自动属性的初始化器

C# 6.0 的发布为自动实现的属性带来了两项增强。你现在可以内联初始化自动实现的属性,也可以定义它们而不需要设置器。

准备工作

为了说明如何实现这两个新的自动实现属性增强,我们将创建另一个类,该类计算给定条形码和折扣类型的折扣后的销售价格。

如何做到这一点…

  1. 首先,创建一个名为Recipe3AutoImplementedProperties的静态类,并将DiscountType枚举器添加到该类中,以及自动实现的属性。然后,将这些自动实现的属性初始化为默认值:

    public static class Recipe3AutoImplementedProperties
    {
        public enum DiscountType { Sale, Clearout, None }
        private static int SaleDiscountPercent { get; } = 20;
        private static int ClearoutDiscountPercent { get; } = 35;
        public static decimal ShelfPrice { get; set; } = 100;
        public static decimal SalePrice { get; set; } = 100;
    }
    
  2. 下一步是添加一个方法来计算与提供给方法的条形码相关联的商品的销售价格:

    public static void CalculateSalePrice(string barCode, DiscountType discount)
    {
      decimal shelfPrice = GetPriceFromBarcode(barCode);
    
      if (discount == DiscountType.Sale)
        SalePrice = (shelfPrice == 0 ? ShelfPrice.CalculateSalePrice(SaleDiscountPercent) : shelfPrice.CalculateSalePrice(SaleDiscountPercent));
    
      if (discount == DiscountType.Clearout)
       SalePrice = (shelfPrice == 0 ? ShelfPrice.CalculateSalePrice(ClearoutDiscountPercent): shelfPrice.CalculateSalePrice(ClearoutDiscountPercent));
    
      if (discount == DiscountType.None)
        SalePrice = (shelfPrice == 0 ? ShelfPrice :shelfPrice);
    }
    
  3. 为了模拟数据库查找以找到条形码的销售价格,创建另一个方法来返回给定条形码的价格:

    private static decimal GetPriceFromBarcode(string barCode)
    {            
        switch (barCode)
        {
            case "123450":
                return 19.95m;                    
            case "123451":
                return 7.55m;
            case "123452":
                return 59.99m;
            case "123453":
                return 93.99m;
            default:
                return 0;
        }
    }
    
  4. 最后,我们将创建一个扩展方法类来计算折扣后适用的销售价格:

    public static class ExtensionMethods
    {
        public static decimal CalculateSalePrice(this decimal shelfPrice, int discountPercent)
        {
            decimal discountValue = (shelfPrice / 100) * discountPercent;
            return shelfPrice - discountValue;
        }
    }
    

    注意

    扩展方法默认是静态方法,允许你扩展你代码的功能(扩展现有类型)而无需修改原始类型。你现在可以在你的解决方案中有一个扩展方法类,在那里你可以添加有用的代码。使用扩展方法的一个好例子是计算给定日期的财政年度。扩展方法与其他静态方法的不同之处在于方法签名中使用 this 关键字。在前面的例子中,编译器通过查看它扩展的类型知道这是一个为 decimal 类提供的扩展方法。

  5. 替换你的 Program.cs 文件中的代码并运行程序:

    string BarCode = String.Empty;
    
    BarCode = "123450";
    Chapter1.Recipe3AutoImplementedProperties.CalculateSalePric e(BarCode, Chapter1.Recipe3AutoImplementedProperties.DiscountType.Sale );
    Console.WriteLine(Chapter1.Recipe3AutoImplementedProperties .SalePrice);
    
  6. 在应用折扣后,销售价格被计算并返回到控制台应用程序:如何操作…

它是如何工作的…

如果你再次查看自动实现的属性,你会注意到我们有两个只读自动实现的属性。所有四个自动实现的属性都已使用默认值初始化。SaleDiscountPercentClearoutDiscountPercent 属性是只读的。这确保了折扣值不能以任何方式修改。

你还会注意到,如果从 GetPriceFromBarcode 方法返回的货架价格是零,那么在确定折扣价格时将使用默认的 ShelfPrice 属性值。如果没有应用折扣,CalculateSalePrice 方法将直接返回条形码价格。如果没有从条形码中确定价格,则返回默认的 ShelfPrice 属性值。

自动实现的属性初始化器和只读自动实现的属性可以大大减少不必要的 if else 语句。它还使实现属性的代码更易于阅读,因为意图可以通过初始化属性本身来包含。

看看如果我们尝试将 SaleDiscountPercentClearoutDiscountPercent 属性设置为不同的值会发生什么:

如何工作…

Visual Studio 将为只读属性发出错误,因为我们只能使用 get 关键字从这个属性中读取,而不能为其赋值。

索引初始化器

你需要记住,C# 6.0 并没有引入大的新概念,而是引入了一些旨在使你的代码更简洁、更易于阅读和理解的小特性。使用索引初始化器,这也不例外。你现在可以初始化新创建对象的索引。这意味着你不需要使用单独的语句来初始化索引。

准备工作

这里的变化很微妙。我们将创建一个方法来根据整数返回星期几。我们还将创建一个方法来返回财务年度的开始月份和薪资增长月份,然后设置薪资增长月份为一个不同于默认值的值。最后,我们将使用属性将特定类型的物种返回到控制台窗口。

如何操作…

  1. 首先,创建一个名为 Recipe4IndexInitializers 的新类,并将第二个名为 Month 的类添加到您的代码中。Month 类仅包含两个已初始化的自动实现属性。StartFinancialYearMonth 已设置为二月(2 月),而 SalaryIncreaseMonth 已设置为三月(3 月):

    public static class Recipe4IndexInitializers
    {
    
    }
    
    public class Month
    {
        public int StartFinancialYearMonth { get; set; } = 2;
        public int SalaryIncreaseMonth { get; set; } = 3;
    }
    
  2. 继续添加一个名为 ReturnWeekDay 的方法,该方法接受一个整数作为参数,用于表示天数,到 Recipe4IndexInitializers 类中:

    public static string ReturnWeekDay(int dayNumber)
    {
        Dictionary<int, string> day = new Dictionary<int, string>
        {
            [1] = "Monday",
            [2] = "Tuesday",
            [3] = "Wednesday",
            [4] = "Thursday",
            [5] = "Friday",
            [6] = "Saturday",
            [7] = "Sunday"
        };
    
        return day[dayNumber];
    }
    
  3. 对于第二个示例,将一个名为 ReturnFinancialAndBonusMonth 的方法添加到 Recipe4IndexInitializers 类中:

    public static List<int> ReturnFinancialAndBonusMonth()
    {
        Month currentMonth = new Month();
        int[] array = new[] { currentMonth.StartFinancialYearMonth, currentMonth.SalaryIncreaseMonth };
        return new List<int>(array) { [1] = 2 };  
    }
    
  4. 最后,向类中添加几个自动实现的属性以包含物种,并在 Recipe4IndexInitializers 类中添加一个名为 DetermineSpecies 的方法。您的代码应如下所示:

    public static string Human { get; set; } = "Homo sapiens";
    public static string Sloth { get; set; } = "Choloepus hoffmanni";
    public static string Rabbit { get; set; } = "Oryctolagus cuniculus";
    public static string Mouse { get; set; } = "Mus musculus";
    public static string Hedgehog { get; set; } = "Erinaceus europaeus";
    public static string Dolphin { get; set; } = "Tursiops truncatus";
    public static string Dog { get; set; } = "Canis lupus familiaris";
    
    public static void DetermineSpecies()
    {
        Dictionary<string, string> Species =  new Dictionary<string, string>
        {
            [Human] = Human + " : Additional species information",
            [Rabbit] = Rabbit + " : Additional species information",
            [Sloth] = Sloth + " : Additional species information",
            [Mouse] = Mouse + " : Additional species information",
            [Hedgehog] = Hedgehog + " : Additional species information",
            [Dolphin] = Dolphin + " : Additional species information",
            [Dog] = Dog + " : Additional species information"
        };
    
        Console.WriteLine(Species[Human]);            
    }
    
  5. 在您的控制台应用程序中,添加以下代码以调用 Recipe4IndexInitializers 类中的代码:

    int DayNumber = 3;
    string DayOfWeek = Chapter1.Recipe4IndexInitializers.ReturnWeekDay(DayNumber);
    Console.WriteLine($"Day {DayNumber} is {DayOfWeek}");
    
    List<int> FinancialAndBonusMonth = Chapter1.Recipe4IndexInitializers.ReturnFinancialAndBonusMo nth();
    Console.WriteLine("Financial Year Start month and Salary Increase Months are:");
    for (int i = 0; i < FinancialAndBonusMonth.Count(); i++)
    {
        Console.Write(i == 0 ? FinancialAndBonusMonth[i].ToString() + " and " : FinancialAndBonusMonth[i].ToString());
    }
    
    Console.WriteLine();
    Chapter1.Recipe4IndexInitializers.DetermineSpecies();
    Console.Read();
    
  6. 一旦添加了所有代码,运行你的应用程序。输出将如下所示:如何操作…

它是如何工作的…

第一个方法 ReturnWeekDay 创建了一个 Dictionary<int, string> 对象。您可以看到索引是如何用星期名称初始化的。如果我们现在将天数整数传递给该方法,我们可以通过引用索引来返回星期名称。

注意

ReturnWeekDay 中不使用零基索引的原因是因为一周的第一天与数值 1 相关联。

在第二个示例中,我们调用了一个名为 ReturnFinancialAndBonusMonth 的方法,该方法创建一个数组来存储财务年度开始月份和薪资增长月份。Month 类的两个属性分别初始化为 23。您可以看到,我们正在覆盖 SalaryIncreaseMonth 属性的值并将其设置为 2。这是在以下代码行中完成的:

return new List<int>(array) { [1] = 2 };

最后一个示例使用 HumanRabbitSlothMouseHedgehogDolphinDog 属性来返回 Species 对象的正确索引值。

nameof 表达式

nameof 表达式特别有用。现在您可以为代码中命名的对象提供一个字符串。如果您正在抛出异常,这尤其方便。现在您可以看到哪个变量引发了异常。在过去,开发者必须依赖于代码中的混乱字符串字面量。这特别容易出错,并且容易受到拼写错误的影响。另一个问题是,任何代码重构都可能错过一个字符串字面量,然后该代码就变得过时并损坏。

nameof 表达式已经拯救了我们的困境。编译器会看到你正在引用特定变量的名称,并将其正确地转换为字符串。因此,nameof 表达式也与任何你可能进行的重构保持同步。

准备工作

我们将使用本章中 字符串插值 菜单中编写的相同代码示例,并进行一些小的修改。我们将创建一个 Student 对象并向其中添加学生。然后,我们将该对象返回到控制台并输出学生数量。

如何操作…

  1. 创建一个名为 Recipe5NameofExpression 的类。向该类添加一个名为 StudentCount 的自动实现属性:

    public static class Recipe5NameofExpression
    {
        public static int StudentCount { get; set; } = 0;
    }
    
  2. 接下来,我们需要添加一个名为 GetStudents 的方法,该方法返回一个 List<Student> 对象。该方法包含一个 try/catch 语句,并将抛出 ArgumentNullException()

    public static List<Student> GetStudents()
    {
        List<Student> students = new List<Student>();
        try
        {                
            Student st = new Student();
    
            st.FirstName = "Dirk";
            st.LastName = "Strauss";
            st.JobTitle = "";
            st.Age = 19;
            st.StudentNumber = "20323742";
            students.Add(st);
    
            st.FirstName = "Bob";
            st.LastName = "Healey";
            st.JobTitle = "Lab Assistant";
            st.Age = 21;
            st.StudentNumber = "21457896";
            students.Add(st);
    
            //students = null;
    
            StudentCount = students.Count();
    
            return students;
        }
        catch (Exception ex)
        {
            throw new ArgumentNullException(nameof(students));
        }
    }
    

    注意

    实际上,我们不会立即简单地返回 ArgumentNullException。这只是为了说明在 ArgumentNullException 中使用 nameof 表达式的概念。

  3. 在控制台应用程序中,我们将添加代码以返回 List<Student> 对象,并通过将 StudentCount 属性值输出到控制台窗口来报告列表中包含的学生数量:

    try
    {
        List<Chapter1.Student> StudentList = Chapter1.Recipe5NameofExpression.GetStudents();
        Console.WriteLine($"There are {Chapter1.Recipe5NameofExpression.StudentCount} students");                
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        Console.Read();
    }
    

工作原理…

以当前代码运行控制台应用程序将调用 GetStudents() 方法。这将创建一个 List<Student> 对象,并向其中添加两个 Student 对象。StudentCount 属性设置为 List<Student> 对象的计数。然后 GetStudents() 方法将结果返回到控制台应用程序,该应用程序读取 StudentCount 属性并在控制台输出中显示它:

工作原理…

如果我们现在修改 GetStudents() 方法中的代码,在调用 students.Count() 之前将 students 变量设置为 null,将会抛出异常。异常在 catch 中被捕获,这就是我们使用 nameof 表达式来显示 students 变量的字符串字面量的地方:

工作原理…

使用 nameof 表达式,我们可以确保表达式与重构操作(如重命名 students 变量)保持同步:

工作原理…

如果我们在 catch 语句中使用字符串字面量编写代码,那么当我们重命名 students 变量时,代码不会自动更新。nameof 表达式有效地允许开发者停止编写 throw new ArgumentNullException("students");,这将不会受到重构操作的影响。

在你的代码中使用 nameof 表达式的好处之一是它不涉及任何运行时成本,因为包含字符串字面量的代码是在编译时生成的。

稍微修改控制台应用程序中的代码,使其看起来像这样:

List<Chapter1.Student> StudentList = Chapter1.Recipe5NameofExpression.GetStudents();

int iStudentCount = Chapter1.Recipe5NameofExpression.StudentCount;
Console.WriteLine($"The value of the { nameof(Chapter1.Recipe5NameofExpression.StudentCount)} property is {iStudentCount}");

当你现在运行你的控制台应用程序时,你可以看到已经使用了 nameof 表达式来创建 StudentCount 属性的字符串字面量:

如何工作…

注意

确保你在 GetStudents() 方法中注释掉了 students = null; 这行代码;否则,你仍然会收到空异常。

你还可以使用 nameof 表达式与枚举一起使用。将以下代码添加到你的类中。我们基本上创建了一个名为 Course 的枚举。在 SetCourse() 方法中,我们根据课程 ID 设置一个课程:

public enum Course { InformationTechnology = 1, Statistics = 2, AppliedSciences = 3 }
public static string SelectedCourse { get; set; }
public static void SetCourse(int iCourseID)
{
    Course course = (Course)iCourseID;
    switch (course)
    {
        case Course.InformationTechnology:
            SelectedCourse = nameof(Course.InformationTechnology);
            break;
        case Course.Statistics:
            SelectedCourse = nameof(Course.InformationTechnology);
            break;
        case Course.AppliedSciences:
            SelectedCourse = nameof(Course.InformationTechnology);
            break;
        default:
            SelectedCourse = "InvalidCourse";
           break;
   }            
}

然后,我们使用 switch 语句根据课程 ID 参数选择定义的课程,并将 SelectedCourse 属性设置为枚举的 nameof 表达式。将以下代码添加到你的控制台应用程序中:

Chapter1.Recipe5NameofExpression.SetCourse(1);
Console.WriteLine($"The selected course is { Chapter1.Recipe5NameofExpression.SelectedCourse}");

运行控制台应用程序将导致所选枚举值的字符串表示形式:

如何工作…

nameof 表达式是处理 C# 6.0 中对象的字符串字面量时保持代码同步的一种非常好的方法。

表达式主体函数和属性

如其名所示,表达式主体函数和属性允许方法和属性拥有一个表达式主体而不是语句主体。你会注意到表达式主体成员看起来很像 lambda 表达式,因为它们受到了 lambda 表达式的启发。

准备工作

要真正欣赏表达式主体函数和属性,我们需要看看之前代码是如何编写的。我们将创建一个类来计算商品的销售价格,该类将包含两个公共方法。一个用于设置货架价格,另一个用于返回显示计算出的销售价格的消息。

如何实现…

  1. 创建一个名为 Recipe6ExpressionBodiedFunctionMembers 的类,并添加两个私有自动实现的属性来存储销售折扣百分比和货架价格:

    public static class Recipe6ExpressionBodiedFunctionMembers
    {
        private static int SaleDiscountPercent { get; } = 20;
        private static decimal ShelfPrice { get; set; } = 100;
    }
    
  2. 如果你还没有在之前的菜谱中这样做,请添加一个扩展方法类来计算商品的销售价格:

    public static class ExtensionMethods
    {
        public static decimal CalculateSalePrice(this decimal shelfPrice, int discountPercent)
        {
            decimal discountValue = (shelfPrice / 100) * discountPercent;
            return shelfPrice - discountValue;
        }
    }
    
  3. 现在,我们将向类中添加一个计算属性。这个计算属性使用 ShelfPrice 属性上的扩展方法来获取销售价格:

    private static decimal GetCalculatedSalePrice
    {
        get { return Math.Round(ShelfPrice.CalculateSalePrice(SaleDiscountPercen t) ,2); } 
    }
    
  4. 最后,向你的类中添加两个方法来设置货架价格,另一个方法用于返回带有销售价格的消息:

    public static void SetShelfPrice(decimal shelfPrice)
    {
        ShelfPrice = shelfPrice;
    }                
    
    public static string ReturnMessage(string barCode)
    {
        return $"The sale price for barcode {barCode} is {GetCalculatedSalePrice}";
    }
    
  5. 要查看代码的结果,请将以下代码添加到你的控制台应用程序中:

    string BarCode = "12345113";
    decimal ShelfPrice = 56.99m;
    Chapter1.Recipe6ExpressionBodiedFunctionMembers.SetShelfPri ce(ShelfPrice);            Console.WriteLine(Chapter1.Recipe6ExpressionBodiedFunctionM embers.ReturnMessage(BarCode));
    Console.Read();
    

如何工作…

运行你的应用程序将显示计算出的销售价格的消息:

如何工作…

注意

在这里,我们只是在输出消息中提供了条形码。然而,在实际系统中,货架价格将根据特定的条形码从数据存储中查找。

回顾我们的类,我们可以看到它有些庞大。我们有一个返回销售价格的计算属性,以及两个只有一个 return 语句的方法。一个用于设置货架价格,另一个获取包含销售价格的消息。这就是表达式主体函数成员发挥作用的地方。将 Recipe6ExpressionBodiedFunctionMembers 类中的代码修改如下:

public static class Recipe6ExpressionBodiedFunctionMembers
{
    private static int SaleDiscountPercent { get; } = 20;
    private static decimal ShelfPrice { get; set; } = 100;

    private static decimal GetCalculatedSalePrice => Math.Round(ShelfPrice.CalculateSalePrice(SaleDiscountPercent));

    public static void SetShelfPrice(decimal shelfPrice) => ShelfPrice = shelfPrice;

    public static string ReturnMessage(string barCode) => $"The sale price for barcode {barCode} is {GetCalculatedSalePrice}";        
}

我们剩下的是一个简洁的类,它与我们之前编写的代码完全一样。代码更少,更容易阅读,看起来也更干净。您会注意到使用了 lambda => 操作符。对于 GetCalculatedSalePrice 计算属性,get 关键字是缺失的。当我们将计算属性体更改为表达式时,它就隐含了。

但是,有一点需要注意,表达式主体函数成员不支持构造函数。

使用静态

C# 6.0 引入了一种新的 using 语句,现在它引用的是类型而不是命名空间。这意味着类型的静态成员将直接放入作用域。这对于您的代码意味着什么,可以从这个菜谱的简化结果中明显看出。

准备工作

我们将创建一个名为 Recipe7UsingStatic 的类,该类将根据星期几确定商品的销售价格。如果是星期五,我们将对商品应用销售折扣。在其他任何一天,我们将以货架价格出售商品。

如何实现...

  1. 首先创建一个名为 Recipe7UsingStatic 的类,该类包含两个自动实现的属性和一个表示星期的枚举:

    public static class Recipe7UsingStatic
    {
        public enum TheDayOfWeek
        {
            Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
        }
    
        private static int SaleDiscountPercent { get; } = 20;
        private static decimal ShelfPrice { get; set; } = 100;        
    }
    
  2. 现在我们将向 Recipe7UsingStatic 类中添加一个计算属性和两个方法。一个方法用于设置货架价格,另一个方法用于获取销售价格:

    private static decimal GetCalculatedSalePrice
    {
        get { return Math.Round(ShelfPrice.CalculateSalePrice (SaleDiscountPercen t), 2); }
    }        
    
    public static void SetShelfPrice(decimal shelfPrice)
    {
        ShelfPrice = shelfPrice;
    }
    
    public static decimal GetSalePrice(TheDayOfWeek dayOfWeek)
    {
        return dayOfWeek == TheDayOfWeek.Friday ? GetCalculatedSalePrice : ShelfPrice;
    }
    
  3. 在控制台应用程序中,我们将添加代码来定义星期几,设置货架价格,然后获取销售价格。然后,将销售价格写入控制台应用程序:

    decimal ShelfPrice = 56.99m;
    
    Chapter1.Recipe7UsingStatic.TheDayOfWeek weekday = Chapter1.Recipe7UsingStatic.TheDayOfWeek.Friday;
    Chapter1.Recipe7UsingStatic.SetShelfPrice(ShelfPrice);
    Console.WriteLine(Chapter1.Recipe7UsingStatic.GetSalePrice( weekday));
    Console.Read();
    

工作原理…

运行您的控制台应用程序,并查看销售价格是否正确计算并输出到控制台应用程序:

工作原理…

现在,让我们更仔细地看看代码。特别是,看看 GetCalculatedSalePrice 计算属性。它使用 Math.Round 函数将销售价格四舍五入到两位小数:

private static decimal GetCalculatedSalePrice
{
    get { return Math.Round(ShelfPrice.CalculateSalePrice (SaleDiscountPercent), 2); }
}

实际上,Math 类是一个静态类,其中包含了一组函数,您可以在代码的任何地方使用这些函数来执行不同的数学计算。因此,请在上面的 Recipes.cs 文件顶部添加以下 using 语句:

using static System.Math;

现在,我们可以将计算属性 GetCalculatedSalePrice 修改为省略 Math 类名:

private static decimal GetCalculatedSalePrice
{
    get { return Round(ShelfPrice.CalculateSalePrice(SaleDiscountPercent), 2); }
}

这实际上是一个非常棒的增强功能。看看以下代码行:

Math.Sqrt(64);
Math.Tan(64);
Math.Pow(8, 2);

由于这个增强功能,前面的代码可以简单地写成以下这样:

Sqrt(64);
Tan(64);
Pow(8, 2);

然而,使用 static 关键字的功能还有更多。我们在这个章节的所有食谱中都使用了静态类。因此,我们也可以为我们的自定义静态类实现 using static 语句。将以下 using 语句添加到控制台应用程序的 Program 类顶部:

using static Chapter1.Recipe7UsingStatic;
using static Chapter1.Recipe7UsingStatic.TheDayOfWeek;
using static System.Console;

你会注意到我们在 using static 语句中包含了枚举器。这同样很棒,因为周五显然是一周中的某一天,枚举器不需要完全调用,就像旧的控制台应用程序代码中那样。通过添加 using static 语句,我们控制台应用程序中的代码可以按如下方式更改:

TheDayOfWeek weekday = Friday;
SetShelfPrice(ShelfPrice);
WriteLine(GetSalePrice(weekday));
Read();

这正是 using static 语句真正好处显现的地方。这意味着代码更少,使代码更易于阅读。回顾一下 C# 6.0 的理念,它并没有引入大的新概念,而是引入了许多小功能,使代码更干净,意图更容易理解。using static 功能正是如此。

异常过滤器

异常过滤器已经存在一段时间了。Visual Basic.NET(VB.NET)和 F# 开发者已经有一段时间拥有这项功能了。幸运的是,它现在已经被引入到 C# 6.0 中。异常过滤器不仅仅如表面所见。乍一看,异常过滤器似乎只是指定了当需要捕获异常时的条件。毕竟,“异常过滤器”这个名字就是这样暗示的。然而,仔细观察后,我们发现异常过滤器不仅仅是语法糖。

准备工作

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

如何操作…

  1. 创建一个名为 Recipe8ExceptionFilters 的类,该类包含两个方法。一个方法读取 XML 文件,另一个方法记录任何异常错误:

    public static class Recipe8ExceptionFilters
    {
        public static void ReadXMLFile(string fileName)
        {
            try
            {
                bool blnReadFileFlag = true;
                if (blnReadFileFlag)
                {
                    File.ReadAllLines(fileName);
                }
            }
            catch (Exception ex)
            {
                Log(ex);
                throw;
            }
        }
    
        private static void Log(Exception e)
        {
            /* Log the error */            
        }
    }
    
  2. 在控制台应用程序中,添加以下代码以调用 ReadXMLFile 方法,并传递要读取的文件名:

    string File = @"c:\temp\XmlFile.xml";
    Chapter1.Recipe8ExceptionFilters.ReadXMLFile(File);
    

如何工作…

如果我们现在运行应用程序,显然会收到错误(这里假设你实际上在 temp 文件夹中没有名为 XMLFile.xml 的文件)。Visual Studio 将在 throw 语句处中断:

如何工作…

注意

你需要在代码文件顶部添加正确的命名空间 System.IO

Log(ex) 方法已记录异常,但看看 Watch1 窗口。我们不知道 blnReadFileFlag 的值是什么。当捕获到异常时,堆栈会回溯(给代码添加开销)到实际的捕获块。因此,异常发生前的堆栈状态丢失。按照以下方式修改你的 ReadXMLFileLog 方法,以包含异常过滤器:

public static void ReadXMLFile(string fileName)
{
    try
    {
        bool blnReadFileFlag = true;
        if (blnReadFileFlag)
        {
            File.ReadAllLines(fileName);
        }
    }
    catch (Exception ex) when (Log(ex))
    {

    }
}

private static bool Log(Exception e)
{
    /* Log the error */
    return false;
}

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

如何工作…

更重要的是,blnReadFileFlag 的值仍然在作用域内。这是因为异常过滤器可以看到异常发生时的堆栈状态,而不是异常被处理时的状态。查看 Visual Studio 中的 Locals 窗口,你会看到变量在异常发生时仍然在作用域内:

如何工作…

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

catch (Exception ex) 
{

}
catch (FileNotFoundException ex)
{

}

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

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

}
catch (FileNotFoundException ex)
{

}

上述代码将捕获所有异常,并在捕获异常的过程中准确记录异常,但不会进入异常处理程序,因为 Log(ex) 方法返回 false

异常过滤器的另一种实现是允许开发者在失败的情况下重试代码。你可能不希望专门捕获第一个异常,但可以在你的方法中实现一种超时元素。当错误计数器达到最大迭代次数时,你可以捕获并处理异常。你可以在这里看到基于 try 子句计数的异常捕获示例:

public static 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 static bool RetryRead(Exception e, int tryCount)
{
    bool blnThrowEx = tryCount <= 10 ? blnThrowEx = false : blnThrowEx = true;
    /* Log the error if blnThrowEx = false */
    return blnThrowEx;
}

异常过滤是处理代码中异常的一种非常有用且极其强大的方式。异常过滤器背后的工作原理并不像人们想象的那样立即明显,但这里正是异常过滤器的实际力量所在。

在 catch 和 finally 块中使用 await 操作符

最后,在 C# 6.0 中,你现在可以在 catchfinally 块中使用 await 关键字。以前,开发者不得不求助于各种奇怪的解决方案来实现现在在 C# 6.0 中可以轻松实现的功能。实际上,这并没有比以下内容更多。

准备工作

我们将创建另一个类来模拟文件的删除。将抛出一个异常,然后执行 catch 块以及 finally 语句。在 catchfinally 子句中,我们将延迟并等待一个任务 3 秒。然后,我们将此延迟输出到控制台应用程序窗口。

如何实现…

  1. 创建一个名为 Recipe9AwaitInCatchFinally 的类,并向该类添加一个名为 FileRunAsync() 的方法,其代码如下。确保 filePath 变量给出的路径中不存在文件:

    public static class Recipe9AwaitInCatchFinally
    {
        public static void FileRunAsync()
        {
            string filePath = @"c:\temp\XmlFile.xml";
            RemoveFileAcync(filePath);
            ReadLine();
        }
    }
    
  2. 然后,向类中添加另一个名为 RemoveFileAcync() 的方法,该方法接受一个文件路径作为参数。在这个方法中包含 try catch,并添加尝试读取指定路径文件的代码:

    public static async void RemoveFileAcync(string filepath)
    {
        try
        {
            WriteLine("Read file");
            File.ReadAllLines(filepath);
        }
        catch (Exception ex)
        {
    
        }
        finally
        {        
    
        }
    }
    
  3. catch 子句中,添加以下代码来模拟一个需要几秒钟才能完成的过程:

    WriteLine($"Exception - wait 3 seconds {DateTime.Now.ToString("hh:MM:ss tt")}");
    await Task.Delay(3000);
    WriteLine($"Exception - Print {DateTime.Now.ToString("hh:MM:ss tt")}");
    WriteLine(ex.Message);
    
  4. finally 子句中,添加另一个延迟来模拟一个也需要几秒钟才能完成的任务:

    WriteLine($"Finally - wait 3 seconds {DateTime.Now.ToString("hh:MM:ss tt")}");
    await Task.Delay(3000);
    WriteLine($"Finally - completed {DateTime.Now.ToString("hh:MM:ss tt")}");
    
  5. 在控制台应用程序中,只需在 Recipe9AwaitInCatchFinally 类中调用 FileRunAsync() 方法:

    Chapter1.Recipe9AwaitInCatchFinally.FileRunAsync();
    

如何工作…

添加代码后,运行控制台应用程序并查看输出:

如何工作…

你会注意到抛出的异常是“文件未找到”异常。在 catch 中,代码在任务延迟时停止了 3 秒。对于 finally 子句中的代码也是如此。它也在任务延迟时延迟了 3 秒。

这意味着现在,在你的 C# 6.0 应用程序中,例如,你可以在异常日志消息写入日志时在 catch 子句中等待。你可以在关闭数据库连接以释放其他对象时在 finally 子句中做同样的事情。

编译器如何实现这个过程相当复杂。然而,你不需要担心这个功能是如何实现的。你所需要做的就是知道,await 关键字现在作为开发人员可用,可以在 catchfinally 块中使用。

小贴士

有关下载代码包的详细步骤,请参阅本书的序言。请查看。本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/CSharp-Programming-Cookbook。我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。查看它们!

第二章. 类和泛型

类是软件开发的基础,对于编写良好的代码至关重要。在本章中,我们将探讨类和泛型以及为什么我们需要使用它们。我们将涵盖的食谱如下:

  • 创建和实现一个抽象类

  • 创建和实现一个接口

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

  • 创建和使用泛型接口

简介

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

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

创建和实现一个抽象类

许多开发者都听说过抽象类,但它们的实现却是个谜。作为开发者,你如何识别一个抽象类并决定何时使用它?实际上,定义相当简单。一旦你理解了抽象类的这个基本定义,何时以及为什么使用它就变得明显了。

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

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

准备工作

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

如何做…

  1. 在 Visual Studio 解决方案资源管理器中,右键单击解决方案,单击添加,然后单击新建项目。选择类库选项将新的类库项目添加到您的解决方案中,并将其命名为Chapter2如何操作…

  2. 已将名为Chapter2的类库项目添加到您的解决方案中。请右键单击添加到您的Chapter2项目中的默认类Class1.cs,并将其重命名为Recipes.cs如何操作…

  3. 完成此操作后,您的代码应类似于以下代码列表。您可以看到默认类已被重命名为Recipes,并且它存在于Chapter2命名空间中:

    namespace Chapter2
    {
        public class Recipes
        {
        }
    }
    
  4. 现在,我们将默认类Recipes更改为名为Cat的抽象类。为此,将abstract关键字添加到类中,并将名称从Recipes更改为Cat。我们现在准备好描述Cat抽象类:

    namespace Chapter2
    {
        public abstract class Cat
        {
        }
    }
    

    注意

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

  5. 向名为Eat()Hunt()Sleep()的抽象类中添加三个方法。您会注意到这些方法不包含主体(花括号)。这是因为它们已被定义为抽象的。与抽象类一样,抽象类中包含的抽象方法不包含实现。这三个方法基本上描述了所有猫共有的功能。所有猫都必须进食、狩猎和睡眠。因此,为了确保从Cat抽象类继承的所有类都包含此功能,它被添加到抽象类中。然后,这些方法在派生类中实现,我们将在接下来的步骤中看到:

        public abstract class Cat
        {
            public abstract void Eat();
            public abstract void Hunt();
            public abstract void Sleep();
        }
    
  6. 我们想定义两种猫的类型。我们想要定义的第一种猫是狮子。为此,我们创建一个Lion类:

    public class Lion
    {
    
    }
    
  7. 在此阶段,Lion类只是一个普通类,不包含在Cat抽象类中定义的任何公共功能。为了从Cat抽象类继承,我们需要在Lion类名称后添加: Cat。冒号表示Lion类继承自Cat抽象类。因此,Lion类是Cat抽象类的派生类:

    public class Lion : Cat
    {
    
    }
    

    一旦你指定Lion类从Cat类继承,Visual Studio 就会显示一个错误。这是预期的,因为我们已经告诉编译器Lion类需要继承Cat抽象类的所有功能,但我们实际上并没有将这些功能添加到Lion类中。派生类被称为覆盖抽象类中的方法,并且需要特别使用override关键字来编写。

  8. 如果你将鼠标悬停在Lion类下方的红色波浪线处,Visual Studio 将通过灯泡功能提供错误解释。正如你所见,Visual Studio 告诉你,虽然你已经定义了类以从抽象类继承,但你没有实现Cat类的任何抽象成员:如何操作…

    注意

    因此,你可以看到使用抽象类是强制系统内特定功能的一种绝佳方式。如果你在抽象类中定义了抽象成员,从该抽象类继承的派生类必须实现这些成员;否则,你的代码将无法编译。这可以用来强制执行公司采用的标准和惯例,或者简单地允许其他开发者在使用你的基类作为派生类时实施某些最佳实践。随着 Visual Studio 2015 功能代码分析器的出现,这可以确保团队的开发工作保持一致。

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

    在 Visual Studio 添加了建议窗口中显示的更改后,你的Lion类将正确无误,并看起来像以下步骤中列出的代码。

  10. 你会注意到 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类,将会生成一个运行时异常。从我们的抽象类继承的想法是扩展它并实现共同的功能。这就是我们需要实现这些功能的地方,这也是为什么抽象类中没有实现的原因。抽象类只是告诉我们以下方法需要实现。派生类负责实际的实现。

  11. 然后添加Lion类重写方法的实现。首先,将using static语句添加到类文件顶部,用于Console.WriteLine方法:

    using static System.Console;
    
  12. 然后,按照以下方式添加实现的方法代码:

    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.");
    }
    
  13. 接下来,我们将创建另一个名为Tiger的类,它也继承自抽象类Cat。按照第 7 步到第 12 步创建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();
        }
    }
    
  14. 按照以下方式为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.");
    }
    
  15. 对于我们的Lion类,添加一个ColorSpectrum枚举和一个名为LionColor的属性。在这里,LionTiger类的实现将有所不同。虽然它们都必须实现抽象类中指定的共同功能,即Eat()Hunt()Sleep(),但只有狮子可以在其颜色范围内具有棕色或白色:

    public enum ColorSpectrum { Brown, White }
    public string LionColor { get; set; }
    
  16. 接下来,在我们的Lion类中添加Lion()构造函数。这将使我们能够为猫舍中的狮子指定颜色。构造函数还接受一个ColorSpectrum枚举类型的变量作为参数:

    public Lion(ColorSpectrum color)
    {
        LionColor = color.ToString();
    }
    
  17. 与此类似,但在颜色上有所不同,Tiger类只能有一个定义老虎为橙色、白色、金色、蓝色(是的,你实际上可以得到一只蓝色的老虎)或黑色的ColorSpectrum枚举。将ColorSpectrum枚举添加到Tiger类中,以及一个名为TigerColor的属性:

    public enum ColorSpectrum { Orange, White, Gold, Blue, Black }
    public string TigerColor { get; set; }
    
  18. 最后,我们将为我们的Tiger类创建一个Tiger()构造函数,以将猫舍中老虎的颜色设置为老虎实际存在的有效颜色。通过这样做,我们在各自的类中分离了仅针对老虎和狮子的一些特定功能,而所有共同功能都包含在抽象类Cat中:

    public Tiger(ColorSpectrum color)
    {
        TigerColor = color.ToString();
    }
    
  19. 要看到类在实际中的应用,我们首先需要将Chapter2.cs类文件添加为引用。在控制台应用程序项目中右键单击引用如何操作…

  20. 引用管理器窗口将打开CodeSamples项目。选择Chapter2并单击确定按钮。然后,添加using Chapter2;语句:如何操作…

  21. 现在,我们需要实例化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();
    
    Console.ReadLine();
    
  22. 当你运行你的控制台应用程序时,你会看到方法按顺序被调用:如何操作…

它是如何工作的…

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

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

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

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

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

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

  • 因为抽象类内部的抽象方法没有实现,所以它们也不包含主体。

创建和实现接口

对于许多开发者来说,接口是令人困惑的,它们的用途也不清楚。一旦您理解了定义接口的概念,接口实际上很容易掌握。

接口就像动词。例如,如果我们必须创建两个名为 LionTiger 的类,它们从 Cat 抽象类派生,那么接口将描述某种动作。狮子和老虎可以吼叫(但不能咕噜)。然后我们可以创建一个名为 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();
    }
    
  2. 接下来,添加一个名为 Cheetah 的类,该类从 Cat 抽象类继承:

    public class Cheetah : Cat
    {
    
    }
    
  3. 一旦您从 Cat 抽象类继承,Visual Studio 将通过灯泡功能显示警告。由于您从抽象类 Cat 继承,您必须在派生类 Cheetah 中实现抽象成员:如何操作…

  4. 这可以通过按Ctrl +.(句号)来轻松修复,并修复文档中的所有出现。您也可以为项目或解决方案执行此操作。对于我们的目的,我们只选择灯泡建议底部的文档链接。Visual Studio 将自动将抽象类中定义的抽象方法添加到您的 Cheetah 类中实现:如何操作…

  5. 你会注意到 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();
        }
    }
    
  6. 为了添加一些实现,按照以下方式修改你的 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; 语句来将静态类引入作用域。

  7. 创建一个名为 IPurrable 的接口,该接口将在 Cheetah 类中实现。接口的常见命名约定规定,接口名称应该以大写 I 开头:

    interface IPurrable
    {
    
    }
    
  8. 接下来,我们将向接口添加一个方法,任何实现该接口的类都必须实现。你会注意到接口的 SoftPurr 方法根本没有任何实现。然而,它指定我们需要传递一个整数值给这个方法,这个值将代表 Cheetah 类将发出多少分贝的呼噜声:

    interface IPurrable
    {
        void SoftPurr(int decibel);
    }
    
  9. 下一步是在 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.");
        }
    }
    
  10. 在指定 Cheetah 类实现了 IPurrable 接口后,Visual Studio 再次通过灯泡功能显示警告。它警告我们,Cheetah 类没有实现接口 IPurrable 中定义的 SoftPurr 方法:如何操作…

  11. 就像我们之前做的那样,我们可以让 Visual Studio 通过输入 Ctrl + .(句号)来建议可能的修复方案。Visual Studio 会建议接口可以隐式或显式地实现:如何操作…

  12. 知道何时使用隐式或显式实现也很简单。我们首先需要知道在什么情况下使用其中一种而不是另一种会更合适。让我们从通过选择灯泡建议中的第一个选项来隐式实现 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.");
        }
    }
    
  13. 如果我们查看 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.");
        }        
    }
    
  14. Visual Studio 会立即通过告诉我们Cheetah类已经包含SoftPurr的定义来显示一个警告:如何操作…

  15. 正是在这里,显式实现的使用变得明显。这指定了SoftPurr方法是IPurrable接口中定义的实现的一个成员:如何操作…

  16. 因此,选择第二个选项显式实现接口将把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.");
        }        
    }
    

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

  17. 为了这本书的目的,我们只需使用隐式实现。让我们为SoftPurr方法编写一些实现,并使用 C# 6.0 中的新nameof关键字以及插值字符串进行输出。同时,删除之前添加的SoftPurr属性:

    public void SoftPurr(int decibel)
    {
        WriteLine($"The {nameof(Cheetah)} purrs at {decibel} decibels.");
    }
    
  18. 转到我们的控制台应用程序,我们可以如下调用我们的Cheetah类:

    Cheetah cheetah = new Cheetah();
    cheetah.Hunt();
    cheetah.Eat();
    cheetah.Sleep();
    cheetah.SoftPurr(60);
    Console.ReadLine();
    
  19. 运行应用程序将产生以下输出:如何操作…

它是如何工作的…

所以,你可能想知道抽象类和接口之间的区别是什么。这基本上取决于你希望在哪里实现你的代码。如果你需要在派生类之间共享功能,那么抽象类最适合你的需求。换句话说,我们有一些对所有猫(狮子、老虎和猎豹)都共同的事情,比如狩猎、进食和睡眠。这最好在抽象类中使用。

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

了解这些区别以及你需要在哪里放置你的实现将帮助你决定是否需要使用抽象类或接口。

创建和使用泛型类或方法

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

准备工作

我们将从一个可以接受任何数据类型作为其构造函数参数并对其进行操作的泛型类开始。

如何操作...

  1. 声明一个泛型类实际上非常简单。我们只需要创建一个带有泛型类型参数<T>的类:

    public class PerformAction<T>
    {
    
    }
    

    注意

    泛型类型参数基本上是一个占位符,用于在实例化变量类时需要定义的具体类型。这意味着泛型类 PerformAction<T> 不能在没有在实例化类时指定尖括号内的类型参数的情况下使用。

  2. 接下来,创建一个泛型类型参数 Tprivate 变量。这个变量将保存我们传递给泛型类的值:

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

    public class PerformAction<T>
    {
        private T _value;
    
        public PerformAction(T value)
        {
            _value = value;
        }
    }
    
  4. 最后,为了完成我们的泛型类,创建一个返回类型为 void 的方法,命名为 IdentifyDataType()。这个方法将要做的就是告诉我们我们传递给泛型类的数据类型。我们可以使用 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()}");
        }
    }
    
  5. 要看到我们的泛型类在实际操作中的真正美,请在控制台应用程序中实例化泛型类,并在每个新的实例化中指定不同的数据类型参数:

    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();                        
    
    Console.ReadLine();
    
  6. 运行你的控制台应用程序将输出你每次实例化泛型类时给定的数据类型:如何操作…

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

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()}");
        }
    }
    
  2. 返回控制台应用程序并查看泛型类的先前实例化:如何操作…

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

  3. 注释掉这些代码行,并将以下实例化添加到你的控制台应用程序中:

    DataSet dsData = new DataSet();
    PerformAction<DataSet> oAction = new PerformAction<DataSet>(dsData);
    oAction.IdentifyDataType();
    

    注意

    注意,为了使这可行,你可能需要在你的代码文件中添加 using System.Data;。这是必要的,以便你可以声明一个 DataSet

  4. 如你所知,DataSet 类型实现了 IDisposable,因此它是一个有效的类型,可以传递给我们的泛型类。继续运行控制台应用程序:如何操作…

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

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

  1. 让我们继续创建一个新的辅助类,命名为 MyHelperClass

    public class MyHelperClass
    {
    }
    
  2. 在这个辅助类内部,我们将创建一个名为InspectType的泛型方法。这个泛型方法有趣的地方在于它可以返回多种类型,因为返回类型也被标记为泛型类型参数。您的泛型方法不必返回任何内容。它也可以声明为void

    public class MyHelperClass
    {
        public T InspectType<T>(T value) 
        {
    
        }
    }
    
  3. 为了说明这个通用方法可以返回多种类型,我们将输出传递给通用方法的类型到控制台窗口,然后返回该类型并在控制台应用程序中显示它。您会注意到,在返回时需要将返回类型转换为(T)

    public class MyHelperClass
    {
        public T InspectType<T>(T value) 
        {
            WriteLine($"The data type of the supplied parameter is {value.GetType()}");
    
            return (T)value;
        }
    }
    
  4. 在控制台应用程序中,创建一个名为MyEnum的枚举器。泛型方法也可以接受枚举器:

    public enum MyEnum { Value1, Value2, Value3 }
    
  5. 在创建枚举器后,将以下代码添加到控制台应用程序中。我们正在实例化和调用oHelper类,并传递不同的值给它:

    MyHelperClass oHelper = new MyHelperClass();
    var intExample = oHelper.InspectType(25);
    Console.WriteLine($"An example of this type is {intExample}");
    
    var decExample = oHelper.InspectType(11.78m);
    Console.WriteLine($"An example of this type is {decExample}");
    
    var strExample = oHelper.InspectType("Hello Generics");
    Console.WriteLine($"An example of this type is {strExample}");
    
    var enmExample = oHelper.InspectType(MyEnum.Value2);
    Console.WriteLine($"An example of this type is {enmExample}");
    
    Console.ReadLine();
    
  6. 如果您运行控制台应用程序,您将看到泛型方法正确地识别了传递给它的参数类型,然后将其返回到控制台应用程序的调用代码中:如何操作…

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

它是如何工作的…

泛型的核心在于能够重用单个类或方法。它允许开发者基本上在整个代码库中不重复类似的代码。这很好地符合不要重复自己DRY)原则。这个设计原则指出,特定的逻辑位只应在代码中表示一次。

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

泛型还允许开发者编写更少的代码,因为代码可以重用,更少的代码也执行得更好。

创建和使用泛型接口

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

准备工作

我们需要检查几个类的属性。为此,我们将创建一个通用接口,该接口将返回一个字符串列表,其中包含为该类找到的所有属性。

如何操作…

让我们看看以下泛型接口的实现方式:

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

    interface IListClassProperties<T>
    {
        List<string> GetPropertyList();
    }
    
  2. 接下来,创建一个名为 InspectClass<T> 的泛型类。让这个泛型类实现之前步骤中创建的 IListClassProperties<T> 接口:

    public class InspectClass<T> : IListClassProperties<T>
    {
    
    }
    
  3. 如同往常,Visual Studio 将突出显示接口成员 GetPropertyList()InspectClass<T> 泛型类中尚未实现:如何操作…

  4. 要显示任何潜在的修复,键入 Ctrl + .(句点)并隐式实现接口:如何操作…

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

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

    public class InspectClass<T> : IListClassProperties<T>
    {
        T _classToInspect;
        public InspectClass(T classToInspect)
        {
            _classToInspect = classToInspect;
        }
    
        public List<string> GetPropertyList()
        {
            throw new NotImplementedException();
        }
    }
    
  7. 为了完成我们的类,我们需要向 GetPropertyList() 方法添加一些实现。在这里,我们将使用 LINQ 查询来返回一个包含在构造函数提供的类中所有属性的 List<string> 对象:

    public List<string> GetPropertyList()
    {
        return _classToInspect.GetType().GetProperties().Select(p => p.Name).ToList();
    }
    
  8. 转到我们的控制台应用程序,创建一个简单的类 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; }
    }
    
  9. 我们现在可以利用我们的 InspectClass<T> 泛型类,该类实现了 IListClassProperties<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)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
    
  10. 继续运行代码,查看通过检查 Invoice 类的属性生成的输出:如何操作…

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

但前面的实现仍然存在一个小问题。让我们看看这个问题的变体之一:

  1. 考虑以下控制台应用程序中的代码:

    InspectClass<int> oClassInspector = new InspectClass<int>(10);
    List<string> lstProps = oClassInspector.GetPropertyList();
    foreach (string property in lstProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
    

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

  2. 在类实现接口之后,在接口实现末尾添加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();
        }
    }
    
  3. 如果我们回到我们的控制台应用程序代码,你会看到 Visual Studio 已经下划线了传递给InspectClass<T>类的int类型:如何做到这一点…

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

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

  1. 创建一个名为AcmeObject的抽象类,并指定所有从AcmeObject继承的类都必须实现一个名为ID的属性:

    public abstract class AcmeObject
    {
        public abstract int ID { get; set; }
    }
    
  2. 现在,我们可以确保我们代码中创建的对象,需要从其读取属性的对象,都继承自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();
        }
    }
    
  3. 在控制台应用程序中,将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; }            
    }
    
  4. 创建两个名为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; }
    }
    
  5. 创建获取InvoiceSalesOrder类属性列表所需的代码。代码很简单,我们可以看到 Visual Studio 对这两个类都没有任何抱怨:

    Invoice oInvoice = new Invoice();
    InspectClass<Invoice> oInvClassInspector = new InspectClass<Invoice>(oInvoice);
    List<string> invProps = oInvClassInspector.GetPropertyList();
    
    foreach (string property in invProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
    SalesOrder oSalesOrder = new SalesOrder();
    InspectClass<SalesOrder> oSoClassInspector = new InspectClass<SalesOrder>(oSalesOrder);
    List<string> soProps = oSoClassInspector.GetPropertyList();
    
    foreach (string property in soProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
    
  6. 然而,如果我们试图对我们的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

  • 开放/封闭 原则

  • Liskov 替换 原则LSP

  • 接口分离原则

  • 依赖倒置原则

让我们先解释面向对象编程的四个支柱,然后我们将更详细地探讨 SOLID 原则。

在 C# 中使用继承

在当今世界,继承通常与事物的终结相关联。然而,在面向对象编程(OOP)中,它与新事物的开始和改进相关联。当我们创建一个新的类时,我们可以从一个已经存在的类中继承,我们的新类将继承它的所有特性,以及新类中添加的额外特性。这是继承的根本。我们将从另一个类继承的类称为派生类。

准备工作

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

如何操作…

  1. 通过右键单击你的解决方案并从上下文菜单中选择 添加 然后选择 新项目 来创建一个新的类库:如何操作…

  2. 添加新项目 对话框中,从已安装的模板中选择 类库,并将你的类命名为 Chapter3如何操作…

  3. 你的新类库将以默认名称 Class1.cs 添加到你的解决方案中,我们将其重命名为 Recipes.cs 以便正确区分代码。然而,如果你觉得更有意义,你可以将你的类重命名为任何你喜欢的名称。

  4. 要重命名你的类,只需在 解决方案资源管理器 中单击类名,然后从上下文菜单中选择 重命名如何操作…

  5. Visual Studio 将要求你确认是否更改项目中所有对代码元素 Class1 的引用的名称。只需单击 如何操作…

  6. 现在,让我们创建一个名为 SpaceShip 的新类:

    public class SpaceShip
    {
    
    }
    
  7. 我们的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类是所有其他星际飞船的一部分,它成为了其他所有飞船的蓝图。

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

    public class Destroyer : SpaceShip
    {
    
    }
    

    注意

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

  9. 接下来,向Destroyer类添加一些仅属于驱逐舰的独特方法。这些方法仅属于Destroyer类,而不属于SpaceShip类:

    public class Destroyer : SpaceShip
    {
        public void WarRoom()
        {
    
        }
        public void Armory(int payloadCapacity)
        {
    
        }
    
        public void WarSpecialists(int activeBattalions)
        {
    
        }
    }
    
  10. 最后,创建一个名为Annihilator的第三个类。这是最强大的星际飞船,用于在行星上发动战争。通过创建类并将它标记为从Destroyer类派生出来,让Annihilator类继承自Destroyer类,如下所示Annihilator : Destroyer

    public class Annihilator : Destroyer
    {
    
    }
    
  11. 最后,向Annihilator类添加一些仅属于此类飞船的方法:

    public class Annihilator : Destroyer
    {
        public void TractorBeam()
        {
    
        }
    
        public void PlanetDestructionCapability()
        {
    
        }
    }
    
  12. 在控制台应用程序中,通过在CodeSamples项目下的References上右键单击并从上下文菜单中选择Add Reference来添加对Chapter3类库的引用:如何操作…

  13. Reference Manager窗口中,选择Projects | Solutions下的Chapter3解决方案。这将允许你在控制台应用程序中使用我们刚刚创建的类:如何操作…

  14. 现在我们看到的是,当我们创建SpaceShip类的新实例时,只有在该类中定义的方法对我们可用。这是因为SpaceShip类没有从任何其他类继承:如何操作…

  15. 在控制台应用程序中创建包含其方法的SpaceShip类:

    SpaceShip transporter = new SpaceShip();
    transporter.ControlBridge();
    transporter.CrewQuarters(1500);
    transporter.EngineRoom(2);
    transporter.MedicalBay(350);
    transporter.TeleportationRoom();
    

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

  16. 接下来,创建Destroyer类的新实例。你会注意到Destroyer类包含的方法比我们在创建类时定义的方法要多。这是因为Destroyer类继承了SpaceShip类,因此继承了SpaceShip类的方法:如何操作…

  17. 在控制台应用程序中创建包含所有方法的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);
    
  18. 最后,创建Annihilator类的新实例。这个类包含了Destroyer类的所有方法以及SpaceShip类的方法。这是因为Annihilator类从Destroyer类继承,而Destroyer类又从SpaceShip类继承:如何操作…

  19. 在控制台应用程序中创建 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();
    }
    
  2. 接下来,创建另一个名为 SpacePrivate 的抽象类,这个抽象类从 SpaceCadet 抽象类继承。我们基本上是在说,当一名太空新兵被训练成太空列兵时,他们仍然会保留作为太空新兵时学到的所有技能:

    public abstract class SpacePrivate : SpaceCadet
    {
        public abstract void AdvancedCommunicationSkill();
        public abstract void AdvancedWeaponsTraining();
        public abstract void Persuader();
    }
    
  3. 为了演示这一点,创建一个名为 LabResearcher 的类,并从 SpaceCadet 抽象类继承。从抽象类继承是通过在新建的类名后定义一个冒号和抽象类名称来完成的。这告诉编译器 LabResearcher 类是从 SpaceCadet 类继承的:

    public class LabResearcher : SpaceCadet
    {
    
    }
    

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

  4. 如果你将鼠标悬停在波浪线上方,你会看到灯泡提示为我们提供了发现的问题:如何做到这一点…

  5. Visual Studio 在提供解决方案以解决发现的问题方面做得很好。通过按 Ctrl + .(控制键和点),你可以让 Visual Studio 显示一些潜在修复(在这种情况下,只有一个修复)以解决已识别的问题:如何做到这一点…

  6. 在 Visual Studio 添加了所需的方法后,你会看到这些方法与 SpaceCadet 抽象类中定义的方法相同。因此,抽象类要求从抽象类继承的类实现抽象类中定义的方法。你还会注意到添加到 LabResearcher 类中的方法没有任何实现,如果直接使用将会抛出异常:

    public class LabResearcher : SpaceCadet
    {
        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();
        }
    }
    
  7. 接下来,创建一个名为 PlanetExplorer 的类,并使这个类继承自 SpacePrivate 抽象类。你会记得 SpacePrivate 抽象类是从 SpaceCadet 抽象类继承而来的:

    public class PlanetExplorer : SpacePrivate
    {
    
    }
    
  8. Visual Studio 将再次警告你,你的新类没有实现你继承的抽象类中的方法。然而,在这里,你会注意到灯泡提示告诉你,你没有在 SpacePrivateSpaceCadet 抽象类中实现任何方法。这是因为 SpacePrivate 抽象类是从 SpaceCadet 抽象类继承而来的:如何操作……

  9. 要修复识别出的问题,请输入 Ctrl + .(控制键和点),让 Visual Studio 显示一些针对识别出的问题的潜在修复方案(在这种情况下,只有一个修复方案):如何操作……

  10. 在将修复方案添加到你的代码后,你会看到 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),以确定它是否能够垂直起飞。它需要施加比其重量更大的推力来对抗重力并进入稳定的轨道。这也取决于航天飞机起飞的行星,因为不同的行星对其表面的物体施加不同的重力。简单来说,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 }
    }
    
  2. 为了我们的类,我们将添加三个重载构造函数,这些构造函数对于根据实例化时的已知事实计算 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);
    }
    
  3. 为了使用传递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;
        }
    }
    
  4. 在以下方法中,只有一个被定义为公共的,因此对类的用户是可见的。创建私有方法来设置万有引力常数、计算 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);
    }
    
  5. 最后,在你的控制台应用程序中,创建以下具有已知值的变量:

    double thrust = 220; // kN
    double shuttleMass = 16.12; // t
    double graviatatonalAccelerationEarth = 9.81;
    double earthMass = 5.9742 * Math.Pow(10, 24);
    double earthRadius = 6378100;
    double thrustToWeightRatio = 0;
    
  6. 创建LaunchShuttle类的新实例,并传递计算 TWR 所需的价值:

    LaunchShuttle NasaShuttle1 = new LaunchShuttle(thrust, shuttleMass, graviatatonalAccelerationEarth);
    thrustToWeightRatio = NasaShuttle1.TWR();
    Console.WriteLine(thrustToWeightRatio);
    
  7. 当你在NasaShuttle1变量上使用点操作符时,你会注意到 IntelliSense 只显示TWR方法。这个类没有暴露任何关于它是如何得到计算出的 TWR 值的内部工作原理。开发者唯一知道的是,给定相同的输入参数,LaunchShuttle类将始终返回正确的 TWR 值:如何做…

  8. 为了测试这一点,创建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();
    
  9. 如果你运行你的控制台应用程序,你会看到 TWR 的值是相同的。这个值表明,一个重 16.12 吨的航天飞机,如果火箭产生 220 千牛顿的推力,将能够从地球表面起飞(即使只是勉强):如何做…

它是如何工作的…

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

  • Public:这个关键字用于变量、属性、类型和方法,并且在任何地方都是可见的。

  • Private:这个关键字用于变量、属性、类型和方法,并且仅在定义它们的代码块中可见。

  • Protected:这个关键字用于变量、属性和方法。不要从公共或私有角度考虑这个问题。受保护的访问级别仅在其使用的类内部以及任何继承的类中可见。

  • Friend:这个关键字用于变量、属性和方法,并且只能由同一项目或程序集内的代码使用。

  • Protected Friend:这个关键字用于变量、属性和方法,并且是受保护作用域和友元作用域的组合(正如其名称所暗示的)。

实现多态性

一旦你研究了并理解了 OOP 的其他支柱,多态性这个概念就很容易掌握了。多态性字面上意味着某物可以有多种形式。这意味着从一个单一接口,你可以创建多个实现。

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

在动态多态中,您正在处理抽象类的创建和实现。这些抽象类充当蓝图,告诉您派生类应该实现什么。下一节将探讨这两者。

准备工作

我们将首先通过展示抽象类的使用来阐述,这是一个动态多态的例子。然后,我们将创建重载构造函数作为静态多态的例子。

如何实现它…

  1. 创建一个名为 Shuttle 的抽象类,并给它一个名为 TWR 的成员,这是计算航天飞机的 TWR:

    public abstract class Shuttle
    {
        public abstract double TWR();
    }
    
  2. 接下来,创建一个名为 NasaShuttle 的类,并使其继承自抽象类 Shuttle,通过在 NasaShuttle 类声明末尾冒号后放置抽象类名称来实现:

    public class NasaShuttle : Shuttle
    {
    
    }
    
  3. Visual Studio 将下划线显示 NasaShuttle 类,因为您已告诉编译器该类继承自一个抽象类,但您尚未实现该抽象类的成员:如何实现它…

  4. 为了修复已识别的问题,按 Ctrl + .(控制键和点)并让 Visual Studio 显示一些潜在的修复方案(在这种情况下,只有一个修复方案)以解决已识别的问题:如何实现它…

  5. Visual Studio 然后将缺失的实现添加到您的 NasaShuttle 类中。默认情况下,它将添加为未实现,因为您需要为在抽象类中重写的抽象成员提供实现:

    public class NasaShuttle : Shuttle
    {
        public override double TWR()
        {
            throw new NotImplementedException();
        }
    }
    
  6. 创建另一个名为 RoscosmosShuttle 的类,并使其继承自相同的 Shuttle 抽象类:

    public class RoscosmosShuttle : Shuttle
    {
    
    }
    
  7. Visual Studio 将下划线显示 RoscosmosShuttle 类,因为您已告诉编译器该类继承自一个抽象类,但您尚未实现该抽象类的成员:如何实现它…

  8. 为了修复已识别的问题,按 Ctrl + .(控制键和点)并让 Visual Studio 显示一些潜在的修复方案(在这种情况下,只有一个修复方案)以解决已识别的问题:如何实现它…

  9. 被重写的方法随后被添加到 RoscosmosShuttle 类中,标记为未实现。您刚刚看到了动态多态的一个实际例子:

    public class RoscosmosShuttle : Shuttle
    {
        public override double TWR()
        {
            throw new NotImplementedException();
        }
    }
    
  10. 要查看静态多态的示例,为 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(单一职责原则)开始。在这里,我们实际上是在说一个类有一个特定的任务需要完成,它不应该做其他任何事情。

准备工作

你将创建一个新的类,并编写代码在向星际飞船添加更多部队时抛出异常,导致其超负荷时,将错误记录到数据库中。

如何操作…

  1. 创建一个名为StarShip的新类:

    public class Starship
    {
    
    }
    
  2. 向你的类中添加一个新方法,用于设置StarShip类的最大部队容量:

    public void SetMaximumTroopCapacity(int capacity)
    {            
    
    }
    
  3. 在这个方法中,添加一个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 规则。

开放/封闭原则

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

准备工作

你将创建一个类,通过查看士兵的类别来确定士兵的技能。我们将向你展示许多开发者创建此类的方式以及如何使用开放/封闭原则创建此类。

如何操作…

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

    public class StarTrooper
    {
    
    }
    
  2. 向这个类添加一个枚举器TrooperClass,以标识我们想要返回技能的士兵类型。此外,创建一个List<string>变量来包含特定士兵类的技能。最后,创建一个名为GetSkills的方法,该方法返回给定士兵类的特定技能集。

    这个类相当简单,但代码的实现是我们经常看到的。有时,您会看到大量的if else语句而不是switch语句。虽然代码的功能是清晰的,但在不更改代码的情况下很难向StarTrooper类添加另一个士兵类。假设您现在必须向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" });
        }
    }
    
  3. 这个问题的解决方案是继承。我们不需要更改代码,而是扩展它。首先,重写上面的StarTrooper类并创建一个Trooper类。GetSkills方法被声明为virtual

    public class Trooper
    {
        public virtual List<string> GetSkills()
        {
            return new List<string>(new string[] { "none" });
        }
    }
    
  4. 现在,我们可以轻松地为可用的SoldierMedicScientist士兵类创建派生类。创建以下从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" });
        }
    }
    
  5. 当扩展类以添加额外的Trooper类时,代码变得极其容易实现。如果我们现在想添加Engineer类,我们只需在从之前创建的Trooper类继承后简单地重写GetSkills方法:

    public class Engineer : Trooper
    {
    public override List<string> GetSkills()
        {
            return new List<string>(new string[] { "Construction", "Demolition" });
        }
    }
    

它是如何工作的...

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

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

第四章。使用响应式扩展(Reactive Extensions)组合基于事件的程序

本章讨论响应式扩展Rx)。为了理解 Rx,我们将涵盖以下食谱:

  • 安装 Rx

  • 事件与可观察对象

  • 使用 LINQ 执行查询

  • 在 Rx 中使用调度器

  • 调试 lambda 表达式

简介

通常,在用 C#开发应用程序的日常事务中,你将不得不使用异步编程。你也可能需要处理许多数据源。想想一个返回当前汇率的服务器,一个返回相关数据流的 Twitter 搜索,或者甚至由多台计算机生成的事件。Rx 提供了一个优雅的解决方案,即IObserver<T>接口。

你使用IObserver<T>接口来订阅事件。然后,维护IObserver<T>接口列表的IObservable<T>接口将在状态变化时通知它们。本质上,Rx 会将多个数据源(社交媒体、RSS 源、UI 事件等)粘合在一起生成数据。因此,Rx 将这些数据源汇集在一个接口中。实际上,Rx 可以被视为由三个部分组成:

  • 可观察对象:将所有这些数据流汇集在一起并代表的接口

  • 语言集成查询LINQ):使用 LINQ 查询这些多个数据流的能力

  • 调度器:使用调度器参数化并发

许多人心中可能都会问,为什么开发者应该使用(或找到使用)Rx。以下是一些 Rx 真正有用的例子:

  • 创建一个具有自动完成功能的搜索。你不想代码为搜索区域中输入的每个值执行搜索。Rx 允许你限制搜索。

  • 使你的应用程序的用户界面更加响应。

  • 当数据发生变化时被通知,而不是必须轮询数据以查找变化。想想实时股价。

要保持 Rx 的更新,你可以查看 GitHub 页面:github.com/Reactive-Extensions/Rx.NET

安装 Rx

在我们开始探索 Rx 之前,我们需要安装它。最简单的方法是使用 NuGet。

准备工作

对于本章节关于 Rx 的内容,我们不会创建一个单独的类。所有代码都将编写在控制台应用程序中。

如何操作…

  1. 右键点击你的解决方案,从上下文菜单中选择管理解决方案的 NuGet 包…如何操作…

  2. 在随后显示的窗口中,在搜索文本框中输入System.Reactive并搜索 NuGet 安装程序:如何操作…

  3. 在撰写本书时,最后一个稳定的版本是 3.0.0。接下来,选择你想要安装 Rx 的项目。为了简单起见,我们只是将其安装到整个项目中:如何操作…

  4. 下一个显示的截图是一个确认对话框,询问你确认对项目的更改。它将显示它将对每个项目进行的更改预览。如果你对更改满意,请点击确定按钮:如何操作…

  5. 最后一个对话框屏幕可能会显示一个许可协议,你需要接受它。要继续,请点击我接受按钮。

  6. 安装完成后,你将在项目的引用节点下看到添加到 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; }
    }
    
  2. 在主程序类中,添加一个名为types的新静态操作事件。基本上,这只是一个委托,将接收一些值,在我们的例子中,是可用的.NET 数据类型:

    class Program
    {
        // Static action event
        static event Action<string> types;
    
        static void Main(string[] args)
        {
    
        }
    }
    
  3. 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);
    
  4. 我们接下来的任务是使用一个简单地将x的值输出到控制台窗口的事件处理程序来订阅此事件。然后,每次我们通过添加types(lstTypes[i].AvailableDatatype);行来遍历lstTypes列表时,我们将引发事件:

    types += x =>
    {
        Console.WriteLine(x);
    };
    
    for (int i = 0; i <= lstTypes.Count - 1; i++)
    {
        types(lstTypes[i].AvailableDatatype);
    }
    
    Console.ReadLine();
    

    注意

    实际上,在引发事件之前,我们应该始终检查事件是否为 null。只有在这个检查之后,我们才应该引发事件。为了简洁,我们在引发事件之前没有添加这个检查。

  5. 当你将步骤 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();
        }
    }
    
  6. 运行你的应用程序将设置我们的值列表,然后引发创建的事件,将列表的值输出到控制台窗口:如何做…

  7. 让我们看看使用 Rx 的事件的工作原理。添加一个静态的Subject字符串。你可能还需要将System.Reactive.Subjects命名空间添加到你的项目中,因为Subjects位于这个单独的命名空间中:

    class Program
    {
    
        static Subject<string> obsTypes = new Subject<string>();
    
        static void Main(string[] args)
        {
    
        }
    }
    
  8. 在创建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();
    
  9. 当你完成所有代码的添加后,你的应用程序应该看起来像这样:

    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();
        }
    }
    
  10. 当你运行应用程序时,你将在控制台窗口中看到与之前相同的项输出:如何做…

如何工作…

在 Rx 中,我们可以使用Subject关键字声明一个事件流。因此,我们有一个事件源,我们可以使用OnNext来发布。为了在控制台窗口中看到这些值,我们使用Subscribe订阅了事件流。

Rx 允许你拥有仅是发布者或仅是订阅者的对象。这是因为IObservableIObserver接口实际上是分开的。此外,请注意,在 Rx 中,可观察对象可以作为参数传递,作为结果返回,并存储在变量中,这使得它们成为一等公民:

如何工作…

Rx 还允许你指定事件流已完成或发生了错误。这真正将 Rx 与.NET 中的事件区分开来。此外,重要的是要注意,在项目中包含System.Reactive.Linq命名空间允许开发者对Subject类型编写查询,因为Subject是一个IObservable接口:

如何工作…

这又是 Rx 与.NET 中的事件区别的一个特性。

使用 LINQ 进行查询

Rx 允许开发者使用代表同步数据流的IObservable接口,通过 LINQ 编写查询。为了回顾,Rx 可以被视为由三个部分组成:

  • 可观察对象:将所有这些数据流汇集并代表的接口

  • 语言集成查询LINQ):使用 LINQ 查询这些多个数据流的能力

  • 调度器:使用调度器参数化并发

在这个菜谱中,我们将更详细地查看 Rx 的 LINQ 功能。

准备工作

由于可观察者只是数据流,我们可以使用 LINQ 来查询它们。在下面的示例中,我们将根据 LINQ 查询将文本输出到屏幕。

如何操作…

  1. 首先向您的解决方案添加一个新的 Windows Forms 项目:如何操作…

  2. 将项目命名为winformRx并点击确定按钮:如何操作…

  3. 工具箱中搜索文本框控件并将其添加到您的表单中:如何操作…

  4. 最后,将标签控件添加到您的表单中:如何操作…

  5. 右键单击您的winformRx项目,并从上下文菜单中选择管理 NuGet 包…如何操作…

  6. 在搜索文本框中输入System.Reactive以搜索 NuGet 包,然后点击安装按钮:如何操作…

  7. Visual Studio 将要求您审查它即将对项目进行的更改。点击确定按钮:如何操作…

  8. 在安装开始之前,您可能需要通过点击我接受按钮来接受许可协议:

  9. 安装完成后,如果您展开项目的引用,应该会看到新添加到您的winformRx项目中的引用:如何操作…

  10. 最后,右键单击项目,并通过从上下文菜单中选择设置为启动项目来将winformRx设置为您的启动项目:如何操作…

  11. 通过在 Windows Form 上任何位置双击来创建表单的加载事件处理程序。向此表单添加Observable关键字。您会注意到关键字立即被下划线标注。这是因为您缺少对System.Reactive的 LINQ 组件的引用:如何操作…

  12. 要添加此内容,请按Ctrl + .(句号)以显示可能的建议以修复问题。选择将using System.Reactive.Linq命名空间添加到您的项目中:如何操作…

  13. 继续将以下代码添加到表单的加载事件中。基本上,您正在使用 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

  14. 点击运行按钮来运行您的应用程序。Windows Form 将显示带有文本框和标签的表单:如何操作…

  15. 注意,随着您输入文本,文本将输出到表单上的标签:如何操作…

  16. 让我们通过向 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);
    }
    
  17. 运行你的应用程序并开始输入一行文本。你会看到,当你输入时,标签控件没有输出任何内容,正如我们在添加我们的Where条件之前的上一个例子中所见:如何做到这一点…

  18. 添加一个句号,并开始添加第二行文本:如何做到这一点…

  19. 你会看到,只有在每个周期之后,输入的文本才会添加到标签中。因此,我们的Where条件工作得非常完美:如何做到这一点…

它是如何工作的…

Rx 的 LINQ 方面允许开发者构建观察序列。以下是一些示例:

  • Observable.Empty<>: 返回一个空的观察序列

  • Observable.Return<>: 返回包含单个元素的观察序列

  • Observable.Throw<>: 返回以异常终止的观察序列

  • Observable.Never<>: 返回一个非终止的观察序列,持续时间无限

LINQ 在 Rx 中的使用允许开发者操纵和过滤数据流,以返回他们所需的确切内容。

在 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

  4. 运行您的应用程序并在文本框中输入一些文本。立即,我们会收到一个异常。这是一个跨线程违规。这发生在尝试从后台线程更新 UI 时。Observable接口正在从System.Threading运行计时器,它不在 UI 的同一线程上。幸运的是,有一个简单的方法可以克服这个问题。嗯,结果证明 UI 线程功能位于不同的程序集,我们发现通过包管理器控制台获取它最简单:如何操作…

  5. 点击查看 | 其他窗口 | 包管理器控制台以访问包管理器控制台如何操作…

  6. 输入以下命令:PM> Install-Package System.Reactive.Windows.Forms如何操作…

    注意

    请注意,你需要确保在包管理器控制台默认项目选择设置为winformRx。如果你看不到这个选项,调整包管理器控制台屏幕宽度直到选项显示。这样你可以确定包被添加到正确的项目中。

  7. 安装完成后,修改你的代码在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 Forms 计时器来创建更新我们的 UI 的间隔。消息发生在正确的线程上,我们不再有跨线程违规。

  8. 如果你还没有将System.Reactive.Concurrency命名空间添加到你的项目中,Visual Studio 将会用波浪线下划线标记ControlScheduler代码行。按下Ctrl + .(控制键和点)将允许你添加缺少的命名空间:如何操作…

  9. 这意味着System.Reactive.Concurrency包含一个可以与 Windows Forms 控件通信的调度器,以便它可以进行调度。再次运行您的应用程序并在文本框中输入一些文本:如何操作…

  10. 停止输入五秒后,节流条件得到满足,文本输出到我们的标签:如何操作…

它是如何工作的…

我们需要记住的是,从我们创建的代码中,有ObserveOnSubscribe。你不应该混淆这两个。在大多数情况下,处理调度器时,你会使用ObserveOnObserveOn方法允许你参数化OnNextOnCompletedOnError消息的运行位置。使用Subscribe,我们参数化实际订阅和取消订阅代码的运行位置。

我们还需要记住,Rx 使用线程计时器 (System.Threading.Timer) 作为默认设置,这就是为什么我们之前遇到了跨线程违规。然而,正如你所看到的,我们使用了调度器来参数化使用哪个计时器。调度器这样做是通过暴露三个组件来实现的。这些是:

  • 调度器执行某些操作的能力

  • 执行要执行的操作或工作的顺序

  • 允许调度器有时间概念的时钟

使用时钟的重要性在于它允许开发者使用远程机器上的计时器,例如(在你和他们之间可能存在时间差异的情况下),告诉它们在特定时间执行操作。

调试 Lambda 表达式

Visual Studio 2015 为开发者添加了调试 lambda 表达式的功能。这是我们最喜欢的 IDE 功能的绝佳补充。它允许我们即时检查 lambda 表达式的结果,并修改表达式以测试不同的场景。

准备工作

我们将创建一个非常基础的 lambda 表达式,并在监视窗口中更改它以产生不同的值。

如何操作…

  1. 添加一个名为 CSharpSix 的类。向这个类添加一个名为 FavoriteFeature 的属性:

    public class CSharpSix
    {
        public string FavoriteFeature { get; set; }
    }
    
  2. 接下来,创建一个 List<CSharpSix> 对象,并将一些你最喜欢的 C# 6 特性添加到这个列表中:

    List<CSharpSix> FavCSharpFeatures = new List<CSharpSix>();
    CSharpSix feature1 = new CSharpSix();
    feature1.FavoriteFeature = "String Interpolation";
    FavCSharpFeatures.Add(feature1);
    
    CSharpSix feature2 = new CSharpSix();
    feature2.FavoriteFeature = "Exception Filters";
    FavCSharpFeatures.Add(feature2);
    
    CSharpSix feature3 = new CSharpSix();
    feature3.FavoriteFeature = "Nameof Expressions";
    FavCSharpFeatures.Add(feature3);
    
  3. 然后,创建一个只返回以 "Ex" 字符串开头的特性的表达式。在这里,我们显然期望看到异常过滤器作为结果:

    var filteredFeature = FavCSharpFeatures.Where(feature => feature.FavoriteFeature.StartsWith("Ex"));
    
  4. 在表达式中放置断点并运行你的应用程序。当代码在断点处停止时,你可以复制 lambda 表达式:如何操作…

  5. 将 lambda 表达式粘贴到你的监视窗口中,并更改 StartsWith 方法中的字符串。你会发现结果已经更改为 "Nameof Expressions" 字符串:如何操作…

它是如何工作的…

能够调试 lambda 表达式使我们能够轻松地更改和调试 lambda 表达式。这是在 Visual Studio 的早期版本中不可能做到的事情。当与这些表达式一起工作时,了解这个技巧显然非常重要。

另一个需要注意的点是,你可以在 Visual Studio 2015 的立即窗口中做同样的事情,也可以从 lambda 表达式中的固定变量中做。

第五章:在 Azure Service Fabric 上创建微服务

本章涉及令人兴奋的微服务世界和Azure Service Fabric。在本章中,我们将涵盖以下食谱:

  • 下载和安装 Service Fabric

  • 使用无状态 actor 服务创建 Service Fabric 应用程序

  • 使用 Service Fabric Explorer

简介

传统上,开发者以单体方式编写应用程序。这意味着一个单一的可执行文件,通过类等组件被分割。单体应用程序需要大量的测试,并且由于单体应用程序的庞大,部署过程繁琐。即使你有多个开发团队,他们也需要对整个应用程序有一个稳固的理解。

微服务是一种旨在解决单体应用程序和传统应用程序开发方式问题的技术。使用微服务,你可以将应用程序分解成更小的部分(服务),这些服务可以独立运行,不依赖于其他任何服务。这些较小的服务可以是无状态的或是有状态的,在功能规模上也比较小,这使得它们更容易开发、测试和部署。你还可以独立于其他服务对每个微服务进行版本控制。如果一个微服务比其他微服务接收更多的负载,你可以仅将该服务扩展以满足其需求。对于单体应用程序,你必须尝试扩展整个应用程序以满足应用程序中单个组件的需求。

以一个流行的在线网店为例,其运作可能包括购物车、购物者资料、订单管理、后端登录、库存管理、计费、退货等更多功能。传统上,会创建一个单独的 Web 应用程序来提供所有这些服务。使用微服务,你可以将每个服务隔离为独立的、自包含的功能和代码库。你也可以指派一个开发团队专注于网店的一个部分。如果这个团队负责库存管理微服务,他们将处理其所有方面。例如,这意味着从编写代码和增强功能到测试和部署的每一个环节。

微服务的另一个优秀副作用是它允许你轻松隔离可能遇到的任何故障。最后,你还可以使用任何你想要的任何技术(C#、Java、VB.NET)创建微服务,因为它们是语言无关的。

Azure Service Fabric 允许您轻松扩展微服务并提高应用程序的可用性,因为它实现了故障转移。当与 Fabric 一起使用时,微服务成为一项非常强大的技术。将 Azure Service Fabric 视为平台即服务PaaS)解决方案,您的微服务位于其上。我们称微服务所在的集合为 Service Fabric 集群。每个微服务都位于一个虚拟机上,在 Service Fabric 集群中被称为节点。这个 Service Fabric 集群可以存在于云中或本地机器上。如果某个节点因任何原因变得不可用,Service Fabric 集群将自动将微服务重新分配到其他节点,以确保应用程序保持可用。

最后,关于有状态和无状态微服务的区别,这里有一句话要说。您可以将微服务创建为有状态或无状态。当一个微服务依赖于外部数据存储来持久化数据时,它本质上是无状态的。这仅仅意味着微服务不内部维护其状态。另一方面,有状态的微服务通过在它所在的本地服务器上存储来维护其状态。

下载和安装 Service Fabric

在您能够创建和测试 Service Fabric 应用程序之前,您必须在您的 PC 上安装和设置一个本地 Service Fabric 集群。

准备工作

我们将从 Azure 网站下载并安装软件开发工具包SDK),这将允许我们在您的本地开发机器上创建一个本地 Service Fabric 集群。

如何操作…

  1. 从 Microsoft Azure 网站下载 SDK,并通过 Service Fabric 学习路径访问其他资源,例如文档,请访问azure.microsoft.com/en-us/documentation/learning-paths/service-fabric/如何操作…

  2. 在安装开始之前,您需要接受许可条款:如何操作…

  3. 然后 Web 平台安装程序开始下载 Microsoft Azure Service Fabric 运行时。请允许此过程完成:如何操作…

  4. 下载完成后,安装过程将开始:如何操作…

  5. 安装完成后,以下产品将被安装,这也在下面的屏幕截图中显而易见:

    • Microsoft Azure Service Fabric 运行时

    • Microsoft Azure Service Fabric Core SDK 预览版

    • Microsoft Azure Service Fabric Visual Studio 2015 工具预览版

    • Microsoft Azure Service Fabric SDK 预览版

    如何操作…

  6. 下一个任务是作为管理员打开 PowerShell。在 Windows 10 开始菜单中输入单词 PowerShell,搜索将立即返回桌面应用程序作为结果。右键单击桌面应用程序,并在上下文菜单中选择 以管理员身份运行如何操作…

  7. 一旦 Windows PowerShell 打开,运行命令 Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force -Scope CurrentUser。这样做的原因是 Service Fabric 使用 PowerShell 脚本创建本地开发集群。它还用于部署 Visual Studio 开发的应用程序。运行此命令可以防止 Windows 阻止这些脚本:如何操作…

  8. 接下来,创建本地 Service Fabric 集群。输入命令 & "$ENV:ProgramFiles\Microsoft SDKs\Service Fabric\ClusterSetup\DevClusterSetup.ps1"

    这将创建托管 Service Fabric 应用程序所需的本地集群:

    如何操作…

  9. 集群创建后,PowerShell 将启动服务:如何操作…

  10. 此过程可能需要几分钟。请确保让它完成:如何操作…

  11. 一旦命名服务就绪,您可以关闭 PowerShell:如何操作…

  12. 要查看创建的集群,您可以在本地计算机上导航到 http://localhost:19080/Explorer

    这将为您提供集群的健康状况和状态的快照。它还将显示集群中运行的应用程序:

    如何操作…

它是如何工作的…

如您所见,Service Fabric 集群对于创建和运行在 Visual Studio 中创建的应用程序至关重要。这将允许我们在将它们发布到云之前,直接在您的本地计算机上测试应用程序。

创建具有无状态演员服务的状态化 Service Fabric 应用程序

作为本章介绍的组成部分,我们探讨了有状态和无状态微服务的区别。可用的 Service Fabric 应用程序模板进一步分为 可靠服务(有状态/无状态)和 可靠演员(有状态/无状态)。何时使用哪一个将取决于您应用程序的具体业务需求。

简单来说,如果您想创建一个应该向任何时间点上的许多应用程序用户公开的服务,一个可靠服务可能是一个不错的选择。想象一下,一个公开最新汇率的服务,可以被许多用户或应用程序同时消费。

再次,回顾本章的引言部分,我们使用了在线网店和购物车的例子。可靠的演员可以很好地适应每个购买商品的客户,因此你可以有一个购物车演员。作为服务框架框架的一部分,可靠的演员基于虚拟演员模式。查看有关虚拟演员模式的文章,请访问research.microsoft.com/en-us/projects/orleans/

为了展示使用无状态演员服务作为示例创建微服务是多么容易,我们将使用 Visual Studio 将服务发布到服务框架集群,并从控制台(客户端)应用程序调用该服务。

准备工作

要完成这个配方,你必须确保你在本地机器上安装了你的本地服务框架集群。

如何做…

  1. 在 Visual Studio 中,通过转到文件 | 新建 | 项目来创建一个新的项目。如何做…

  2. Visual C#节点展开节点,直到你看到节点。当你点击它时,你会看到 Visual Studio 现在列出了一个名为服务框架应用程序的新模板。选择服务框架应用程序模板,命名为sfApp,然后点击确定如何做…

  3. 接下来,从弹出的创建服务窗口中选择无状态可靠 演员。我们将其命名为UtilitiesActor如何做…

  4. 一旦你的解决方案创建完成,你会注意到它由三个项目组成。这些是:

    • sfApp

    • UtilitiesActor

    • UtilitiesActor.Interfaces

    如何做…

  5. 我们将首先修改IUtilitiesActor接口。这个接口将简单地要求UtilitiesActor实现一个名为ValidateEmailAsync的方法,该方法接受一个电子邮件地址作为参数,并返回一个布尔值,表示它是否是一个有效的电子邮件地址:

    namespace UtilitiesActor.Interfaces
    {
        public interface IUtilitiesActor : IActor
        {
            Task<bool> ValidateEmailAsync(string emailToValidate);
        }
    }
    
  6. 接下来,打开你的UtilitiesActor项目并查看类。它将用红色波浪线标记,因为它没有实现接口成员ValidateEmailAsync()如何做…

  7. 使用Ctrl + .(句号),实现接口。删除所有其他不必要的默认代码(如果有):如何做…

  8. 为您插入的实现接口代码应如下所示。目前,它只包含NotImplementedException。这就是我们将实现代码以验证电子邮件地址的地方:

    namespace UtilitiesActor
    {
        internal class UtilitiesActor : StatelessActor, IUtilitiesActor
        {
            public Task<bool> ValidateEmailAsync(string emailToValidate)
            {
                throw new NotImplementedException();
            }        
        }
    }
    
  9. 我们将使用正则表达式来验证通过参数传递给此方法的电子邮件地址。正则表达式非常强大。然而,在我的编程生涯中,我从未自己编写过表达式。这些在互联网上很容易找到,并且您可以为您的项目创建一个实用工具类(或扩展方法类)以供重用。您可以使用正则表达式和其他常用代码。

    最后,您将注意到 ActorEventSource 代码。这只是为了创建 Windows 事件跟踪 (ETW) 事件,这些事件将帮助您从 Visual Studio 的诊断事件窗口中查看应用程序中的情况。要打开诊断事件窗口,请转到 视图其他窗口,然后单击 诊断事件查看器

    internal class UtilitiesActor : StatelessActor, IUtilitiesActor
    {
        public async Task<bool> ValidateEmailAsync(string emailToValidate)
        {
            ActorEventSource.Current.ActorMessage(this, "Email Validation");
    
            return await Task.FromResult(Regex.IsMatch(emailToValidate, @"\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0- 9!#$%&'*+/=?^_`{|}~-]+)*@(?:a-z0-9?\.)+a-z0-9?)\Z", RegexOptions.IgnoreCase));
        }        
    }
    
  10. 一定要添加对 System.Text.RegularExpressions 命名空间的引用。没有它,您将无法使用正则表达式。如果您在代码中添加了正则表达式而没有添加引用,Visual Studio 将在 Regex 方法下显示一条红色的波浪线:如何操作…

  11. 使用 Ctrl + . (句点),将 using 语句添加到您的项目中。这将把正则表达式命名空间引入作用域:如何操作…

  12. 现在我们已经创建了接口,并添加了该接口的实现,是时候添加一个我们将用于测试的客户端应用程序了。在您的解决方案上右键单击,然后添加一个新项目:如何操作…

  13. 最简单的方法是添加一个简单的控制台应用程序。将您的客户端应用程序命名为 sfApp.Client 并单击 确定 按钮:如何操作…

  14. 在您将控制台应用程序添加到解决方案后,您的解决方案应如下所示:如何操作…

  15. 现在,您需要添加对您的客户端应用程序的引用。在 sfApp.Client 项目的 引用 节点处右键单击,并从上下文菜单中选择 添加引用如何操作…

  16. 首先,添加对 UtilitiesActor.Interfaces 项目的引用:如何操作…

  17. 您还需要添加对几个 Service Fabric 动态链接库 (DLL) 的引用。当您创建 Service Fabric 应用程序时,它应该在项目文件夹结构中添加一个名为 packages 的文件夹。浏览到该文件夹,并从那里添加您的 Service Fabric DLL。在添加了所需的 DLL 之后,您的项目应如下所示:如何操作…

  18. 在您的控制台应用程序的 Program.cs 文件中,您需要在 Main 方法中添加以下代码:

    namespace sfApp.Client
    {
        class Program
        {
            static void Main(string[] args)
            {
                var actProxy = ActorProxy.Create<IUtilitiesActor> (ActorId.NewId(), "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();
            }
        }
    }
    

    我们所做的一切只是为我们的演员创建一个代理,并将电子邮件验证的输出写入控制台窗口。您的客户端应用程序现在已准备就绪。

  19. 在我们能够运行客户端应用程序之前,然而,我们首先需要发布我们的服务。在解决方案资源管理器中,右键单击sfApp服务,然后从上下文菜单中选择发布如何操作…

  20. 现在,将显示发布 Service Fabric 应用程序窗口。单击连接端点文本框旁边的选择…按钮:如何操作…

  21. 连接端点选择为本地集群,然后单击确定如何操作…

  22. 目标配置文件应用程序参数文件更改为Local.xml。完成后,单击发布按钮:如何操作…

  23. 如果你导航到http://localhost:19080/Explorer,你会注意到你创建的服务已经发布到你的本地 Service Fabric 集群中:如何操作…

  24. 现在,你已准备好运行你的客户端应用程序。右键单击sfApp.Client项目,然后从上下文菜单中选择调试启动新实例如何操作…

  25. 控制台应用程序调用验证方法来检查电子邮件地址,并将结果显示在控制台窗口中。结果符合预期:如何操作…

  26. 然而,在创建 actor ID 时,我们可以更加具体。我们可以给它一个特定的名称。修改你的代理代码,并创建一个新的ActorId方法,并给它任何字符串值:

    var actProxy = ActorProxy.Create<IUtilitiesActor>(new ActorId("Utilities"), "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").Resu lt);
    ReadLine();
    

    注意

    ActorId方法可以接受类型为Guidlongstring的参数。

  27. 当你再次调试你的客户端应用程序时,你会注意到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 时,将出现以下窗口:如何操作…

  2. 注意左侧的树视图显示 应用视图节点视图如何操作…

  3. 右侧面板将显示有关本地集群的信息。这使得您很容易看到本地服务集群的整体健康状况:如何操作…

  4. 当您展开 应用视图 时,您会注意到我们的 sfApp 服务已经发布。进一步展开,您会看到 sfApp 服务已经在 Node.2 上发布。展开 节点视图Node.2 以查看在该节点上活动的服务:如何操作…

  5. 为了说明微服务的可伸缩性,右键单击 Node.2,然后在上下文菜单中停止该节点。然后,点击窗口顶部的刷新按钮以刷新节点和应用。

  6. 如果您现在继续展开 应用视图 并再次查看服务,您会注意到 Service Fabric 集群已经注意到 Node.2 已关闭。然后它自动将服务推送到一个新的、健康的节点(在本例中为 Node.5):如何操作…

  7. Service Fabric Explorer 右侧面板中的本地集群节点视图也报告说 Node.2 已关闭:如何操作…

工作原理…

Service Fabric Explorer 将允许您查看所选节点的信息,并且您将能够深入查看有关 Service Fabric 集群应用的丰富信息。

第六章。使用异步编程使应用程序响应

本章将向您介绍异步编程。本章将涵盖以下食谱:

  • 异步函数的返回类型

  • 异步编程中的任务处理

  • 异步编程中的异常处理

简介

异步编程是 C# 中的一项激动人心的特性。它允许你在主线程上继续程序执行的同时,一个长时间运行的任务在其自己的线程中单独运行,与主线程分开。当这个长时间运行的任务完成时,它会通知主线程它已经完成(或失败)。异步编程的好处是它提高了应用程序的响应性。了解和掌握异步编程的最佳方式是亲身体验。以下食谱将向您展示一些基本概念。

异步函数的返回类型

在异步编程中,async 方法可以有三种可能的返回类型。这些是:

  • void

  • Task

  • Task<TResult>

我们将在接下来的食谱中查看每种返回类型。

准备工作

异步方法中 void 返回类型有什么用?通常,void 与事件处理器一起使用。请记住,void 不返回任何内容,因此您不能等待它。因此,如果您调用返回类型为 void 的异步方法,您的调用代码应该能够在不需要等待异步方法完成的情况下继续执行代码。

对于返回类型为 Task 的异步方法,您可以使用 await 操作符暂停当前线程的执行,直到被调用的异步方法完成。请记住,返回类型为 Task 的异步方法基本上不返回操作数。因此,如果它被编写为同步方法,它将是一个 void 返回类型的方法。这个声明可能有些令人困惑,但在接下来的食谱中会变得清晰。

最后,具有 return 语句的异步方法具有 TResult 返回类型。换句话说,如果异步方法返回布尔值,您将创建一个返回类型为 Task<bool> 的异步方法。

让我们从返回类型为 void 的异步方法开始。

如何操作…

  1. 通过右键单击您的解决方案并从上下文菜单中选择 添加,然后选择 新建项目 来创建一个新的类库:如何操作…

  2. 新建项目 对话框屏幕上,从已安装的模板中选择 类库,并将您的类命名为 Chapter6如何操作…

  3. 您的新类库将以默认名称 Class1.cs 添加到您的解决方案中,我们将它重命名为 Recipes.cs 以便正确地区分代码。然而,如果您觉得更合适,您可以将您的类重命名为任何您喜欢的名称。

  4. 要重命名您的类,只需在 解决方案资源管理器 中单击类名,并从上下文菜单中选择 重命名如何操作…

  5. Visual Studio 将要求您确认项目中所有对代码元素 Class1 的引用的新名称。只需点击 如何操作…

  6. 下一步是添加另一个新项目。在解决方案上右键单击,并从上下文菜单中选择 添加,然后选择 新项目如何操作…

  7. 这次,您将为您的解决方案创建一个新的 Windows Forms 应用程序。我们需要这样做,以便我们可以创建一个按钮点击事件。我们称我们的项目为 winformAsync如何操作…

  8. 您的 解决方案资源管理器 现在将类似于以下截图,其中添加了 Winforms 应用程序:如何操作…

  9. 在您添加了 Winforms 应用程序后,添加对您之前创建的 Chapter6 类的引用。为此,在 winformAsync 项目的 引用 下右键单击,并从上下文菜单中选择 添加引用 菜单项:如何操作…

  10. 引用管理器 屏幕中,选择位于左侧树视图中的 项目 | 解决方案 节点下的 Chapter6 类,然后单击 确定 按钮:如何操作…

  11. 另一个重要步骤是将 winformAsync 项目设置为解决方案中的启动项目。为此,右键单击 winformAsync 项目,并从上下文菜单中选择 设置为启动项目 菜单项:如何操作…

  12. winformAsync 表单设计器中,打开 工具箱 并选择位于 所有 Windows Forms 节点下的 按钮 控件:如何操作…

  13. 将按钮控件拖放到 Form1 设计器中:如何操作…

  14. 在选择了按钮控件后,双击控件以在代码后面创建点击事件。Visual Studio 将为您插入事件代码:

    namespace winformAsync
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
    
            }
        }
    }
    
  15. 修改 button1_Click 事件并添加 async 关键字到点击事件。这是一个返回 void 的异步方法的示例:

    private async void button1_Click(object sender, EventArgs e)
    {
    
    }
    
  16. Chapter6 类库中,添加一个名为 AsyncDemo 的新类:

    public class AsyncDemo
    {
    }
    
  17. 要添加到 AsyncDemo 类的下一个方法是返回 TResult(在这种情况下,一个布尔值)的异步方法。此方法简单地检查当前年份是否为闰年,然后向调用代码返回一个布尔值:

    async Task<bool> TaskOfTResultReturning_AsyncMethod()
    {
        return await Task.FromResult<bool> (DateTime.IsLeapYear(DateTime.Now.Year));
    }
    
  18. 下一个要添加的方法是返回 Task 类型的 void 返回方法,这样它允许您等待方法。该方法本身不返回任何结果,因此它是一个返回 void 的方法。然而,为了使用 await 关键字,您需要从此异步方法返回 Task 类型:

    async Task TaskReturning_AsyncMethod()
    {
        await Task.Delay(5000);
        Console.WriteLine("5 second delay");
    }
    
  19. 最后,添加一个方法来调用前面的异步方法并显示闰年检查的结果。您会注意到我们在两个方法调用中都使用了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();
    }
    
  20. 在按钮点击中,添加以下代码以异步调用长时间运行的任务:

    private async void button1_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Button Clicked");
        Chapter6.AsyncDemo oAsync = new Chapter6.AsyncDemo();
        await oAsync.LongTask();
        Console.WriteLine("Button Click Ended");
    }
    
  21. 运行您的应用程序将显示 Windows 窗体应用程序:如何操作…

  22. 在点击button1按钮之前,确保输出窗口是可见的:如何操作…

  23. 视图菜单中,点击输出菜单项或按Ctrl + Alt + O显示输出窗口。这将允许我们看到Console.Writeline()输出,因为我们已经将它们添加到Chapter6类和 Windows 应用程序中的代码中。

  24. 点击button1按钮将在我们的输出窗口中显示输出。在整个代码执行过程中,窗体保持响应:如何操作…

  25. 最后,您还可以在单独的调用中使用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;
    }
    

它是如何工作的…

在前面的代码中,我们看到了在button1_Click事件中使用的返回void类型的异步方法。我们还创建了一个返回Task类型的方法,该方法不返回任何内容(如果用于同步编程,则将是void),但返回Task类型允许我们等待该方法。最后,我们创建了一个返回Task<TResult>类型的方法,它执行一些任务并将结果返回给调用代码。

异步编程中的任务处理

基于任务的异步模式TAP)现在是创建异步代码的推荐方法。它在一个线程池的线程上异步执行,而不会在您应用程序的主线程上同步执行。它允许我们通过调用Status属性来检查任务的状态。

准备工作

我们将创建一个任务来读取一个非常大的文本文件。这将通过异步的Task来完成。

如何操作…

  1. 创建一个大的文本文件(我们将其命名为taskFile.txt),并将其放置在您的C:\temp文件夹中:如何操作…

  2. AsyncDemo类中,创建一个名为ReadBigFile()的方法,该方法返回Task<TResult>类型,它将用于返回从我们的大文本文件中读取的字节数:

    public Task<int> ReadBigFile()
    {    
    
    }
    
  3. 将以下代码添加到打开和读取文件字节的操作中。您将看到我们正在使用ReadAsync()方法,该方法异步地从流中读取一系列字节,并通过从该流中读取的字节数在流中前进位置。您还会注意到我们正在使用一个缓冲区来读取这些字节:

    public Task<int> ReadBigFile()
    {
        var bigFile = File.OpenRead(@"C:\temp\taskFile.txt");
        var bigFileBuffer = new byte[bigFile.Length];
        var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, (int)bigFile.Length);
    
        return readBytes;
    }
    

    注意

    您可能需要处理的ReadAsync()方法异常包括ArgumentNullExceptionArgumentOutOfRangeExceptionArgumentExceptionNotSupportedExceptionObjectDisposedExceptionInvalidOperatorException

  4. 最后,在var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, (int)bigFile.Length);行之后立即添加代码的最后一部分,该部分使用 lambda 表达式来指定任务需要执行的工作。在这种情况下,是要读取文件中的字节:

    public Task<int> ReadBigFile()
    {
        var bigFile = File.OpenRead(@"C:\temp\taskFile.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;
    }
    
  5. 如果在之前的菜谱中没有这样做,请将按钮添加到 Windows 窗体应用程序的窗体设计器中。在winformAsync窗体设计器中,打开工具箱并选择按钮控件,该控件位于所有 Windows 窗体节点下:如何操作…

  6. 将按钮控件拖放到Form1设计器中:如何操作…

  7. 选择按钮控件后,双击控件以在代码后创建点击事件。Visual Studio 将为您插入事件代码:

    namespace winformAsync
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
    
            }
        }
    }
    
  8. 修改button1_Click事件并给点击事件添加async关键字。这是一个返回void的异步方法的示例:

    private async void button1_Click(object sender, EventArgs e)
    {
    
    }
    
  9. 现在,请确保您添加代码以异步调用AsyncDemo类的ReadBigFile()方法。请记住将方法的结果(即读取的字节)读取到整数变量中:

    private async void button1_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Start file read");
        Chapter6.AsyncDemo oAsync = new Chapter6.AsyncDemo();
        int readResult = await oAsync.ReadBigFile();
        Console.WriteLine("Bytes read = " + readResult);
    }
    
  10. 运行您的应用程序将显示 Windows 窗体应用程序:如何操作…

  11. 在点击button1按钮之前,请确保输出窗口是可见的:如何操作…

  12. 视图菜单中,点击输出菜单项或按Ctrl + Alt + O显示输出窗口。这将允许我们看到在Chapter6类和 Windows 应用程序中添加的Console.Writeline()输出。

  13. 点击button1按钮将在我们的输出窗口中显示输出。在整个代码执行过程中,窗体保持响应:如何操作…

    注意

    注意,显示在您的输出窗口中的信息将与截图不同。这是因为您使用的文件与我的不同。

它是如何工作的…

任务在单独的线程池线程上执行。这允许在处理大文件时应用程序保持响应。可以通过多种方式使用任务来改进您的代码。这个菜谱只是其中的一个例子。

异步编程中的异常处理

异步编程中的异常处理一直是一个挑战。这在捕获块中尤其如此。从 C# 6 开始,你现在可以在异常处理程序的catchfinally块中编写异步代码。

准备工作

应用程序将模拟读取日志文件的动作。假设第三方系统在另一个应用程序中处理日志文件之前,总是先备份日志文件。当这个处理正在进行时,日志文件被删除并重新创建。然而,我们的应用程序需要定期读取这个日志文件。因此,我们需要准备好文件可能不在我们预期的位置的情况。因此,我们将故意省略主日志文件,以便我们可以强制产生错误。

如何操作…

  1. 创建一个文本文件和两个文件夹来包含日志文件。然而,我们将在BackupLog文件夹中只创建一个日志文件。MainLog文件夹将保持为空:如何操作…

  2. 在我们的AsyncDemo类中,编写一个方法来读取MainLog文件夹中的主日志文件:

    private async Task<int> ReadMainLog()
    {
        var bigFile = File.OpenRead(@"C:\temp\Log\MainLog\taskFile.txt");
        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("Main Log RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
                Console.WriteLine("Main Log Faulted");
    
            bigFile.Dispose();
        });
        return await readBytes;
    }
    
  3. BackupLog文件夹中创建一个读取备份文件的第二个方法:

    private async Task<int> ReadBackupLog()
    {
        var bigFile = File.OpenRead(@"C:\temp\Log\BackupLog\taskFile.txt");
        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("Backup Log RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
                Console.WriteLine("Backup Log Faulted");
    
            bigFile.Dispose();
        });
        return await readBytes;
    }
    

    注意

    实际上,我们可能只会创建一个方法来读取日志文件,只传递路径作为参数。在生产应用程序中,创建一个类并重写方法来读取不同的日志文件位置会是一个更好的方法。然而,对于本配方,我们特别希望创建两个独立的方法,以便在代码中清楚地看到对异步方法的调用。

  4. 然后,我们将创建一个主要的ReadLogFile()方法,尝试读取主日志文件。由于我们尚未在MainLog文件夹中创建日志文件,代码将抛出FileNotFoundException。然后,它将在ReadLogFile()方法的catch块中运行异步方法并等待(这是在 C#的先前版本中不可能做到的),将读取的字节返回给调用代码:

    public async Task<int> ReadLogFile()
    {
        int returnBytes = -1;
        try
        {
            Task<int> intBytesRead = ReadMainLog();
            returnBytes = await ReadMainLog();
        }
        catch (Exception ex)
        {
            try
            {
                returnBytes = await ReadBackupLog();
            }
            catch (Exception)
            {
                throw;
            }
        }
        return returnBytes;
    }
    
  5. 如果在先前的配方中没有这样做,请将按钮添加到 Windows Forms 应用程序的窗体设计器中。在winformAsync窗体设计器中打开工具箱并选择按钮控件,该控件位于所有 Windows 窗体节点下:如何操作…

  6. 将按钮控件拖动到Form1设计器上:如何操作…

  7. 在选择按钮控件后,双击控件以在代码后创建点击事件。Visual Studio 将为您插入事件代码:

    namespace winformAsync
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
    
            }
        }
    }
    
  8. 修改button1_Click事件并给点击事件添加async关键字。这是一个返回void的异步方法的示例:

    private async void button1_Click(object sender, EventArgs e)
    {
    
    }
    
  9. 接下来,我们将编写代码来创建AsyncDemo类的新实例并尝试读取主日志文件。在现实世界的例子中,代码在这个时候并不知道主日志文件不存在:

    private async void button1_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Read backup file");
        Chapter6.AsyncDemo oAsync = new Chapter6.AsyncDemo();
        int readResult = await oAsync.ReadLogFile();
        Console.WriteLine("Bytes read = " + readResult);
    }
    
  10. 运行您的应用程序将显示 Windows 窗体应用程序:如何操作…

  11. 在点击button1按钮之前,确保输出窗口是可见的:如何操作…

  12. 视图菜单中,点击输出菜单项或按Ctrl + Alt + O键以显示输出窗口。这将允许我们看到Console.Writeline()的输出,因为我们已经将它们添加到了Chapter6类和 Windows 应用程序中的代码。

  13. 为了模拟文件未找到异常,我们从MainLog文件夹中删除了文件。您将看到异常被抛出,并且catch块运行了读取备份日志文件的代码:如何操作…

它是如何工作的…

我们可以在catchfinally块中等待的事实,为开发者提供了更多的灵活性,因为异步结果可以在整个应用程序中一致地等待。正如您从我们编写的代码中可以看到的,一旦异常被抛出,我们就异步地读取了备份文件的读取方法。

第七章.使用 C#中的并行和多线程进行高性能编程

本章将探讨如何使用多线程和并行编程来提高代码的性能。在本章中,我们将介绍以下菜谱:

  • 创建和终止一个低优先级的后台线程

  • 增加最大线程池大小

  • 创建多个线程

  • 锁定一个线程直到有争议的资源可用

  • 使用 Parallel.Invoke 调用方法进行并行调用

  • 使用并行 foreach 循环来运行多个线程

  • 取消并行 foreach 循环

  • 在并行 foreach 循环中捕获错误

  • 调试多个线程

简介

如果你今天能在电脑上找到一个单核 CPU,那可能意味着你站在一个博物馆里。今天每台新电脑都利用了多核的优势。程序员可以在自己的应用程序中利用这种额外的处理能力。随着应用程序的大小和复杂性的增长,在许多情况下,它们实际上需要利用多线程。

虽然并非所有情况都适合实现多线程代码逻辑,但了解如何使用多线程来提高应用程序的性能是很好的。本章将带你了解 C#编程中这项激动人心的技术的核心。

创建和终止一个低优先级的后台线程

我们之所以特别关注后台线程,是因为默认情况下,由主应用程序线程或 Thread 类构造函数创建的所有线程都是前台线程。那么,前台线程和后台线程究竟有什么区别呢?好吧,后台线程与前台线程相同,唯一的区别是,如果所有前台线程都终止了,后台线程也会停止。这在你的应用程序中有一个必须防止应用程序终止的过程时很有用。换句话说,当你的应用程序运行时,后台线程必须继续运行。

准备工作

我们将创建一个简单的应用程序,该应用程序定义的线程被创建为后台线程。然后它将挂起、恢复和终止线程。

如何操作…

  1. 通过在解决方案上右键单击并从上下文菜单中选择添加然后新建项目来创建一个新的类库:如何操作…

  2. 添加新项目对话框屏幕中,从已安装的模板中选择类库,并将你的类命名为 Chapter7如何操作…

  3. 你的新类库将以默认名称 Class1.cs 添加到你的解决方案中,我们将它重命名为 Recipes.cs 以便正确区分代码。然而,如果你觉得这样更有意义,你可以将你的类重命名为任何你喜欢的名字。

  4. 要重命名你的类,只需在解决方案资源管理器中单击类名,然后从上下文菜单中选择重命名如何操作…

  5. Visual Studio 将要求你确认重命名项目中所有对代码元素Class1的引用。只需点击如何操作…

  6. 以下类被添加到你的Chapter7库项目中:

    namespace Chapter7
    {
        public class Recipes
        {
    
        }
    }
    
  7. Recipes类内部,添加一个名为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");
            }
    

    注意

    确保你已经为System.Threadingstatic System.Console添加了using语句。

  8. 在之前添加的名为CodeSamples的控制台应用程序内部,通过在CodeSamples项目下的引用上右键单击并从上下文菜单中选择添加引用来添加对Chapter7类库的引用:如何操作…

  9. 参考管理器窗口中,通过访问项目 | 解决方案选择Chapter7解决方案。这将允许你在你的控制台应用程序中使用我们刚刚创建的类:如何操作…

  10. void Main方法中,创建你Recipes类的一个新实例并将其添加到名为backgroundThread的新线程中。将这个新创建的线程定义为后台线程,然后启动它。最后,让线程休眠五秒钟。我们需要这样做,因为我们创建了一个设置为休眠三秒的后台线程。后台线程不会阻止前台线程终止。因此,如果主应用程序线程(默认为前台线程)在后台线程完成之前终止,应用程序将终止,并且也会终止后台线程:

    static void Main(string[] args)
    {
        Chapter7.Recipes oRecipe = new Chapter7.Recipes();
        var backgroundThread = new Thread(oRecipe.DoBackgroundTask);
        backgroundThread.IsBackground = true;
        backgroundThread.Start();
        Thread.Sleep(5000);
    }
    

    注意

    请注意,你可能需要添加using System.Threading指令。

  11. 通过按F5键运行你的控制台应用程序。你会看到我们创建了一个具有正常优先级的后台线程:如何操作…

  12. 让我们修改我们的线程并将其优先级降低到低。将以下代码行添加到你的控制台应用程序中:backgroundThread.Priority = ThreadPriority.Lowest;。这一行将降低线程优先级:

    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    var backgroundThread = new Thread(oRecipe.DoBackgroundTask);
    backgroundThread.IsBackground = true;
    backgroundThread.Priority = ThreadPriority.Lowest;
    backgroundThread.Start();
    Thread.Sleep(5000);
    
  13. 再次运行你的控制台应用程序。这次,你会看到线程优先级已被设置为最低优先级:如何操作…

  14. 返回到你的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");
    }
    
  15. 当你运行你的控制台应用程序时,你会看到线程在调用Thread.Sleep方法之前被终止。然而,以这种方式终止线程通常是不推荐的:如何操作…

如何工作…

能够创建后台线程是在不干扰主应用程序线程进程的情况下在主线程之外工作的好方法。另一个附加的好处是,一旦主应用程序线程完成,后台线程就会立即终止。这个过程确保您的应用程序将优雅地终止。

增加最大线程池大小

.NET 中的线程池位于System.Threading.ThreadPool类中。通常,关于创建自己的线程而不是使用线程池有很多讨论。流行观点认为,线程池应该用于短期任务。这是因为线程池的大小有限。系统中还有许多其他进程会使用线程池。因此,您不希望您的应用程序占用线程池中的所有线程。

规则是您不能将最大工作线程或完成线程的数量设置得少于您计算机上的处理器数量。您也不得将最大工作线程或完成线程的数量设置得少于最小线程池大小。

准备工作

我们将读取当前计算机上的处理器数量。然后,我们将获取最小和最大允许的线程池大小,生成一个介于最小和最大线程池大小之间的随机数,并设置线程池上的最大线程数。

如何操作…

  1. Recipes类中创建一个新的方法IncreaseThreadPoolSize()

    public class Recipes
    {
        public void IncreaseThreadPoolSize()
        {
    
        }
    }
    
  2. 首先,通过使用Environment.ProcessorCount添加读取当前机器上处理器数量的代码:

    public class Recipes
    {
        public void IncreaseThreadPoolSize()
        {
            int numberOfProcessors = Environment.ProcessorCount;
            WriteLine($"Processor Count = {numberOfProcessors}");
        }
    }
    
  3. 接下来,我们将检索线程池中可用的最大和最小线程数:

    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}");
    
  4. 然后,我们将生成一个介于线程池中最大和最小线程数之间的随机数:

    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}");
    
  5. 现在,我们需要尝试通过调用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 线程数。

  6. 当您已将步骤中列出的所有代码添加完毕后,您的IncreaseThreadPoolSize()方法应如下所示:

    public class Recipes
    {
        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");
    
        }
    }
    
  7. 直接进入您的控制台应用程序,创建一个新的Recipe类实例,并调用IncreaseThreadPoolSize()方法:

    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.IncreaseThreadPoolSize();
    Console.ReadLine();
    
  8. 最后,运行您的控制台应用程序并注意输出:如何操作…

工作原理…

从控制台应用程序中,我们可以看到处理器数量是8。因此,线程池的最小线程数也等于 8。然后我们读取最大线程池大小,并在最小和最大数字之间生成一个随机数。最后,我们将最大线程池大小设置为随机生成的最小和最大值。

虽然这只是一个概念验证,并不是在生产应用程序中会做的事情(将线程池设置为随机数),但它清楚地说明了将线程池设置为开发人员指定的值的能力。

提示

本食谱中的代码是为 32 位编译的。尝试将您的应用程序更改为 64 位应用程序并再次运行代码。看看 64 位带来的差异。

创建多个线程

有时候,我们需要创建多个线程。然而,在我们继续之前,我们需要等待这些线程完成它们需要做的任何事情。为此,使用任务是最合适的。

准备工作

确保您已将using System.Threading.Tasks;语句添加到Recipes类的顶部。

如何做…

  1. 在您的Recipes类中创建一个名为MultipleThreadWait()的新方法。然后,创建一个名为RunThread()的第二个方法,使用private修饰符,它接受一个整数秒数来使线程休眠。这将模拟进行一段时间工作的过程:

    public class Recipes
    {
        public void MultipleThreadWait()
        {        
    
        }
    
        private void RunThread(int sleepSeconds)
        {        
    
        }
    }
    

    注意

    在现实中,你可能不会调用相同的方法。从所有目的来看,你可以调用三个不同的方法。然而,为了简单起见,我们将使用不同的睡眠时长调用相同的方法。

  2. 将以下代码添加到您的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");
    
  3. 然后,在RunThread()方法中,我们将读取当前线程 ID,然后使线程休眠指定的毫秒数。这仅仅是秒的整数值乘以1000

    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");
    
  4. 当您完成代码后,您的Recipes类应该看起来像这样:

    public class Recipes
    {
        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");
        }
    }
    
  5. 最后,将Recipe类的新实例添加到您的控制台应用程序中,并调用MultipleThreadWait()方法:

    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.MultipleThreadWait();
    Console.ReadLine();
    
  6. 运行您的控制台应用程序并查看产生的输出:如何做…

它是如何工作的…

您会注意到创建了三个线程(thread 9thread 10thread 11)。然后通过使它们休眠不同时间来暂停这些线程。在每个线程唤醒后,代码将等待所有三个线程完成,然后再继续执行应用程序代码。

锁定一个线程直到有争议的资源可用

有时候,我们希望将特定线程对进程的独占访问权。我们可以使用lock关键字来实现这一点。这将以线程安全的方式执行此进程。因此,当线程运行进程时,它将在锁的作用域内获得对进程的独占访问权。如果另一个线程试图在锁定代码内访问进程,它将被阻塞并必须等待直到锁被释放。

准备工作

对于这个例子,我们将使用任务。确保你已经将using System.Threading.Tasks;语句添加到你的Recipes类的顶部。

如何做…

  1. Recipes类中,添加一个名为threadLock的对象,并使用private修饰符。然后,添加两个名为LockThreadExample()ContendedResource()的方法,这两个方法接受一个表示睡眠秒数的整数作为参数:

    public class Recipes
    {
        private object threadLock = new object();
        public void LockThreadExample()
        {        
    
        }
    
        private void ContendedResource(int sleepSeconds)
        {        
    
        }
    }
    

    注意

    被认为是一种最佳实践,将锁定对象定义为私有。

  2. 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");
    
  3. ContendedResource()方法中,使用privatethreadLock对象创建一个锁,然后使线程睡眠方法传递给方法的秒数:

    int threadID = Thread.CurrentThread.ManagedThreadId;
    lock (threadLock)
    {
        WriteLine($"Locked for thread {threadID}");
        Thread.Sleep(sleepSeconds * 1000);
    }
    WriteLine($"Lock released for thread {threadID}");
    
  4. 在控制台应用程序中,添加以下代码以实例化一个新的Recipes类并调用LockThreadExample()方法:

    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.LockThreadExample();
    Console.ReadLine();
    
  5. 运行控制台应用程序,查看信息输出到控制台窗口:如何做…

它是如何工作的…

我们可以看到thread 11获得了对竞争资源的独占访问权。同时,thread 11thread 12试图访问由thread 11锁定的竞争资源。这导致其他两个线程必须等待直到thread 11完成并释放锁。结果是代码按顺序执行,如控制台窗口输出所示。每个线程等待它的轮次,直到它可以访问资源并锁定其线程。

使用 Parallel.Invoke 调用方法的并行调用

Parallel.Invoke允许我们以(你猜对了)并行的方式执行任务。有时,你需要同时执行操作,这样就可以加快处理速度。因此,可以预期处理任务的总时间等于运行时间最长的进程。使用Parallel.Invoke相当简单。

准备工作

确保你已经将using System.Threading.Tasks;语句添加到你的Recipes类的顶部。

如何做…

  1. 首先,在Recipes类中创建两个名为ParallelInvoke()PerformSomeTask()的方法,这两个方法接受一个表示睡眠秒数的整数作为参数:

    public class Recipes
    {
        public void ParallelInvoke()
        {        
    
        }
    
        private void PerformSomeTask(int sleepSeconds)
        {        
    
        }
    }
    
  2. 将以下代码添加到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");
    
  3. PerformSomeTask()方法中,让线程休眠的方法参数指定的秒数(通过将秒数乘以1000将其转换为毫秒):

    int threadID = Thread.CurrentThread.ManagedThreadId;
    WriteLine($"Sleep thread {threadID} for {sleepSeconds} seconds");
    Thread.Sleep(sleepSeconds * 1000);
    WriteLine($"Thread {threadID} resumed");
    
  4. 当你添加了所有代码后,你的Recipes类应该看起来像这样:

    public class Recipes
    {
        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");
        }
    }
    
  5. 在控制台应用程序中,实例化Recipes类的一个新实例并调用ParallelInvoke()方法:

    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.ParallelInvoke();
    Console.ReadLine();
    
  6. 运行控制台应用程序,查看控制台窗口中产生的输出:如何做到这一点…

它是如何工作的…

由于我们正在并行运行所有这些线程,我们可以假设最长的过程将表示所有任务的总持续时间。这意味着整个过程的持续时间将是 5 秒,因为最长的任务将花费 5 秒来完成(我们将thread 10设置为最多休眠 5 秒)。

正如我们所见,Parallel.Invoke的开始和结束之间的时间差正好是 5 秒。

使用并行foreach循环运行多个线程

很久以前,在一次工作 retreat(是的,我工作的公司真的很酷),我的同事之一 Graham Rook 向我展示了一个并行foreach循环。它确实大大加快了处理速度。但是,这里的问题是。如果你处理的是少量数据或小任务,使用并行foreach循环是没有意义的。并行foreach循环在需要大量处理或处理大量数据时表现最佳。

准备工作

我们首先来看看并行foreach循环在性能上并不优于标准foreach循环的地方。为此,我们将创建一个包含 500 个项目的列表,并遍历列表,将项目写入控制台窗口。

为了说明并行foreach循环的强大功能,我们将使用相同的列表并为列表中的每个项目创建一个文件。在第二个示例中,并行foreach循环的强大功能和好处将变得明显。

如何做到这一点…

  1. 首先,在Recipes类中创建两个方法。调用一个方法ReadCollectionForEach()并传递一个List<string>参数。创建第二个方法,称为ReadCollectionParallelForEach(),它也接受一个List<string>参数:

    public class Recipes
    {
        public double ReadCollectionForEach(List<string> intCollection)
        {        
    
        }
    
        private double ReadCollectionParallelForEach(List<string> intCollection)
        {        
    
        }
    }
    
  2. ReadCollectionForEach()方法中,添加一个标准的foreach循环,该循环将遍历传递给它的字符串集合,并将找到的值写入控制台窗口。然后,清除控制台窗口。使用计时器来跟踪foreach循环期间的总秒数:

    var timer = Stopwatch.StartNew();
    foreach (string integer in intCollection)
    {
        WriteLine(integer);
        Clear();
    }
    return timer.Elapsed.TotalSeconds;
    
  3. 在第二个方法,称为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;
    
  4. 当你添加了所有必要的代码后,你的 Recipes 类应该看起来像这样:

    public class Recipes
    {
        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;
        }
    }
    
  5. 在控制台应用程序中,创建 List<string> 集合并将其传递给 Recipes 类中创建的两个方法。你会注意到我们只创建了一个包含 500 个项目的集合。代码完成后,返回经过的时间(以秒为单位)并将其输出到控制台窗口:

    List<string> integerList = new List<string>();
    for (int i = 0; i <= 500; i++)
    {
        integerList.Add(i.ToString());
    }
    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    double timeElapsed1 = oRecipe.ReadCollectionForEach(integerList);
    double timeElapsed2 = oRecipe.ReadCollectionParallelForEach(integerList);
    WriteLine($"foreach executed in {timeElapsed1}");
    WriteLine($"Parallel.ForEach executed in {timeElapsed2}");
    
  6. 运行你的应用程序。从显示的输出中,你会看到使用 Parallel.ForEach 循环的性能提升是可以忽略不计的。实际上,在这种情况下,Parallel.ForEach 循环只将性能提升了 0.4516 百分比:如何做…

  7. 现在让我们使用一个不同的例子。我们将创建一个密集型任务,并测量 Parallel.ForEach 循环将为我们带来的性能提升。创建两个名为 CreateWriteFilesForEach()CreateWriteFilesParallelForEach() 的方法,这两个方法都接受 List<string> 集合作为参数:

    public class Recipes
    {
        public void CreateWriteFilesForEach(List<string> intCollection)
        {        
    
        }
    
        private void CreateWriteFilesParallelForEach(List<string> intCollection)
        {        
    
        }
    }
    
  8. 将以下代码添加到 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");
    
  9. foreach 循环内部,添加代码以检查是否已使用 filePath 变量的文件名部分附加的 integer 值创建了一个具有特定名称的文件。创建该文件(确保在尝试写入时使用 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()}");
        }
    }
    
  10. 接下来,将以下代码添加到 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");
    
  11. 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()}");
        }
    }
    
  12. 当你完成时,你的代码需要看起来像这样:

    public class Recipes
    {
        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");
        }
    }
    
  13. 转到控制台应用程序,稍微修改 List<string> 对象,并将计数从 500 增加到 1000。然后,调用 Recipes 类中创建的文件方法:

    List<string> integerList = new List<string>();
    for (int i = 0; i <= 1000; i++)
    {
        integerList.Add(i.ToString());
    }
    
    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.CreateWriteFilesForEach(integerList);
    oRecipe.CreateWriteFilesParallelForEach(integerList);
    ReadLine();
    
  14. 最后,当你准备好时,请确保你有 C:\temp\output 目录,并且该目录中没有其他文件。运行你的应用程序并查看控制台窗口的输出。这一次,我们可以看到 Parallel.ForEach 循环产生了巨大的差异。性能提升非常显著,比标准的 foreach 循环提高了 60.7074 百分比:如何做…

它是如何工作的…

从本食谱中使用的示例中可以看出,在使用并行 foreach 循环时应该仔细考虑。如果你处理的是相对较低的数据量或非处理密集型事务,并行 foreach 循环不会对你的应用程序性能带来太多好处。在某些情况下,标准的 foreach 循环可能比并行 foreach 循环快得多。然而,如果你在处理大量数据或运行处理器密集型任务时发现应用程序出现性能问题,可以尝试使用并行 foreach 循环。它可能会让你感到惊讶。

取消并行 foreach 循环

在处理并行 foreach 循环时,一个明显的问题是,如何根据某个条件(如超时)提前终止循环。实际上,并行 foreach 循环很容易提前终止。

准备工作

我们将创建一个方法,该方法接受一个项目集合,并在这个集合中以并行 foreach 循环的方式遍历。它还将知道一个超时值,如果超过,将终止循环并退出方法。

如何操作…

  1. 首先,在 Recipes 类中创建一个名为 CancelParallelForEach() 的新方法,它接受两个参数。一个是 List<string> 集合,另一个是指定超时值的整数。当超时值超过时,Parallel.ForEach 循环必须终止:

    public class Recipes
    {
        public void CancelParallelForEach(List<string> intCollection, int timeOut)
        {        
    
        }    
    }
    
  2. 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}");
    });
    
  3. 在控制台应用程序中,创建 List<string> 对象并添加 1000 个项目到其中。使用 5 秒的超时时间调用 CancelParallelForEach() 方法:

    List<string> integerList = new List<string>();
    for (int i = 0; i <= 1000; i++)
    {
        integerList.Add(i.ToString());
    }
    
    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.CancelParallelForEach(integerList, 5);
    WriteLine($"Parallel.ForEach loop terminated");
    ReadLine();
    
  4. 运行你的控制台应用程序并查看输出结果:如何操作…

它是如何工作的…

你可以从控制台窗口输出中看到,一旦经过的时间超过超时值,并行循环就会在系统最早方便的时候通知停止执行当前迭代之后的迭代。这种对 Parallel.ForEach 循环的控制能力允许开发者避免失控的循环,并允许用户通过点击按钮来取消循环操作,或者当超时值达到时,应用程序自动终止。

在并行 foreach 循环中捕获错误

使用并行 foreach 循环,开发者可以将循环包裹在 try catch 语句中。然而,需要注意,Parallel.ForEach 将会抛出 AggregatedException,它将多个线程中遇到的异常合并为一个。

准备工作

我们将创建一个包含机器 IP 地址集合的List<string>对象。Parallel.ForEach循环将检查 IP 地址以查看给定 IP 地址另一端的机器是否存活。它是通过 ping IP 地址来做到这一点的。执行Parallel.ForEach循环的方法还将获得所需存活机器的最小数量作为整数值。如果未达到存活机器的最小数量,则会抛出异常。

如何操作…

  1. Recipes类中,添加一个名为CheckClientMachinesOnline()的方法,该方法接受一个List<string>集合的 IP 地址和一个指定所需在线的最小机器数量的整数作为参数。添加第二个名为MachineReturnedPing()的方法,该方法将接收一个要 ping 的 IP 地址。为了我们的目的,我们将只返回false来模拟一个死机(ping 到 IP 地址超时):

    public class Recipes
    {
        public void CheckClientMachinesOnline(List<string> ipAddresses, int minimumLive)
        {        
    
        }   
    
        private bool MachineReturnedPing(string ip)
        {            
            return false;
        } 
    }
    
  2. 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;
    }
    
  3. 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");
        }
    }
    
  4. 如果你已经正确添加了所有代码,你的Recipes类将看起来像这样:

    public class Recipes
    {
        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;
        } 
    }
    
  5. 在控制台应用程序中,创建一个List<string>对象来存储一组模拟的 IP 地址。实例化你的Recipes类并调用CheckClientMachinesOnline()方法,将 IP 地址集合和所需在线的最小机器数量传递给它:

    List<string> ipList = new List<string>();
    for (int i = 0; i <= 10; i++)
    {
        ipList.Add($"10.0.0.{i.ToString()}");
    }
    
    try
    {
        Chapter7.Recipes oRecipe = new Chapter7.Recipes();
        oRecipe.CheckClientMachinesOnline(ipList, 2);
    }
    catch (Exception ex)
    {
        WriteLine(ex.InnerException.Message);
    }
    ReadLine();
    
  6. 运行你的应用程序,并在控制台窗口中查看输出:如何操作…

工作原理…

从控制台窗口输出中,你可以看到所需的最小在线机器数量没有达到。应用程序随后抛出了异常,并在Parallel.ForEach循环中捕获了它。能够处理此类并行循环中的异常对于通过处理发生的异常来维护应用程序的稳定性至关重要。

我们鼓励你稍微尝试一下Parallel.ForEach循环,并深入研究AggregareException类的内部方法,以更好地理解它。

调试多个线程

在 Visual Studio 中调试多个线程很棘手,尤其是这些线程都在同时运行。幸运的是,我们作为开发者有一些工具可以使用,以更好地了解我们的多线程应用程序中正在发生的事情。

准备工作

在调试多线程应用程序时,你可以通过在 Visual Studio 中转到调试 | 窗口来访问各种窗口。

如何操作…

  1. 在代码中添加断点后开始调试你的多线程应用程序。你可以通过在 Visual Studio 中转到调试 | 窗口来访问各种调试窗口:如何操作…

  2. 可供你访问的第一个窗口是线程窗口。通过在 Visual Studio 中转到调试 | 窗口或按Ctrl + Alt + H来访问它。在这里,你可以右键单击一个线程来监视和标记它。如果你给你的线程命名了,你将在名称列中看到那个名称。要给你的线程命名,你可以在你的应用程序中添加以下代码,该代码在单独的线程上运行方法:

    int threadID = Thread.CurrentThread.ManagedThreadId;
    Thread.CurrentThread.Name = $"New Thread{threadID}";
    

    你还能够在调试器中看到当前活动的线程。它将被一个黄色箭头标记。然后是托管 ID,这是你之前用来创建唯一线程名称的相同 ID。

    位置列显示了线程当前所在的方法。线程窗口允许你通过双击位置字段来查看线程的堆栈。你还可以冻结和解冻线程。冻结会停止线程执行,而解冻允许冻结的线程正常继续:

    如何操作…

  3. 任务窗口可以通过转到调试 | 窗口或按住Ctrl + Shift + D然后按K来访问。你会注意到在线程窗口中之前标记的线程在这里的任务窗口中也被标记了。任务的状态显示了那一刻的状态,可以是活动死锁等待已调度完成如何操作…

  4. 并行堆栈窗口可以通过在 Visual Studio 中转到调试 | 窗口或按住Ctrl + Shift + D,然后按S键来访问。在这里,你可以看到任务和线程的图形视图。你可以在并行堆栈窗口的右上角的下拉列表中选择,在线程任务视图之间切换:如何操作…

  5. 将选择更改为任务将显示调试会话中的当前任务:如何操作…

  6. 下一个窗口,无疑是我最喜欢的,是并行监视窗口。实际上,它与 Visual Studio 中的标准监视窗口相同,但这是监视应用程序中所有线程的值。你可以在并行监视中输入任何有效的 C#表达式,并看到在调试会话中的那一刻的值。正如你所看到的,我们添加了sleepSeconds变量和线程名称到监视中:如何操作…

它是如何工作的…

能够在 Visual Studio 中有效地使用多线程应用的调试工具,使您更容易理解您应用程序的结构,并帮助您识别可能的错误、瓶颈和关注区域。

我们鼓励您了解您可用的各种调试窗口。

第八章。代码合约

本章将向您介绍代码合约。这是一项非常强大的技术,它将使您能够确保您的代码免受不必要的错误。这尤其适用于您正在编写一个由多个开发者共享的类。代码合约允许您检查和处理在合约下传递给您的方法的参数。如果合约验证失败,您可以在您的方 法中采取果断行动来处理这种情况。本章将涵盖以下步骤:

  • 下载、安装和将代码合约集成到 Visual Studio 中

  • 创建代码合约前置条件

  • 创建代码合约后置条件

  • 创建代码合约不变量

  • 创建代码合约 AssertAssume 方法

  • 创建代码合约 ForAll 方法

  • 创建代码合约 ValueAtReturn 方法

  • 创建代码合约 Result 方法

  • 在抽象类中使用代码合约

  • 使用合约缩写方法

  • 使用 IntelliTest 创建测试

  • 在扩展方法中使用代码合约

简介

您可能想知道代码合约究竟是什么。为了用通俗易懂的语言解释,代码合约是您添加到您的方法中的定义。它告诉编译器,合约下的方法将始终遵守特定的条件。例如,该方法永远不会向调用代码返回空值,或者该方法将始终期望一个大于特定值的参数。如果任何这些条件未满足,您的代码可以抛出异常,并且与您的类集成的开发者将被提示改进他们的调用代码。另一方面,当开发者调用您的类时,他们可以确信合约下的方法将始终以特定的方式行为,并且永远不会偏离。

在开发团队中工作时,代码合约确实非常突出,但在单开发者解决方案中实现这项技术只会提高你的代码质量。

下载、安装和将代码合约集成到 Visual Studio 中

在你可以在应用程序中使用代码合约之前,你需要下载并安装它们。最简单的方法是通过扩展和更新来完成。安装完成后,你需要为代码合约定义一些设置,以便它们开始针对其实现的代码进行功能。让我们看看以下步骤。

准备工作

首先,我们将创建一个新类并将其添加到我们的 Visual Studio 项目中。然后,我们将获取代码合约安装程序并为我们的项目安装它。

如何操作…

  1. 通过右键单击你的解决方案并从上下文菜单中选择添加然后新建项目来创建一个新类:如何操作…

  2. 添加新项目对话框屏幕中,选择已安装的模板中的类库,并将您的类命名为 Chapter8如何操作…

  3. 你的新类库将以默认名称 Class1.cs 添加到你的解决方案中,我们将其重命名为 Recipes.cs 以便正确区分代码。然而,你可以将你的类重命名为你喜欢的任何名称。

  4. 要重命名你的类,只需在 解决方案资源管理器 中单击类名,并从上下文菜单中选择 重命名如何操作…

  5. Visual Studio 将要求你确认项目中对代码元素 Class1 的所有引用的重命名。只需点击 如何操作…

  6. 接下来,点击 工具 菜单并选择 扩展和更新…如何操作…

  7. 你将看到 扩展和更新 窗口出现。务必点击左侧的 Visual Studio 代码库 并以 Code Contracts 作为搜索词。如果你还没有 Code Contracts 安装程序,你将在 Code Contracts for .NET 结果中看到一个下载按钮。点击它以下载和安装代码合约:如何操作…

  8. 在代码合约安装完成后,你可能需要重新启动 Visual Studio。完成此操作后,右键单击 Chapter8 项目,并从上下文菜单中选择 属性如何操作…

  9. 你会注意到,为你的 Chapter8 项目属性页添加了一个新的 代码合约 选项卡。点击此选项卡,并确保 执行运行时合约检查 被勾选。然后,保存你的更改并关闭属性页:如何操作…

  10. 最后,将你的 Chapter8 项目引用添加到之前创建的控制台应用程序中。通过展开控制台应用程序项目,右键单击 引用 项,并从上下文菜单中选择 添加引用如何操作…

  11. 确保你在项目引用部分已选择 Chapter8,然后点击 确定如何操作…

它是如何工作的…

现在,你已经安装并配置了最小要求,以在 Chapter8 类中启用代码合约。你可以继续构建你的解决方案,以确保一切构建成功。

创建代码合约预设条件

预设条件允许你在方法中使用参数之前精确控制参数的形状。这意味着你可以假设调用代码发送到你的方法的数据有很多东西。例如,你可以指定一个参数永远不能为空,或者一个值必须始终在特定的值范围内。可以检查日期,并对对象进行验证和审查。

你对你的方法传入的数据有完全的控制权。这让你在使用数据时感到安心,一旦它通过了你的合约,就不需要做额外的检查。

准备工作

确保你已经安装了代码合约,并且已经按照前一个菜谱中描述的项目属性中正确配置了设置。

如何操作…

  1. 在你的 Recipes 类中,创建一个名为 ValueGreaterThanZero() 的新方法,并让它接受一个整数作为参数:

    public static class Recipes
    {
        public static void ValueGreaterThanZero(int iNonZeroValue)
        {
    
        }
    }
    
  2. ValueGreaterThanZero() 方法中,输入 Contract 声明的开头,你会注意到代码被一条红色的波浪线下划线。按下 Crtl + .(点)来显示潜在修复的建议。点击建议以将代码合约的 using 语句添加到你的类中:如何操作…

  3. 完成后,继续输入先决条件。定义参数值必须大于零:

    public static void ValueGreaterThanZero(int iNonZeroValue)
    {
        Contract.Requires(iNonZeroValue >= 1, "Parameter iNonZeroValue not greater than zero");
    }
    
  4. 如果你返回到控制台应用程序,添加以下 using 语句:

    using static System.Console;
    using static Chapter8.Recipes;
    
  5. 由于我们已经创建了一个静态类,并通过 using 语句将其引入作用域,你可以在 Recipes 类中直接调用方法名。为了了解代码合约的工作原理,将零参数传递给方法:

    try
    {
        ValueGreaterThanZero(0);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
        ReadLine();
    }
    
  6. 最后,运行你的控制台应用程序,查看生成的异常:如何操作…

工作原理…

代码合约检查了先决条件,并确定传递给合约中方法的参数值未通过先决条件检查。抛出异常并输出到控制台窗口。

创建代码合约后置条件

正如代码合约先决条件控制传递给合约中方法的哪些信息一样,代码合约后置条件控制合约中方法返回给调用代码的信息。因此,你可以指定方法永远不会返回空值或空数据集,例如。实际条件并不重要;这是根据具体情况而变化的。这里要记住的重要事情是,这个代码合约允许你对自己的代码返回的数据有更多的控制。

准备工作

假设合约中的方法需要确保返回的值始终大于零。使用代码合约后置条件,我们可以轻松地强制执行此规则。

如何操作…

  1. 在开始之前,确保你已经将以下 using 语句添加到 Recipes 类的顶部:

    using System.Diagnostics.Contracts;
    
  2. Recipes 类中添加一个名为 NeverReturnZero() 的方法,并将一个整数参数传递给此方法:

    public static class Recipes
    {
        public static int NeverReturnZero(int iNonZeroValue)
        {
    
        }
    }
    
  3. 在方法内部,添加你的后置条件合约。正如预期的那样,合约类中的方法被称为 Ensures。这非常描述了它的功能。代码合约确保特定的方法结果永远不会返回。你可以在 Contract.Ensures 方法的签名中看到这一点。因此,后置条件确保此方法的结果永远不会为零:

    public static int NeverReturnZero(int iNonZeroValue)
    {
        Contract.Ensures(Contract.Result<int>() > 0, "The value returned was not greater than zero");
    
        return iNonZeroValue - 1;
    }
    
  4. 返回到控制台应用程序,并添加以下 using 语句:

    using static System.Console;
    using static Chapter8.Recipes;
    
  5. 由于你已经创建了一个静态类,并且使用using语句将其引入作用域,你只需直接在Recipes类中调用方法名。将NeverReturnZero()方法传递一个值为1的参数:

    try
    {
        NeverReturnZero(1);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
        ReadLine();
    }
    
  6. 最后,运行你的控制台应用程序,并在控制台窗口中查看输出:如何操作…

它是如何工作的…

当将1的值传递给合同中的方法时,它导致返回值为零。我们通过从传递给方法的参数中减去1来强制这样做。由于该方法确保非零值,因此抛出了我们定义的消息异常。

创建代码合同不变量

被定义为不变量的东西告诉我们它永远不会改变。它始终是相同的,无论发生什么。如果我们从代码合同的角度考虑这一点,这会带来大量的用例。不变量代码合同基本上用于验证类的内部状态。那么,“内部状态”是什么意思呢?嗯,类的属性给这个类一个特定的状态。让我们假设我们想要保证我们使用的类的属性只接受特定的值,从而确保该类的内部状态。这就是代码合同不变量发挥作用的地方。

准备工作

你可以通过以下示例更好地理解不变量的使用。假设该类需要存储日期。尽管如此,我们永远不能存储过去的日期。在类中使用的任何日期都必须是当前或未来的日期。

如何操作…

  1. 在继续之前,请确保你已经将代码合同using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 接下来,我们将在Recipes.cs类文件中添加一个新的类,名为InvariantClassState。这样做是为了我们可以创建一个实例类而不是一个静态类:

    public class InvariantClassState
    {
    
    }
    
  3. 将以下private属性添加到你的InvariantClassState类中,这些属性将接受年、月和日的整数值:

    private int _Year { get; set; }
    private int _Month { get; set; }
    private int _Day { get; set; }
    
  4. 我们现在将向我们的InvariantClassState类添加一个构造函数。构造函数将接受参数来设置之前创建的属性:

    public InvariantClassState(int year, int month, int day)
    {
        _Year = year;
        _Month = month;
        _Day = day;
    }
    

    注意

    如果你创建public属性,始终是一个好习惯用private设置器来创建它们,例如public int Value { get; private set; }

  5. 我们需要添加的下一个方法是合同不变量方法。你可以给这个方法起任何你喜欢的名字,在这个例子中,它被命名为Invariants()。你会看到许多开发者表示,一个普遍接受的做法是将这个方法命名为ObjectInvariant()。然而,这个方法的命名对不变量代码合同没有任何影响。你会注意到我们用[ContractInvariantMethod]装饰了这个方法,这就是定义这个方法(无论名字如何)作为不变量代码合同的原因。还有另一件重要的事情需要记住,不变量代码合同方法必须是一个void方法,并且被指定为private方法。

    在我们的代码合同不变性方法内部,我们现在指定哪些属性是不变的。换句话说,那些在这个代码合同不变性方法内部永远不会是其他值的属性。首先,我们将指定年值不能在过去。我们还将确保月值是一个在 112 之间的有效值。最后,我们将指定日值不能是月份包含的天数之外的值或小于 1 的值:

    [ContractInvariantMethod]
    private void Invariants()
    {
        Contract.Invariant(this._Year >= DateTime.Now.Year);
        Contract.Invariant(this._Month <= 12);
        Contract.Invariant(this._Month >= 1);
        Contract.Invariant(this._Day >= 1);
        Contract.Invariant(this._Day <= DateTime.DaysInMonth(_Year, _Month);
    }
    
  6. 你可以通过提供一个异常消息来进一步扩展 Contract.Invariant 方法。然后你的 Invariants() 方法将看起来像这样:

    [ContractInvariantMethod]
    private void Invariants()
    {
        Contract.Invariant(this._Year >= DateTime.Now.Year, "The supplied year is in the past");
        Contract.Invariant(this._Month <= 12, $"The value {_Month} is not a valid Month value");
        Contract.Invariant(this._Month >= 1, $"The value {_Month} is not a valid Month value");
        Contract.Invariant(this._Day >= 1, $"The value {_Day} is not a valid calendar value");
        Contract.Invariant(this._Day <= DateTime.DaysInMonth(_Year, _Month), $"The month given does not contain {_Day} days");
    }
    
  7. 最后,添加另一个方法,该方法返回按月/日/年格式化的日期:

    public string ReturnGivenMonthDayYearDate()
    {            
        return $"{_Month}/{_Day}/{_Year}";
    }
    
  8. 当你完成时,你的 InvariantClassState 类将看起来像这样:

    public class InvariantClassState
    {
        private int _Year { get; set; }
        private int _Month { get; set; }
        private int _Day { get; set; }
    
        public InvariantClassState(int year, int month, int day)
        {
            _Year = year;
            _Month = month;
            _Day = day;
        }
    
        [ContractInvariantMethod]
        private void Invariants()
        {
            Contract.Invariant(this._Year >= DateTime.Now.Year, "The supplied year is in the past");
            Contract.Invariant(this._Month <= 12, $"The value {_Month} is not a valid Month value");
            Contract.Invariant(this._Month >= 1, $"The value {_Month} is not a valid Month value");
            Contract.Invariant(this._Day >= 1, $"The value {_Day} is not a valid calendar value");
            Contract.Invariant(this._Day <= DateTime.DaysInMonth(_Year, _Month), $"The month given does not contain {_Day} days");
        }
    
        public string ReturnGivenMonthDayYearDate()
        {            
            return $"{_Month}/{_Day}/{_Year}";
        }
    }
    
  9. 返回到控制台应用程序,并将以下 using 语句添加到你的控制台应用程序 Program.cs 文件中:

    using Chapter8;
    
  10. 我们现在将添加一个新的 InvariantStateClass 类实例,并将值传递给构造函数。首先,将小于 1 的当前年传递给构造函数。这将导致将上一年传递给构造函数:

    try
    {
        InvariantClassState oInv = new InvariantClassState(DateTime.Now.Year - 1, 13, 32);
        string returnedDate = oInv.ReturnGivenMonthDayYearDate();
        WriteLine(returnedDate);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);                
    }
    ReadLine();
    
  11. 运行你的控制台应用程序将导致代码合同不变性抛出异常,因为传递给构造函数的年份是过去的:如何做…

  12. 让我们通过将有效的年值传递给构造函数来修改我们的代码,但保持其余的参数值不变:

    try
    {
        InvariantClassState oInv = new InvariantClassState(DateTime.Now.Year, 13, 32);
        string returnedDate = oInv.ReturnGivenMonthDayYearDate();
    
        WriteLine(returnedDate);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);                
    }
    ReadLine();
    
  13. 运行控制台应用程序将再次导致一个异常消息,指出月值不能大于 12如何做…

  14. 再次修改传递给方法的参数,并提供一个有效的年和月值,但传递一个无效的日值:

    try
    {
        InvariantClassState oInv = new InvariantClassState(DateTime.Now.Year, 11, 32);
        string returnedDate = oInv.ReturnGivenMonthDayYearDate();
    
        WriteLine(returnedDate);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);                
    }
    ReadLine();
    
  15. 再次运行控制台应用程序将导致代码合同不变性抛出异常,因为日显然是错误的。没有一个月包含 32 天:如何做…

  16. 再次修改传递给构造函数的参数,这次,为年、月和日添加有效的值:

    try
    {
        InvariantClassState oInv = new InvariantClassState(DateTime.Now.Year, 11, 25);
        string returnedDate = oInv.ReturnGivenMonthDayYearDate();
    
        WriteLine(returnedDate);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);                
    }
    ReadLine();
    
  17. 因为 2016 年 11 月 25 日是一个有效的日期(因为当前年份是 2016 年),所以格式化的日期被返回到控制台应用程序窗口:如何做…

  18. 让我们稍微改变一下,将 2017 年 2 月 29 日传递给构造函数:

    try
    {
        InvariantClassState oInv = new InvariantClassState(DateTime.Now.Year + 1, 2, 29);
        string returnedDate = oInv.ReturnGivenMonthDayYearDate();
    
        WriteLine(returnedDate);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);                
    }
    ReadLine();
    
  19. 再次,代码合同不变性方法抛出异常,因为 2017 年不是闰年:如何做…

它是如何工作的…

代码合同不变量方法是一种简单而有效的方法,以确保你的类状态没有被修改。然后你可以假设你在类内部使用的属性始终是正确的,并且永远不会包含意外的值。我们喜欢将代码合同不变量视为一种不可变类型(尽管它不是)。字符串是不可变的,这意味着当值改变时,原始值永远不会被修改。当你改变字符串的值时,内存中总是会创建一个新的空间。同样,这让我想起了定义为不变量的属性。这些属性值永远不会改变到由我们的代码合同不变量方法定义之外的其他值。

创建代码合同 Assert 和 Assume 方法

代码合同的AssertAssume方法可能一开始看起来有些令人困惑,但它们都提供了特定的功能。之前的代码合同条件必须出现在它们定义的方法的开始部分,而Assert方法可以放置在方法内部的任何位置。这意味着它将在编译的特定时间对代码产生影响。例如,如果你在合同下的方法中某处执行计算并需要检查计算出的值,你可以使用Assert在原地执行检查以确认计算值是否通过合同。

注意

不要混淆Debug.AssertContract.Assert。它们不是同一回事。Debug.Assert只有在你的代码以调试模式运行时才会产生影响。Contract.Assert将在调试发布模式下运行。

然而,使用Contract.Assume,我们正在告诉代码合同,它需要假设它需要检查的条件是真实的。这仅适用于静态检查器已开启时,这一点将在本食谱中变得更加清晰。

准备工作

我们将使用相同的合同方法来展示在静态检查器开启时使用AssertAssume方法。

如何做到这一点…

  1. 在继续之前,请确保你已经将代码合同using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 在类中添加一个名为ValueIsValid()的方法,它接受两个整数参数:

    public static int ValueIsValid(int valueForCalc, int valueToDivide)
    {
    
    }
    
  3. 在此方法中,添加一个计算步骤(它出现在方法中合同之前),从valueForCalc参数中减去1。将Contract.Assert方法放置在计算之后,以检查计算值的值。我们希望确保该值不为零:

    public static int ValueIsValid(int valueForCalc, int valueToDivide)
    {
        int calculatedVal = valueForCalc - 1;
        Contract.Assert(calculatedVal >= 1, "Calculated value will result in divide by zero exception.");
        return valueToDivide / calculatedVal;
    }
    
  4. 在控制台应用程序中,将相关的using语句添加到Program.cs类中,以便将静态类引入作用域:

    using static Chapter8.Recipes;
    
  5. 通过传递两个整数值调用ValueIsValid()方法。正如你所见,第一个参数将在合同下的方法内部计算出零值:

    try
    {
        int calcVal = ValueIsValid(1, 9);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
        ReadLine();
    }
    
  6. 运行您的控制台应用程序并检查输出窗口。我们可以看到Assert合约正确地抛出了异常,因为计算出的值是零:如何操作…

  7. 然而,如果我们想在构建应用程序时检查我们的代码呢?这就是静态检查器发挥作用的地方。右键单击Chapter8项目并选择属性如何操作…

  8. 点击代码合约选项卡,并选中执行静态合约检查旁边的复选框。同时,取消选中在后台检查复选框,并选择警告时失败构建。此外,将警告级别设置为如何操作…

    注意

    我们假设 Code Contracts 的开发者本意是在低和高之间设置警告级别。“Hi”可能是代码中的误拼。

  9. 保存您的代码合约设置并运行您的控制台应用程序。您会注意到您的构建失败:如何操作…

  10. 如果我们查看ValueIsValid()方法,我们可以看到静态检查器已经识别出在合约下的方法需要定义一个额外的合约。静态检查器已经识别出我们需要在我们的方法中添加Contract.Requires来检查valueForCalc参数是否大于零:如何操作…

  11. 如果我们必须纠正这一点,我们将在方法中添加Contract.Requires,如下所示:

    public static int ValueIsValid(int valueForCalc, int valueToDivide)
    {
        Contract.Requires((valueForCalc - 1) >= 1);
        int calculatedVal = valueForCalc - 1;
        Contract.Assert(calculatedVal >= 1, "Calculated value will result in divide by zero exception.");
        return valueToDivide / calculatedVal;
    }
    
  12. 现在,让我们忽略静态检查器的建议,而是将Contract.Assume添加到我们的方法中。在这里,我们正在告诉静态检查器假设在valueForCalc参数的计算完成后,该值永远不会为零:

    public static int ValueIsValid(int valueForCalc, int valueToDivide)
    {
        Contract.Assume((valueForCalc - 1) >= 1);
        int calculatedVal = valueForCalc - 1; 
        Contract.Assert(calculatedVal >= 1, "Calculated value will result in divide by zero exception.");
        return valueToDivide / calculatedVal; 
    }
    
  13. 如果我们再次运行我们的控制台应用程序,我们将得到一个干净的构建,因为静态检查器假设您最了解情况,并且计算后的值永远不会等于零。然而,如果计算出的值实际上是零,Assume仍然会在运行时检查该值,如果值等于零,则会抛出异常:如何操作…

它是如何工作的…

您可能想知道代码合约中Assume的使用目的。实际上,当您处理无法控制的代码时,这非常有用。如果您实现了无法编辑或不含代码合约的代码,您可以告诉静态检查器忽略基于检查产生的错误的具体代码部分。

创建代码合约 ForAll 方法

如果这个代码合约听起来像是在验证某些或其他的集合,那么您就是正确的。代码合约ForAll将对IEnumerable集合执行验证。这对于开发者来说非常方便,因为您不需要对集合进行任何类型的迭代,也不需要编写验证逻辑。这个合约为您完成了这一切。

准备工作

我们将创建一个简单的整数列表,并用值填充该列表。我们的代码契约将验证列表中不包含任何零值。

如何做…

  1. 在继续之前,确保你已经将代码契约的using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 在你的类中添加一个名为ValidateList()的方法,并将一个List<int>集合传递给它:

    public static void ValidateList(List<int> lstValues)
    {
    
    }
    
  3. ValidateList()方法内部,添加Contract.ForAll契约。有趣的是,你会注意到我们在这里使用Contract.Assert来检查这个列表是否通过我们的契约条件。Contract.ForAll将使用 lambda 表达式来检查我们整数列表中的任何值都不等于零:

    public static void ValidateList(List<int> lstValues)
    {
        Contract.Assert(Contract.ForAll(lstValues, n => n != 0), "Zero values are not allowed");
    }
    
  4. 在控制台应用程序中,将相关的using语句添加到Program.cs类中,以便将静态类引入作用域:

    using static Chapter8.Recipes;
    
  5. 然后,你可以添加一个包含至少一个零值的简单整数列表,并将其传递给ValidateList()方法:

    try
    {
        List<int> intList = new List<int>();
        int[] arr;
        intList.AddRange(arr = new int[] { 1, 3, 2, 6, 0, 5});
        ValidateList(intList);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
        ReadLine();
    }
    
  6. 运行控制台应用程序,并在输出中检查结果:如何做…

它是如何工作的…

我们可以看到,ForAll契约正好按我们预期的那样工作。这是一个极其有用的代码契约,特别是当你不需要添加大量的样板代码来检查集合中的各种无效值时。

创建代码契约 ValueAtReturn 方法

当使用代码契约ValueAtReturn时,我们能想到的最好例子是out参数。我个人并不经常使用out参数,但有时你需要使用它们。代码契约为此提供了支持,你可以在返回时检查值。

准备工作

我们将创建一个简单的方法,从参数中减去一个值。out参数将由代码契约验证,并将结果输出到控制台窗口。

如何做…

  1. 在继续之前,确保你已经将代码契约的using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. Recipes类中,创建一个新的方法ValidOutValue(),并传递一个名为secureValueout参数:

    public static void ValidOutValue(out int secureValue)
    {
    
    }
    
  3. 最后,将Contract.ValueAtReturn添加到方法中。有趣的是,你会发现这需要包含在Contract.Ensures中。这实际上是有道理的,因为代码契约确保我们将返回的值将符合特定的条件:

    public static void ValidOutValue(out int secureValue)
    {
        Contract.Ensures(Contract.ValueAtReturn<int>(out secureValue) >= 1, "The secure value is less or equal to zero");
        secureValue = secureValue - 10;
    }
    
  4. 在控制台应用程序中,将相关的using语句添加到Program.cs类中,以便将静态类引入作用域:

    using static Chapter8.Recipes;
    
  5. 然后,添加一些代码来调用ValidOutValue()方法,并将一个out参数传递给它:

    try
    {
        int valueToCheck = 5;
        ValidOutValue(out valueToCheck);
        WriteLine("The value is not zero");
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
    }
    ReadLine();
    
  6. 运行控制台应用程序,并在输出窗口中检查结果:如何做…

它是如何工作的…

我们可以看到,out参数已经成功验证。一旦条件不满足,代码契约抛出了我们能够捕获的异常。

创建代码契约 Result 方法

有时候,我们只是想有一种方式来验证方法的结果。我们希望能够检查返回的内容,并对其进行验证。正是在这里,代码契约Result可以派上用场。它将检查契约下方法返回的值与指定的契约,然后成功或失败。

如何操作…

  1. 在继续之前,请确保你已经将代码契约using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. Recipes类中,添加一个名为ValidateResult()的新方法,该方法接受两个整数作为参数:

    public static int ValidateResult(int value1, int value2)
    {
    
    }
    
  3. 向此方法添加检查方法结果的代码契约Result。必须指出的是,代码契约Result永远不能在void方法中使用。这是显而易见的,因为这种代码契约的目的是检查和验证方法的结果。你还会注意到,代码契约Result方法与Contract.Ensures方法一起使用。Contract.Result的格式由返回类型<int>()和需要遵守的条件>= 0组成:

    public static int ValidateResult(int value1, int value2)
    {
        Contract.Ensures(Contract.Result<int>() >= 0, "Negative result not allowed");
        return value1 - value2;
    }
    
  4. 在控制台应用程序中,将相关的using语句添加到Program.cs类中,以便将静态类引入作用域:

    using static Chapter8.Recipes;
    
  5. 在契约下的静态方法中添加调用,并传递将导致代码契约抛出异常的参数。在这种情况下,我们传递了1023,这将导致ValidateResult()方法返回一个负数:

    try
    {
        WriteLine(ValidateResult(10, 23));
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
    }
    ReadLine();
    
  6. 最后,运行控制台应用程序,检查返回到控制台输出窗口的结果:如何操作…

它是如何工作的…

你会看到代码契约检查了ValidateResult()方法的返回值,并发现它违反了契约。随后,将抛出异常并在控制台窗口中显示。

在抽象类上使用代码契约

如果你已经在代码中使用抽象类,你会知道能够通过代码契约来控制它们的使用,这将导致代码更加健壮。但究竟我们如何使用代码契约与抽象类结合呢?特别是既然抽象类不应该包含任何实现?好吧,这绝对可能,下面就是如何做到这一点的方法。

准备工作

如果你之前没有使用过抽象类,我们建议你首先阅读第二章,类和泛型,以熟悉如何使用和创建抽象类。

如何操作…

  1. 在继续之前,请确保你已经将代码契约using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 创建一个名为Shape的抽象类,它定义了两个方法,分别称为Length()Width(),每个方法都接受一个整数作为参数。记住,抽象类不包含任何实现:

    public abstract class Shape
    {
        public abstract void Length(int value);
        public abstract void Width(int value);
    }
    
  3. 创建另一个名为 ShapeContract 的抽象类,它继承自 Shape 抽象类。我们的代码合同将驻留在这里:

    public abstract class ShapeContract : Shape
    {
    
    }
    
  4. 重写 Shape 抽象类的 Length()Width() 方法,并确保它们需要一个非零参数:

    public abstract class ShapeContract : Shape
    {
        public override void Length(int value)
        {
            Contract.Requires(value > 0, "Length must be greater than zero");
        }
    
        public override void Width(int value)
        {
            Contract.Requires(value > 0, "Width must be greater than zero");
        }
    }
    
  5. 我们现在需要将 ShapeContract 合同类与 Shape 抽象类关联起来。我们将通过使用属性来完成此操作。在你的 Shape 抽象类顶部添加以下属性:

    [ContractClass(typeof(ShapeContract))]
    
  6. 完成此操作后,你的 Shape 抽象类将看起来像这样:

    [ContractClass(typeof(ShapeContract))]
    public abstract class Shape
    {
        public abstract void Length(int value);
        public abstract void Width(int value);
    }
    
  7. 我们还需要将 Shape 抽象类与 ShapeContract 抽象类关联起来,作为告诉编译器合同需要作用于哪个类的手段。我们将通过在 ShapeContract 类顶部添加以下属性来完成此操作:

    [ContractClassFor(typeof(Shape))]
    
  8. 当你完成这些操作后,你的 ShapeContract 类将看起来像这样:

    [ContractClassFor(typeof(Shape))]
    public abstract class ShapeContract : Shape
    {
        public override void Length(int value)
        {
            Contract.Requires(value > 0, "Length must be greater than zero");
        }
    
        public override void Width(int value)
        {
            Contract.Requires(value > 0, "Width must be greater than zero");
        }
    }
    
  9. 我们现在准备好实现 Shape 抽象类。创建一个名为 Rectangle 的新类,并继承 Shape 抽象类:

    public class Rectangle : Shape
    {
    
    }
    
  10. 你会注意到 Visual Studio 用红色波浪线下划线标记了 Rectangle 类。这是因为还没有实现 Shape 类。将鼠标光标悬停在红色波浪线上,查看 Visual Studio 提供的灯泡弹出建议:如何操作…

  11. 通过按住 Ctrl + .(句号),你会看到可以实施的建议修复,以纠正 Visual Studio 警告你的错误。在这种情况下,Visual Studio 建议我们实施的单个修复是实现抽象类:如何操作…

  12. 在点击灯泡建议中的实现抽象类建议后,Visual Studio 将插入 Shape 抽象类的实现。你会注意到为你插入的方法仍然没有任何实现,如果你没有为 Length()Width() 方法添加任何实现,它们将抛出 NotImplementedException如何操作…

  13. 要为我们的 Rectangle 类添加实现,为 Length()Width() 方法创建两个属性,并将这些属性设置为提供的参数值的值:

    public class Rectangle : Shape
    {
        private int _length { get; set; }
        private int _width { get; set; }
        public override void Length(int value)
        {
            _length = value;
        }
    
        public override void Width(int value)
        {
            _width = value;
        }
    }
    
  14. 在控制台应用程序中,向 Program.cs 类添加相关的 using 语句,以便将 Chapter8 类引入作用域:

    using Chapter8;
    
  15. 创建 Rectangle 类的新实例,并将一些值传递给 Rectangle 类的 Length()Width() 方法:

    try
    {
        Rectangle oRectangle = new Rectangle();
        oRectangle.Length(0);
        oRectangle.Width(1);
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
    }
    ReadLine();
    
  16. 最后,运行控制台应用程序并检查输出窗口:如何操作…

它是如何工作的…

由于我们在 Length() 方法中添加了一个零值,抽象类上的代码合同已正确地抛出了异常。能够在抽象类上实现代码合同允许开发者创建更好的代码,尤其是在团队工作中,你需要根据某些业务规则传达实现限制时。

使用契约缩写方法

缩写方法是代码契约功能的一个很好的补充。它们允许我们创建一个包含常用或分组代码契约的单个缩写方法。这意味着我们可以简化我们的代码并使其更易于阅读。

准备工作

我们将创建两个具有相同代码契约要求的方法。然后,通过实现一个缩写方法来包含代码契约,我们将简化契约下的方法。

如何操作…

  1. 在继续之前,请确保您已将代码契约 using 语句添加到您的 Recipes.cs 类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 在添加以下方法之前请考虑。这里有两个方法,每个方法都需要传入的参数不等于零,并且结果也不为零。每个方法内部实现不同,但应用的代码契约是相同的。为了避免代码契约不必要地重复,我们可以使用缩写方法:

    public static int MethodOne(int value)
    {
        Contract.Requires(value > 0, "Parameter must be greater than zero");
        Contract.Ensures(Contract.Result<int>() > 0, "Method result must be greater than zero");
    
        return value - 1;
    }
    
    public static int MethodTwo(int value)
    {
        Contract.Requires(value > 0, "Parameter must be greater than zero");
        Contract.Ensures(Contract.Result<int>() > 0, "Method result must be greater than zero");
    
        return (value * 10) - 10;
    }
    
  3. 在您的 Recipes 类中添加一个名为 StandardMethodContract() 的新方法。此方法的名字可以是任何您喜欢的,但签名需要与缩写的方法匹配。在此方法内部,添加之前在 MethodOne()MethodTwo() 中定义的所需代码契约:

    private static void StandardMethodContract(int value)
    {
        Contract.Requires(value > 0, "Parameter must be greater than zero");
        Contract.Ensures(Contract.Result<int>() >= 1, "Method result must be greater than zero");
    }
    
  4. 将以下属性添加到 StandardMethodContract() 方法的顶部,以将其标识为缩写方法:

    [ContractAbbreviator]
    
  5. 完成此操作后,您的缩写方法应如下所示:

    [ContractAbbreviator]
    private static void StandardMethodContract(int value)
    {
        Contract.Requires(value > 0, "Parameter must be greater than zero");
        Contract.Ensures(Contract.Result<int>() >= 1, "Method result must be greater than zero");
    }
    
  6. 现在,您可以通过在代码契约的位置引用缩写方法来简化 MethodOne()MethodTwo()

    public static int MethodOne(int value)
    {
        StandardMethodContract(value);
    
        return value - 1;
    }
    
    public static int MethodTwo(int value)
    {
        StandardMethodContract(value);
    
        return (value * 10) - 10;
    }
    
  7. 在控制台应用程序中,将相关的 using 语句添加到 Program.cs 类中,以便将静态类引入作用域:

    using static Chapter8.Recipes;
    
  8. 首先,使用以下参数调用两个方法:

    try
    {
        MethodOne(0);
        MethodTwo(1);                
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
    }
    ReadLine();
    
  9. 如果您运行您的控制台应用程序,您将注意到代码契约在缩写契约中抛出异常,告诉我们提供的参数不能为零:如何操作…

  10. 然后,修改您的调用代码,为 MethodOne() 传递一个有效值,但保持对 MethodTwo() 的调用不变。再次运行您的控制台应用程序:

    try
    {
        MethodOne(200);
        MethodTwo(1);                
    }
    catch (Exception ex)
    {
        WriteLine(ex.Message);
    }
    ReadLine();
    
  11. 这次,您将看到缩写方法中的代码契约在返回值不能为零的情况下抛出异常:如何操作…

它是如何工作的…

缩写方法允许我们创建更易于阅读的代码,并将常用代码契约分组在带有 [ContractAbbreviator] 特性的公共方法中。缩写方法是代码契约的一个强大功能,开发人员可以利用它来生成更好的代码。

使用 IntelliTest 创建测试

IntelliTest 允许开发者创建和运行针对其代码合同的测试。这允许开发者通过创建额外的代码合同来通过 IntelliTest 报告的测试失败,从而创建最健壮的代码。但需要注意的是,IntelliTest 仅包含在 Visual Studio Enterprise 中。

准备工作

您需要使用 Visual Studio Enterprise 2015 来创建和运行 IntelliTests。

如何操作…

  1. 在继续之前,请确保您已将代码合同 using 语句添加到 Recipes.cs 类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 在您的 Recipes.cs 文件中添加一个名为 CodeContractTests 的新类:

    public class CodeContractTests
    {    
    
    }
    
  3. 然后,在 CodeContractTests 类中添加一个名为 Calculate() 的方法,并将两个整数值作为参数传递给 Calculate() 方法。在 Calculate() 方法内部,添加一个代码合同以确保该方法的结果永远不会等于零:

    public class CodeContractTests
    {
        public int Calculate(int valueOne, int valueTwo)
        {
            Contract.Ensures(Contract.Result<int>() >= 1, "");
    
            return valueOne / valueTwo;
        }
    }
    
  4. 选择 Calculate() 方法并右键单击它。从上下文菜单中,点击 创建 IntelliTest 菜单项:如何操作…

  5. Visual Studio 将显示 创建 IntelliTest 窗口。在这里,您可以定义您的 IntelliTest 的几个设置。需要注意的是,您可以使用与 MSTest 不同的测试框架。然而,出于我们的目的,我们将使用 MSTest 并将其他设置保留为默认值:如何操作…

  6. 当您点击 确定 按钮时,Visual Studio 将继续为您创建一个新的测试项目:如何操作…

  7. 当项目创建完成后,您将在 解决方案资源管理器 中看到创建的新测试项目。在本例中,因为我们保留了 创建 IntelliTest 窗口中的默认设置,所以我们的新测试项目将被称为 Chapter8.Tests如何操作…

  8. 继续展开 Chapter8.Tests 项目,然后点击为您创建的 CodeContractTestsTest.cs 文件。您将看到 Visual Studio 为您创建的以下代码:

    /// <summary>This class contains parameterized unit tests for CodeContractTests</summary>
    [PexClass(typeof(CodeContractTests))]
    [PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperati onException))]
    [PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentExcept ion), AcceptExceptionSubtypes = true)]
    [TestClass]
    public partial class CodeContractTestsTest
    {
        /// <summary>Test stub for Calculate(Int32, Int32)</summary>
        [PexMethod]
        public int CalculateTest(
            [PexAssumeUnderTest]CodeContractTests target,
            int valueOne,
            int valueTwo
        )
        {
            int result = target.Calculate(valueOne, valueTwo);
            return result;
            // TODO: add assertions to method CodeContractTestsTest.CalculateTest (CodeContractTests, Int32, Int32)
        }
    }
    
  9. CodeContractTests 类中,右键单击 Calculate() 方法并从上下文菜单中选择 运行 IntelliTest如何操作…

  10. IntelliTest 将立即开始工作并打开 IntelliTest 探索结果 窗口:如何操作…

  11. 从我们对 Calculate() 方法运行的测试结果中,我们可以看到有三个失败的测试和一个成功的测试。报告的测试失败是 DivideByZeroExceptionContractExceptionOverflowException。点击单个测试失败可以查看测试详情以及 堆栈跟踪如何操作…

  12. 让我们通过添加以下额外的代码合同来修改 Calculate() 方法:

    public int Calculate(int valueOne, int valueTwo)
    {
        Contract.Requires(valueOne > 0, "Parameter must be greater than zero");
        Contract.Requires(valueTwo > 0, "Parameter must be greater than zero");
        Contract.Requires(valueOne > valueTwo, "Parameter values will result in value <= 0");
        Contract.Ensures(Contract.Result<int>() >= 1, "");
    
        return valueOne / valueTwo;
    }
    
  13. 从附加的代码约定中,我们可以看到,通过要求valueTwo参数大于零,我们已经解决了DivideByZeroException。我们还可以看到,要求valueOne总是大于valueTwo的代码约定已经解决了ContractException。最后,通过要求两个参数都大于零,我们自动解决了OverflowException如何做……

  14. 右键单击Calculate()方法并再次运行 IntelliTest。这次,你会看到所有测试都通过了,我们受合同约束的方法现在可以用于生产代码:如何做……

它是如何工作的……

IntelliTest 允许开发者通过几点击鼠标快速高效地为你的代码约定创建测试。

在扩展方法中使用代码约定

之前的菜谱展示了开发者如何创建各种代码约定来保护你的代码免受意外输入和输出的影响,但让我们看看开发者如何利用代码约定。扩展方法的想法浮现在脑海中,其中我们创建可以在整个项目中使用以执行常用操作的代码。

让我们使用代码约定ForAll方法。这会影响一个集合,因此自然地,它在扩展方法中的使用引导我们到一个可能的实现。在这个菜谱中,我们将创建一个使用代码约定来验证我们刚刚创建的列表的扩展方法。

准备工作

我们将创建一个用于扩展方法的静态类,然后使用ForAll代码约定来验证List集合。

如何做……

  1. 在你继续之前,确保你已经将代码约定using语句添加到你的Recipes.cs类文件顶部:

    using System.Diagnostics.Contracts;
    
  2. 创建一个名为ExtensionMethods的新静态类并将其添加到Recipes.cs类文件中:

    public static class ExtensionMethods
    {
    
    }
    
  3. 接下来,添加一个名为ContainsInvalidValue()的扩展方法,它接受一个匿名类型T的给定列表和一个要检查的类型为T的无效值作为参数:

    public static bool ContainsInvalidValue<T>(this List<T> value, T invalidValue)
    {    
    
    }
    
  4. 在我们的扩展方法内部,添加一个包裹在try catch语句中的代码约定ForAll,该语句检查给定参数是否在列表中存在:

    try
    {
        Contract.Assert(Contract.ForAll(value, n => !value.Contains(invalidValue)), "Zero values are not allowed");
        return false;
    }
    catch 
    {
        return true;
    }
    
  5. 一旦你将所有代码添加到你的扩展方法中,它应该看起来像这样:

    public static class ExtensionMethods
    {
        public static bool ContainsInvalidValue<T>(this List<T> value, T invalidValue)
        {
            try
            {
                Contract.Assert(Contract.ForAll(value, n => !value.Contains(invalidValue)), "Zero values are not allowed");
                return false;
            }
            catch 
            {
                return true;
            }
        }
    }
    
  6. 在控制台应用程序中,将相关的using语句添加到Program.cs类中,以便将Chapter8类引入作用域:

    using Chapter8;
    
  7. 正如我们之前所做的那样,创建一个简单的列表,但这次,调用通过静态扩展方法类在列表上公开的扩展方法。现在,我们将能够通过使用扩展方法和代码约定直接验证我们的列表:

    List<int> intList = new List<int>();
    int[] arr;
    intList.AddRange(arr = new int[] { 1, 3, 2, 6, 0, 5 });
    
    if (intList.ContainsInvalidValue(4)) 
        WriteLine("Invalid integer Value");
    else
        WriteLine("Valid integer List");
    
  8. 运行应用程序将产生以下输出:如何做……

  9. 由于我们在这里使用匿名类型,我们可以轻松地在包含不同类型的列表上调用这个扩展方法。以下是一个在字符串列表上的实现示例:

    List<string> strList = new List<string>();
    string[] arr2;
    strList.AddRange(arr2 = new string[] { "S", "A", "Z" });
    
    if (strList.ContainsInvalidValue("G"))
        WriteLine("Invalid string Value");
    else
        WriteLine("Valid string List");
    
  10. 再次运行应用程序将产生以下输出:如何操作…

它是如何工作的…

我们可以看到,使用代码约定以及 C#的其他强大功能,我们可以利用非常强大的代码检查和验证技术。扩展方法可以在整个项目中使用,以执行频繁的验证或针对您项目的特定代码逻辑。

第九章。正则表达式

正则表达式regex)对许多开发者来说仍然是一个谜。我们承认,我们经常使用它们,这足以证明深入了解它们的工作原理是必要的。另一方面,互联网上有许多经过测试的正则表达式模式,直接重用现有的模式通常比尝试自己创建一个更容易。正则表达式的主题远比本书单章所能解释的要多。

因此,在本章中,我们仅介绍一些正则表达式的概念。为了更深入地理解正则表达式,还需要进一步学习。然而,为了本书的目的,我们将更详细地研究正则表达式的创建方式和它们如何应用于一些常见的编程问题。在本章中,我们将涵盖以下食谱:

  • 开始使用正则表达式

  • 匹配有效日期

  • 清理输入

  • 动态正则表达式匹配

简介

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

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

其次,正则表达式有一组特定的元字符,这些字符对正则表达式引擎具有特殊意义。这些字符是 [ ]{ }( )*+\?|$.^

使用花括号 { },例如,允许开发者指定特定字符集需要出现的次数。另一方面,使用方括号定义需要精确匹配的内容。

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

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

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

正则表达式的基础知识相对容易理解,但你可以用正则表达式做更多的事情。

开始使用正则表达式

我们将创建一个名为 Chapter9 的新类,在这里我们将创建各种方法来展示正则表达式的使用。

准备工作

为了本书的目的,我们将创建一个简单的控制台应用程序来展示正则表达式的使用。实际上,你很可能不会将这种逻辑与你的生产代码混合在一起,因为这会导致代码被重写。正则表达式最佳添加位置是在扩展方法中的辅助类。

如何操作…

  1. 首先右键单击解决方案,转到 添加,然后从上下文菜单中选择 新建项目如何操作…

  2. 添加新项目 窗口打开。选择 类库 项目类型,并将项目命名为 Chapter9如何操作…

  3. 在新类文件被添加后,你的 解决方案资源管理器 应该看起来像这样:如何操作…

  4. 右键单击 Class1.cs 文件,并从上下文菜单中选择 重命名如何操作…

  5. Class1.cs 文件重命名为 Recipes.cs 并在确认对话框中选择 如何操作…

  6. 在控制台应用程序中,点击 引用 部分,并从上下文菜单中选择 添加引用如何操作…

  7. 在控制台应用程序的 引用管理器 中,选择 Chapter9 并点击 确定 以将其添加为控制台应用程序的引用:如何操作…

  8. Recipes 类中添加以下 using 语句,以便我们可以在 .NET 中使用正则表达式程序集:

    using System.Text.RegularExpressions;
    
  9. 完成所有这些后,你的 Chapter9 类应该看起来像这样:

    using System.Text.RegularExpressions;
    namespace Chapter9
    {
        public class Recipes
        {
        }
    }
    

工作原理…

我们已经添加了一个基本的类文件,它将被用来验证正则表达式模式,这个文件是从我们的控制台应用程序中调用的。

匹配有效日期

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

准备工作

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

using System.Text.RegularExpressions;

如何操作…

  1. 创建一个名为 ValidDate() 的新方法,它接受一个字符串作为参数。这个字符串将是我们要验证的日期模式:

    public void ValidDate(string stringToMatch)
    {
    
    }
    
  2. 将以下正则表达式模式添加到你的方法中,到一个方法变量中:

    string pattern = $@"^(19|20)\d\d-./- ./$";
    
  3. 最后,添加正则表达式来匹配提供的字符串参数:

    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.");
    
  4. 完成此操作后,你的方法应该看起来像这样:

    public void ValidDate(string stringToMatch)
    {
        string pattern = $@"^(19|20)\d\d-./- ./$";
    
        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.");            
    }
    
  5. 返回到你的控制台应用程序,添加以下代码并通过点击 开始 来调试你的应用程序:

    Chapter9.Recipes oRecipe = new Chapter9.Recipes();
    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"); 
    Read();
    

    注意

    您会注意到在先前的代码示例中使用了 Read() 而不是 Console.Read()。这是因为我们在控制台应用程序的 using 语句中添加了 using static System.Console;。这样做将允许您省略 Console 关键字。

  6. 日期字符串被传递到正则表达式,模式与参数中的日期字符串进行匹配。输出在控制台应用程序中显示:如何做到这一点…

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

    注意

    我们将简要解释为什么日期 1800-01-21 是无效的。

  8. 回到你的 ValidDate() 方法,将正则表达式更改为以下内容:

    string pattern = $@"^(19|20)\d\d-./-./$";
    
  9. 再次运行控制台应用程序并查看输出:如何做到这一点…

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

它是如何工作的…

让我们更仔细地看看先前的代码示例中使用的两个表达式。将它们相互比较,您可以看到我们用黄色标记出的更改:

如何工作…

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

  • 破折号 (-)

  • 小数点 (.)

  • 斜杠 (/)

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

我们还需要能够告诉正则表达式引擎考虑一个或另一个模式。单词“或”由元字符 | 表示。为了使正则表达式引擎考虑单词“或”而不拆分整个表达式,我们将其括在括号 () 中。

这里是正则表达式中使用的符号:

条件或
&#124;
年份部分
(19&#124;20)
\d\d
有效的分隔符字符集
[-./]
月份和日期的有效数字
0[1-9]
1[0-2]
[1-9]
[12][0-9]
3[01]
字符串的开始和结束
^
`
---
&#124;
年份部分
(19&#124;20)
\d\d
有效的分隔符字符集
[-./]
月份和日期的有效数字
0[1-9]
1[0-2]
[1-9]
[12][0-9]
3[01]
字符串的开始和结束
^
告诉正则表达式引擎在给定字符串的末尾停止匹配。

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

  • ^: 从字符串的开始处开始匹配。

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

  • \d\d: 在检查之后,跟随两个 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(这里的日期分隔符没有影响)。这是因为我们的月份是一个单独的数字,而我们寻找的是以 0 开头的月份。为了解决这个问题,我们必须修改月份逻辑的表达式,以包括 1 到 9 之间的单个数字。我们通过在表达式的末尾添加[1-9]来实现这一点。

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

  • ^: 从字符串的开始处开始匹配。

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

  • \d\d: 在检查之后,跟随两个 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. 在你的 Recipes.cs 类中创建一个新的方法,命名为 SanitizeInput(),并让它接受一个字符串参数:

    public string SanitizeInput(string input)
    {
    
    }
    
  2. 在包含我们想要从输入中移除的脏话的方法中添加一个类型为 List<string> 的列表:

    List<string> lstBad = new List<string>(new string[] { "BadWord1", "BadWord2", "BadWord3" });
    

    注意

    实际上,你可能需要使用数据库调用从数据库中的表中读取黑名单中的单词。你通常不会像这样将它们硬编码到列表中。

  3. 开始构建我们将用来查找黑名单单词的正则表达式。使用 |(或)元字符连接单词,以便正则表达式可以匹配任何单词。当列表完成时,你可以在正则表达式的两边添加 \b 表达式。这表示单词边界,因此只会匹配整个单词:

    string pattern = "";
    foreach (string badWord in lstBad)
        pattern += pattern.Length == 0 ? $"{badWord}" : $"|{badWord}";
    
    pattern = $@"\b({pattern})\b";
    
  4. 最后,我们将添加 Regex.Replace() 方法,该方法接受输入并查找模式中定义的单词的出现,同时忽略大小写并将脏话替换为 *****

    return Regex.Replace(input, pattern, "*****", RegexOptions.IgnoreCase);
    
  5. 完成此操作后,你的 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);            
    }
    
  6. 在控制台应用程序中,添加以下代码来调用 SanitizeInput() 方法并运行你的应用程序:

    string textToSanitize = "This is a string that contains a badword1, another Badword2 and a third badWord3";
    Chapter9.Recipes oRecipe = new Chapter9.Recipes();
    textToSanitize = oRecipe.SanitizeInput(textToSanitize);
    WriteLine(textToSanitize);
    Read();
    
  7. 当你运行你的应用程序时,你将在控制台窗口中看到以下内容:如何操作…

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

它是如何工作的…

让我们逐步分析代码,了解正在发生的事情。我们需要得到一个看起来像这样的正则表达式:\b(wordToMatch1|wordToMatch2|wordToMatch3)\b

这基本上是说,找到任何由\b表示的单词,并且只匹配整个单词。当我们查看我们创建的列表时,我们会看到我们想要从输入字符串中删除的单词:

如何工作…

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

如何工作…

为了完成有效的正则表达式模式,我们需要在模式两侧添加\b表达式,告诉正则表达式引擎只匹配整个单词。正如你所看到的,我们正在使用字符串插值。字符串插值将在第一章中详细介绍,C#6 的新特性

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

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

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

如何工作…

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

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

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

一旦你这样做,编译器就会逐字解释模式字符串,并生成正确的正则表达式模式:

如何工作…

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

如何工作…

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

如何工作…

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

动态正则表达式匹配

动态正则表达式匹配是什么意思呢?嗯,这不是一个官方术语,但我们用它来解释在运行时使用变量生成特定表达式的正则表达式。假设你正在为一个名为 Acme Corporation 的公司工作的文档管理系统,该系统需要实现文档的版本控制。为此,系统会验证文档是否具有有效的文件名。

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

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

注意

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

准备工作

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

using System.Text.RegularExpressions;

如何操作…

  1. 一种非常好的方法是使用扩展方法。这样,您可以直接在文件名变量上调用扩展方法,并对其进行验证。在您的 Recipes.cs 文件中,首先添加一个名为 CustomRegexHelper 的新类,并使用 public static 修饰符:

    public static class CustomRegexHelper
    {
    
    }
    
  2. 将常规扩展方法代码添加到 CustomRegexHelper 类,并调用 ValidAcmeCompanyFilename 方法:

    public static bool ValidAcmeCompanyFilename(this String value)
    {
    
    }
    
  3. 在您的 ValidAcmeCompanyFilename 方法内部,添加以下正则表达式。我们将在本食谱的 工作原理… 部分解释此正则表达式的构成:

    return Regex.IsMatch(value, $@"^acm[_]{DateTime.Now.Year}__(.txt|.docx|.xlsx)$");
    
  4. 完成此操作后,您的扩展方法应如下所示:

    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}]) _ (.txt|.docx|.xlsx)$");
        }
    }
    
  5. 回到 Recipes 类,创建一个名为 DemoExtendionMethod()void 返回类型的方法:

    public void DemoExtendionMethod()
    {
    
    }
    
  6. 添加一些输出文本以显示当前日期和有效的文件名类型:

    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");
    
  7. 然后,添加文件名检查代码:

    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");
    
  8. 您会注意到 if 语句包含对包含文件名的变量的扩展方法的调用:

    filename.ValidAcmeCompanyFilename()
    
  9. 如果您已完成此操作,您的方法应如下所示:

    public void DemoExtendionMethod()
    {
        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");
    }
    
  10. 回到控制台应用程序,添加以下代码,该代码仅调用 void 方法。这只是为了模拟之前提到的版本化方法:

    Chapter9.Recipes oRecipe = new Chapter9.Recipes();
    oRecipe.DemoExtendionMethod();
    Read();
    
  11. 完成后,运行您的控制台应用程序:如何操作…

工作原理…

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

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

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

条件或
&#124;
文件前缀和分隔符
acm
[_]
日期部分
{DateTime.Now.Year}
{DateTime.Now.Month}
0[{DateTime.Now.Month}]
{DateTime.Now.Day}
0[{DateTime.Now.Day}]
有效的文件格式
(.txt&#124;.docx&#124;.xlsx)
字符串的开始和结束
^
`
---
&#124;
文件前缀和分隔符
acm
[_]
日期部分
{DateTime.Now.Year}
{DateTime.Now.Month}
0[{DateTime.Now.Month}]
{DateTime.Now.Day}
0[{DateTime.Now.Day}]
有效的文件格式
(.txt&#124;.docx&#124;.xlsx)
字符串的开始和结束
^
告诉正则表达式引擎在给定字符串的结束位置停止匹配。

以这种方式创建正则表达式使我们总能保持其更新。因为我们必须始终匹配正在验证的文件中的当前日期,这创造了一个独特的挑战,但使用字符串插值、DateTime 和正则表达式 OR 语句可以轻松克服。

仔细查看一些更有用的正则表达式片段,你会发现这一章甚至还没有触及到可以完成的事情的表面。还有许多东西可以探索和学习。互联网上有许多资源,以及一些免费(有些在线)和商业工具可以帮助你创建正则表达式。

第十章. 选择和使用源控制策略

源控制是每个开发者工具箱的一个基本部分。无论你是业余爱好者还是专业程序员;当你从办公桌起身回家时,你最好确保你的代码是安全的。在本章中,我们将探讨选择和使用源控制策略。我们将查看的一些主题包括:

  • 设置 Visual Studio 账户管理和确定最适合你的源控制解决方案

  • 设置 Visual Studio GitHub 集成,首次提交代码,并提交更改

  • 使用 GitHub 进行团队合作,处理和解决代码冲突

简介

在我的职业生涯中,我使用过 Visual SourceSafe、SVN、VSTS、Bitbucket 和 GitHub。实际上,你如何操作并不重要,重要的是你要确保你的源代码安全并进行了版本控制。

设置 Visual Studio 账户管理和确定最适合你的源控制解决方案

Visual Studio 允许开发者创建账户并登录。如果你经常使用热桌或者在不同地点的多台机器上工作(比如工作场所和家用电脑),这尤其有益,因为 Visual Studio 会自动同步你在登录的机器上的设置。

准备工作

本食谱将假设你已经在你的机器上完成了 Visual Studio 2015 的安装。无论你是安装了 Visual Studio 2015 的试用版还是授权版本,这都不重要。

如何操作...

  1. 安装完成后,打开 Visual Studio:如何操作...

  2. 在 Visual Studio 的右上角,你会看到一个登录链接:如何操作...

  3. 点击登录链接,你将被允许在这里输入你的电子邮件地址。我发现使用我的 Outlook 电子邮件地址很有用。在我看来,这是最好的网络电子邮件之一。

    注意

    请注意,我并不是因为任何原因而推荐 Outlook,我只是真的认为它是一个非常好的产品。我还有一个 Gmail 账户以及一个 iCloud 电子邮件账户。

    如何操作...

  4. 添加你的电子邮件账户后,Visual Studio 会重定向你到一个登录页面:如何操作...

  5. 由于我已经有一个 Outlook 账户,Visual Studio 只需我使用它进行登录。如果你没有账户,你可以在这里创建一个:如何操作...

  6. Visual Studio 现在会要求你输入一些额外的信息。需要注意的是,如果你有一个 Team Services 账户,你可以在这里将其链接。目前,请将其留空,因为这将稍后在另一个食谱中处理:如何操作...

  7. 创建账户后,你可以通过查看 Visual Studio IDE 右上角的所选账户来确认你已经登录:如何操作...

  8. 点击您账户名称旁边的向下箭头,您可以看到您的账户设置…如何操作...

  9. 这将显示您的账户概览,从这里您可以进一步个性化您的账户:如何操作...

源代码控制的选择是每个开发者都有强烈意见的话题。不幸的是,如果您为老板工作,这个决定可能甚至不由您决定。许多公司已经按照他们喜欢的样子设置了源代码控制系统,您将需要遵循公司程序。事情就是这样。然而,了解作为独立开发者可用的选项是很好的。

所有优秀的开发者都应该在业余时间编写代码。您不仅在工作时是开发者。我们吃饭、呼吸、睡觉、生活都与代码息息相关。这是我们是谁和我们是什么的一部分。我要说的是,为了使您作为开发者的工作做得更好,您必须在业余时间玩代码。开始一个宠物项目,召集一些朋友,决定一起编写一些软件。这不仅会使你们在所做的事情上变得更好,而且你们会互相学到很多。

如果您是一名远程开发者,每天都不通勤到办公室工作,您仍然可以与开发者社区保持联系。为开发者提供了如此多的资源,开发者社区也乐于帮助新手并帮助他们成长。如果您不承诺(字面意思)保持您的代码安全,那么开始一个个人或宠物项目是没有用的。为此,您甚至不需要支付一分钱。Visual Studio Online(现在称为团队服务)和 GitHub 都为开发者提供了一个绝佳的平台来保护您的代码。

让我们从查看团队服务开始。您可以通过将浏览器指向www.visualstudio.com/en-us/products/what-is-visual-studio-online-vs来找到该网站。

在这里,您将看到微软为开发者提供了一个使用团队服务的绝佳机会。对于最多五个用户,这是完全免费的。这意味着您和您的伙伴们可以协作完成下一个大项目,同时确保您的代码保持安全。注册就像点击免费开始链接一样简单:

如何操作...

第二个出色的选择是 GitHub。它在免费提供方面略有不同,要求开发者在使用免费账户时使用公共仓库。如果您不介意您的代码基本上是开源的,那么 GitHub 是一个不错的选择。然而,在 GitHub 上,您可以拥有无限的合作者和公共仓库:

如何操作...

它是如何工作的...

选择源代码控制主要取决于您代码的开放性。如果您可以承担让其他开发者查看和下载您的代码,那么 GitHub 是一个很好的选择。如果您需要您的代码保持私密,并且只与特定的人共享,那么付费的 GitHub 账户将更适合。如果您目前不想花钱,那么 Team Services 将是您的最佳选择。

设置 Visual Studio GitHub 集成,首次提交代码以及提交更改

GitHub 多年来一直是一个强大的工具。许多开发者对其深信不疑。实际上,当使用苹果的 Xcode IDE 时,它就是默认选项。无论您出于什么原因选择使用 GitHub,请放心,您和您的代码都处于安全可靠的手中。

准备工作

以下步骤将假设您已经注册了 GitHub,并且已经启用了双因素认证。如果您还没有注册 GitHub 账户,您可以通过访问 www.github.com 并创建一个新账户来注册。要在您的 GitHub 账户上启用双因素认证(我强烈建议这样做),请按照以下步骤操作:

  1. 点击您的个人形象旁边的向下箭头,并选择设置准备工作

  2. 在下一页左侧出现的个人设置菜单中,选择安全准备工作

  3. 安全页面上的第一个部分将是您的双因素认证状态。要开始设置,请点击设置双因素认证按钮:准备工作

  4. 您将看到一个关于双因素认证的简要概述,并可以选择使用应用程序设置(我推荐)或使用短信设置。使用应用程序是最简单的,如果您有智能手机或平板电脑,您可以从相应的应用商店下载一个身份验证器应用程序。从那时起,按照 GitHub 给您的提示完成双因素认证的设置:准备工作

如何操作...

  1. 如果您是首次安装 Visual Studio 2015,请查看自定义安装选项。在常用工具下,展开后您将看到将 GitHub 添加到 Visual Studio 安装中的选项。在选择了该选项和其他安装选项后,点击下一步并完成安装向导窗口。现在 Visual Studio 2015 将开始安装。您可以现在休息一下,去喝杯咖啡,因为安装可能需要一段时间,具体取决于您的硬件和互联网连接速度:如何操作...

  2. 如果您已经安装了 Visual Studio 2015 但没有添加 GitHub 扩展,您可以从以下链接轻松下载并安装它:visualstudio.github.com/downloads/GitHub.VisualStudio.vsix

  3. 假设您有一个想要添加到 GitHub 的现有应用程序,将其添加到新仓库的过程相当简单。我仅仅创建了一个只包含模板代码的控制台应用程序,但您可以将任何项目类型和大小添加到 GitHub:如何操作...

  4. 在 Visual Studio 2015 的视图菜单中,选择团队资源管理器选项:如何操作...

  5. 托管服务提供商部分,您将看到两个选项。目前,我们将选择GitHub,因为我们已经有了账户,所以我们将点击连接…如何操作...

  6. 您现在将看到 GitHub 登录页面。如果您没有现有的 GitHub 账户,您也可以从这里注册:如何操作...

  7. 由于我在 GitHub 账户上设置了两步验证,我需要使用我的认证器应用程序输入生成的认证码并验证自己:如何操作...

  8. 经过认证后,您将返回到管理连接屏幕:如何操作...

  9. 接下来,您需要点击顶部的团队资源管理器窗口中的 Home 图标,它是一个小房子的图片。从主页屏幕,点击同步按钮:如何操作...

  10. 这将显示发布窗口。在 GitHub 下,点击开始使用链接。这将把您的项目发布到 GitHub 上的新仓库。

    注意

    记住,如果您使用的是免费的 GitHub,所有您的仓库都是公开的。如果您正在编写不能公开的代码(不是开源的),那么请注册一个包含私有仓库的付费 GitHub 账户。

    如何操作...

  11. 然后,GitHub 将提示您添加此发布的详细信息。因为您之前已经连接到 GitHub,所以您的用户名已经在下拉菜单中选中。准备好后,点击发布如何操作...

  12. 当项目发布到 GitHub 后,您将自动返回到主页屏幕:如何操作...

  13. 在线查看您的 GitHub 账户,您会看到项目已经被添加:如何操作...

  14. 接下来,让我们对CommandCentre应用程序进行一些修改。直接添加一个新类到您的项目中。我命名为Dominion.cs,但您可以根据自己的喜好命名:如何操作...

  15. 你会注意到,一旦你对项目进行了更改,解决方案就会用红色勾号标记更改的项目:如何操作...

  16. 要将更改添加到你的 GitHub 仓库,你可以遵循两条路径。第一个选项是进入团队资源管理器 - 主页窗口,然后点击更改按钮:如何操作...

  17. 第二个(在我看来更方便的)选项是右键单击解决方案资源管理器中的解决方案,然后从上下文菜单中选择提交...菜单项:如何操作...

  18. 当你第一次执行提交操作时,GitHub 可能会要求你提供用户信息:如何操作...

  19. 在你被允许提交更改之前,你必须填写所需的提交信息。在一个真实的项目团队中,你的提交信息应该尽可能详细。考虑使用任务项代码(或待办事项代码)来唯一标识正在添加的代码。这将在未来某个时候为你(或另一位开发者)节省很多麻烦,我保证:如何操作...

  20. 有一个重要的事情需要注意,那就是如果你点击提交所有按钮旁边的向下箭头,你有三个提交选项可供选择。提交所有按钮只会记录你在本地机器上所做的更改。换句话说,更改不会反映在远程仓库中。提交所有并推送按钮会记录你在本地机器上的更改,并将这些更改推送到你的远程 GitHub 仓库。提交所有并同步按钮会记录你在本地机器上的更改,然后它会从远程仓库拉取任何更改,最后进行推送。如果你在团队中工作,你会想要这样做。然而,对于这个配方,我只会进行提交所有并推送,因为我是这个仓库唯一的开发者:如何操作...

  21. 当提交完成时,团队资源管理器窗口会通知你提交成功:如何操作...

  22. 转到 GitHub 在线,你会看到新推送的更改反映在你的 GitHub 仓库中,以及提交信息:如何操作...

  23. GitHub 是任何开发者的绝佳源代码管理解决方案。考虑创建一个开源项目。这比你想象的更有益。

它是如何工作的...

免费 GitHub 账户允许您创建公共仓库。这意味着任何人都可以在 GitHub 上搜索、查看和克隆您的项目到自己的桌面。这是 GitHub 的核心思想。这对不想花钱的独立开发者和企业来说显然是一个关键因素。然而,企业比独立开发者更有能力承担费用。但我认为,一些公司宁愿自己搭建而不是使用云中托管的服务提供商。这意味着他们更愿意通过在自己的企业服务器上设置源控制系统来保持源控制在自己手中。为独立开发者提供 GitHub 选项是一个绝佳的解决方案。对于那些需要私有仓库的人来说,费用也不是障碍。

使用 GitHub 团队合作,处理和解决代码冲突

当在团队中工作时,GitHub 和团队服务真正发挥其优势。协作努力的效果非常强大。有时,这可能会有些挑战。让我们看看如何使用 GitHub 在团队环境中工作。

准备就绪

我们将使用已检查到 GitHub 的现有CommandCentre应用程序。在您允许其他开发者将代码推送到您的分支之前,您需要将他们添加为协作者。为此,登录 GitHub 并点击加号旁边的向下箭头。在菜单中点击新建协作者

准备就绪

您可以通过输入他们的 GitHub 用户名、全名或电子邮件地址来搜索要添加的协作者:

准备就绪

完成后,点击添加协作者按钮将该用户添加为项目的协作者:

准备就绪

如何操作...

  1. 假设一位新开发者(我们称他为 John)加入了团队。您已经将开发者添加为项目的协作者。John 开始设置他的 Visual Studio 环境,包括连接到 GitHub。在菜单中点击团队,然后点击管理连接…如何操作...

  2. 托管服务提供商选项中,选择 GitHub 服务下的连接…如何操作...

  3. 使用您的电子邮件地址和密码登录 GitHub。

    注意

    注意,如果您刚刚注册了 GitHub,您需要点击发送到您注册时指定的电子邮件地址的验证电子邮件。如果不验证您的电子邮件地址,您将无法从 Visual Studio 登录。

    如何操作...

  4. 连接后,您将看到 GitHub 详细信息已加载:如何操作...

  5. 我们现在想要在CommandCentre应用程序上工作。您可以通过按名称搜索在 GitHub 上找到它:如何操作...

  6. 当您找到正确的项目时,从页面上的HTTPS文本框中复制 URL:如何操作...

  7. 在 Visual Studio 中,展开本地 Git 仓库并点击克隆。将复制的 URL 粘贴到 Git 仓库路径中,并指定代码应在您的硬盘上克隆到的位置。准备好后,点击克隆如何操作...

  8. 当代码被克隆时,您将在之前指定的文件夹路径中看到它:如何操作...

  9. 是时候对代码进行一些更改了。以正常方式在 Visual Studio 中打开项目。John 决定在Dominion.cs类上工作,并添加了一个返回倒计时整数的函数:如何操作...

  10. 代码更改完成后,John 将刚刚添加的代码提交到 GitHub 仓库:如何操作...

  11. GitHub 随后要求输入 John 的姓名和电子邮件地址以进行此提交:如何操作...

  12. John 添加了一个有意义的提交信息来描述他所作的更改:如何操作...

  13. 然后,他点击全部提交并同步如何操作...

  14. John 的更改已提交到 GitHub 仓库:如何操作...

  15. 在办公室的另一边,我正在处理同一块代码。唯一的问题是,我添加了与自己的CountDown逻辑实现相同的方法:如何操作...

  16. 我准备好并将我的更改提交到 GitHub:如何操作...

  17. GitHub 立即阻止我这样做。这是因为如果我的代码被推送,John 之前的提交将会丢失。GitHub 在 GitHub 帮助中对此有很好的帮助文件:help.github.com/articles/dealing-with-non-fast-forward-errors/如何操作...

  18. 要解决这个问题,请点击拉取以获取 John 所做的最新提交。此时,您的代码将处于冲突状态。这听起来很糟糕,但并非如此。这使您能够控制决定使用哪段代码。您可以看到,拉取操作显示了有冲突的文件以及 John 添加的传入提交信息:如何操作...

  19. 要查看冲突,请点击消息弹出窗口中的解决冲突链接:如何操作...

  20. 您将看到解决冲突屏幕上列出有冲突的文件。点击一个文件将展开为简短摘要和操作选项屏幕。始终点击比较文件链接以查看冲突文件之间的差异:如何操作...

  21. 代码中的差异立即显而易见。你接下来遵循的过程取决于你们团队合作的方式。通常,冲突可能相当复杂,与相关开发者讨论前进的方式总是一个好主意:如何操作...

  22. 在这种情况下,我和约翰决定他的代码更好、更简洁。因此,决定简单地点击取远程并使用约翰的代码。当你点击了链接后,你需要点击提交合并如何操作...

  23. 在添加提交信息后,你可以将你的代码推送到仓库。在这种情况下,我简单地用约翰的代码替换了我所有的代码,但可能存在你需要使用你自己的代码和另一位开发者的代码的情况。GitHub 允许我们轻松地处理这些冲突:如何操作...

  24. 在将代码推送到远程后,GitHub 会通知你代码已成功同步:如何操作...

它是如何工作的...

GitHub 简化了提交、解决冲突和合并代码的痛苦。毫无疑问,它是任何开发者工具箱中的必备工具,对于开发团队来说是必不可少的。

第十一章. 在 Visual Studio 中创建移动应用程序

Visual Studio 是集成开发环境IDE)中的佼佼者。这一点毫无疑问。作为开发者,你可以通过为广泛的平台创建应用程序来变得非常灵活。其中之一就是移动开发。开发者开始创建移动应用程序,但不想使用不同的 IDE。在 Visual Studio 2015 中,你不必这样做。它将允许你创建 Android 和(现在通过Xamarin)iOS 应用程序。因此,本章将探讨以下概念:

  • 安装 Xamarin 和其他必需组件

  • 使用 Apache Cordova 创建 Android Visual Studio 项目

  • 使用 Xamarin Forms 创建 iOS 应用程序

简介

如果你还没有听说过 Xamarin,我们鼓励你通过 Google 搜索这个工具。传统上,开发者需要使用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 项目,并针对所有三个平台。这使得开发者针对多个平台变得非常容易。

安装 Xamarin 和其他必需组件

Xamarin 可以在自定义 Visual Studio 安装过程中安装。现在,让我们假设 Xamarin 尚未安装,并且在你安装 Visual Studio 之后需要现在安装它。

准备工作

如果你想针对 iOS 进行开发,需要注意的一点是你将需要使用 Mac 来构建你的 iOS 应用程序。

如何操作…

  1. 控制面板中,点击程序和功能。右键单击你的 Visual Studio 安装,然后点击更改如何操作…

  2. 这将显示 Visual Studio 安装程序。在这里,您可以随意添加和删除组件来修改您当前的 Visual Studio 安装。请注意,我们已选择C#/.NET (Xamarin v4.0.3)HTML/JavaScript (Apache Cordova) Update 8.1进行安装。如果您对使用 Xamarin 没有兴趣,则可以不安装 Xamarin 组件,只保留 Apache Cordova 选项选中。这将仍然允许您使用 Apache Cordova 而不是 Xamarin 来创建 Android 应用程序。同样,如果您对 Apache Cordova 没有兴趣,只想使用 Visual Studio 创建 Android 应用程序和 iOS 应用程序,请选择安装 Xamarin 组件。其余的安装过程很简单:如何操作…

  3. 如果我们想使用 Xamarin 来针对 iOS 应用程序,我们还需要采取第二个步骤。我们必须在 Mac 上安装所需的软件。在您的 Mac 上打开 Xamarin 的网站。网址是www.xamarin.com/。点击产品下拉菜单,从列表中选择Xamarin Platform如何操作…

  4. 您也可以通过访问www.xamarin.com/platform来获取所需页面。点击免费下载按钮将在您的 Mac 上安装一个名为 Xamarin Studio 的程序。您需要知道,当安装在 Mac 上时,Xamarin Studio 无法创建 Windows 应用程序。它只会允许您在 Mac 上创建 iOS 和 Android 应用程序。除了 Xamarin Studio,您还将获得 Xamarin Mac Agent(之前称为 Xamarin Build Host)。这是一个必需的组件,以便您可以将您的 PC 连接到您的 Mac 以构建您的 iOS 应用程序。最后,PC 和 Mac 也必须能够通过网络相互连接(关于这一点稍后会有更多说明):如何操作…

  5. 在 Mac 上下载安装程序后,安装过程很简单。只需按照屏幕提示完成安装:如何操作…

它是如何工作的…

我们之前在安装 Xamarin 和 Apache Cordova 时采取的步骤将使我们能够做到以下事情:

  • 安装 Apache Cordova:如果您只想针对 Android、iOS 和 Windows,但不想使用 Xamarin

  • 安装 Xamarin:如果您想针对 Android、iOS、Windows 或三者都进行目标定位,并使用单个解决方案来实现

Visual Studio 非常灵活,为开发者提供了广泛的选择。

使用 Apache Cordova 创建 Android Visual Studio 项目

使用 Apache Cordova 创建 Android 应用程序非常简单。然而,这个配方只会向您展示如何开始。

准备工作

在 Visual Studio 设置过程中,您需要将 Apache Cordova 作为自定义安装选项的一部分进行安装。要了解如何操作,请参阅本章中的安装 Xamarin 和其他必需组件配方。

如何操作…

  1. 新建项目对话框屏幕中,选择Apache Cordova 应用程序,并将空白应用(Apache Cordova)作为要使用的模板。选择项目位置,然后点击确定按钮:如何操作……

  2. 一旦 Visual Studio 创建了你的应用程序,你会注意到它有一个非常具体的结构。从项目来看,你会注意到你可以针对 Android、iOS、Windows 或 Windows Phone 8.1。这是你将用来创建 Android 应用程序的框架:如何操作……

  3. 当你准备好调试时,你可以从调试菜单中选择一个模拟器。这将部署你的应用程序到所选模拟器,并允许你测试你的应用程序:如何操作……

它是如何工作的……

能够使用 Visual Studio 从一个解决方案中针对不同的移动设备,这给了开发者自由去实验,找到最适合他们和发展风格的解决方案。

使用 Xamarin Forms 创建 iOS 应用程序

许多开发者都想尝试编写 iOS 应用程序。一直以来,最大的缺点就是学习一门新的编程语言和新的 IDE。对一些人来说,这可能不是问题,因为他们想学习新东西。但对许多.NET 开发者来说,能够坚持使用他们熟悉的 IDE 和编程语言是非常有利的。嗯,这正是 Xamarin Forms 和 Visual Studio 所实现的。它为.NET 开发者提供了使用 Visual Studio 编写可以在多个平台上轻松运行的应用程序的能力,而不需要为每个平台编写单独的代码库。

准备工作

你需要有一台运行 OS X 的 Mac。你只需要这个来调试 iOS 应用程序。

如何操作……

  1. 在 Visual Studio 2015 中创建一个新的项目。从已安装的模板中选择跨平台,然后选择空白应用(Xamarin.Forms.Portable)。这将使我们能够创建一个跨平台的应用程序,而不是针对单一平台(例如 Android 或 iOS):如何操作……

  2. 项目创建可能需要几分钟才能完成。在这个过程中,你可能会看到一个消息告诉你 Windows 10(假设你在运行 Windows 10)的开发者模式尚未启用:如何操作……

  3. 启用这个功能很简单。你可以在弹出的消息中点击开发者设置链接,或者你可以在 Windows 10 设置中的查找设置搜索框中输入开发者模式如何操作……

  4. 点击开发者模式选项将显示使用开发者功能确认对话框。只需点击继续:如何操作……

  5. 项目创建完成后,你将看到一个开始使用 Xamarin.Forms的屏幕:如何操作……

  6. 查看你的解决方案资源管理器,你会注意到已经创建了几个项目。我们只关注 iOS 项目:如何操作…

  7. 查看调试目标时,你会注意到,当你将目标更改为 Droid,例如,Android 项目被设置为启动项目。如果你将其设置为 iOS,也会发生相同的情况:如何操作…

  8. 目前为止,在你开始调试 iOS 应用程序之前,你需要将 Visual Studio 连接到 Mac 上的 Xamarin Mac Agent。在 Visual Studio 中,将鼠标悬停在 iOS 工具栏上的Xamarin Mac Agent按钮上。它将显示为未连接:如何操作…

    注意

    参考本章前面的安装 Xamarin 和其他必需组件部分,了解如何安装 Xamarin Mac Agent。

  9. 要连接到 Xamarin Mac Agent,点击此按钮。将显示Xamarin Mac Agent 说明窗口。你可以按照此屏幕上的说明操作,说明如下:如何操作…

  10. 在你的 Mac 上,打开系统偏好设置。查找并点击共享图标:如何操作…

  11. 这将显示共享窗口。从左侧菜单中选择远程登录,然后在仅这些用户下选择或添加你当前 Mac 用户到这个列表中:如何操作…

  12. 当你将你的当前 Mac 用户添加到远程登录列表中后,点击后退按钮返回到上一个屏幕。然后查找并点击网络如何操作…

  13. 这将打开网络屏幕。查看显示当前状态为已连接的地方。下面,你会看到一个 IP 地址。记下显示的 IP 地址,因为你将需要使用它将 Visual Studio 连接到 Xamarin Mac Agent:如何操作…

    注意

    只要注意,我已经故意在截图上隐藏了我的 IP 地址。

  14. 在 Windows 中,在 Visual Studio 中点击确定以关闭Xamarin Mac Agent 说明屏幕。现在,Xamarin Mac Agent屏幕将可见。在此屏幕底部,点击添加 Mac…按钮:如何操作…

  15. 这将显示添加 Mac屏幕,在这里你需要输入从 Mac 的系统偏好设置中的网络屏幕上记下的 IP 地址。点击添加按钮:如何操作…

    注意

    只要注意,我已经故意在截图上隐藏了我的 IP 地址。

  16. 现在,你将被要求提供之前在远程登录屏幕上添加的 Mac 用户的用户名和密码。点击登录按钮:如何操作…

    注意

    只要注意,我已经故意在截图上隐藏了 IP 地址和 GUID。

  17. 点击登录后,你应该会自动从 Visual Studio 连接到你的 Xamarin Mac Agent:如何操作…

  18. 你现在可以选中你想要调试的 iOS 设备。正如你所见,有各种各样的 iOS 设备可供选择:如何操作…

  19. 为了本菜谱的目的,我们只是选择了一个iPhone 4S iOS 9.3。点击调试按钮以启动应用:如何操作…

  20. 这将现在构建你的应用程序,并通过网络连接将信息发送到 Xamarin Mac 代理。然后它将在你的 Mac 上启动模拟器。第一次这样做时,启动模拟器可能需要几分钟,但一旦完成,后续的调试会快得多:如何操作…

  21. 在 Mac 上启动模拟器后,Xamarin 应用程序将被启动:如何操作…

  22. 当 Xamarin 启动画面关闭时,你会看到欢迎使用 Xamarin Forms!文本:如何操作…

  23. 在 Visual Studio 中返回,停止调试。你会注意到应用在 Mac 的模拟器中关闭,并且调试在 Visual Studio 中停止。然而,模拟器在你的 Mac 上仍然保持打开状态。

    现在让我们更改一些文本。查看你的 Visual Studio 解决方案中的可移植项目。这是所有其他解决方案中项目将使用的共享项目。在可移植项目中,点击 App.cs 文件:

    如何操作…

  24. 默认代码显示。在这里,你可以看到我们在之前调试的应用程序中看到的 Welcome to Xamarin Forms! 文本:如何操作…

  25. 将代码更改为如下所示。我们正在做的是添加日期和时间。这里有几个需要注意的地方:

    • 我们在这里使用标准的 .NET DateTime 库。

    • 我们使用字符串插值来创建在表单上显示的文本:

      MainPage = new ContentPage
      {
          Content = new StackLayout
          {
              VerticalOptions = LayoutOptions.Center,
              Children = {
                  new Label { HorizontalTextAlignment = TextAlignment.Center,
                      Text = $"Welcome to Xamarin Forms! The date is {DateTime.Now}"
                  }
              }
          }
      };
      
  26. 完成这些后,再次调试你的应用程序。当模拟器在 Mac 上显示你的 iOS 应用程序时,你会看到日期和时间被显示出来:如何操作…

它是如何工作的…

需要注意的一点是,我们在这里做的并没有比在其他任何标准 .NET 应用程序中做的更多。我们正在编写 C# 代码并将其编译以在 iOS 操作系统上运行。我们也可以轻松地将应用程序更改为在任何 iOS 设备上调试。我们不需要学习 Objective-C 或 Swift(尽管 Swift 是一种很棒的语言,值得学习)。我们也不需要熟悉新的 IDE(用于开发 iOS 和 Mac 应用的 Xcode)。我们不需要调整任何约束,修改任何游乐场元素,或学习如何使用任何新的控件。Xamarin Forms 和 Visual Studio 会为我们处理所有这些。最好的是,Xamarin 现在免费与 Visual Studio 一起使用。没有理由你不应该尝试编写 iOS 应用程序。

第十二章。在 Visual Studio 中编写安全代码和调试

在本章中,我们将探讨一些作为开发者提高调试代码效率的示例。我们还将探讨如何编写安全代码。编写安全代码可能是一个挑战,但考虑以下情况:如果你的代码安全部分涉及确保密码被安全存储,为什么要在项目之间反复编写相同的代码?编写一次代码,并在你创建的每个新项目中实施它。我们将探讨的概念如下:

  • 正确加密和存储密码

  • 在代码中使用 SecureString

  • 保护 App.config/web.config 中的敏感部分

  • 防止 SQL 注入攻击

  • 使用 诊断工具历史调试

  • 设置条件断点

  • 使用 PerfTips 识别代码中的瓶颈

简介

许多开发者往往忽略的是编写安全代码的需求。开发截止日期和其他项目相关压力导致开发者将交付代码置于正确执行之上。你们中许多人可能不同意我的观点,但请相信我,我已经多次听到“我们没有预算做这件事”的借口。这通常是在开发预算由其他利益相关者确定,而开发者没有被咨询的情况下发生的。

考虑这样一种情况,顾问告诉开发者他们已经向客户销售了一个系统。现在需要开发这个系统。此外,开发者被告知他们有 x 小时的时间来完成开发。给开发者一份概述要求的文档,并允许开发者开始,并在规定的时间内完成开发。

这种场景是许多开发者面临的现实。你可能认为这种场景根本不可能存在,或者也许你正在阅读这篇文章,并认为这个场景与你公司当前的工作流程相符。无论情况如何,这是软件开发中今天正在发生的事情。

那么开发者如何应对项目自杀?(我之所以这样称呼这些项目,是因为以这种方式处理的项目很少能成功。)首先,创建可重用代码。想想那些你经常重复执行到足以编写可重用 DLL 的过程。你知道你可以创建 Visual Studio 模板吗?如果你有一个你经常使用的标准项目结构,从它创建一个模板,并在每个新项目中重用它,从而加快交付速度并减少错误。

对于项目模板的一些考虑包括数据库层、安全层、常见验证代码(这个数据表是否包含任何数据)、常见扩展方法等。

正确加密和存储密码

我经常看到的是存储不当的密码。仅仅因为密码存储在你的服务器上的数据库中,并不意味着它是安全的。那么存储不当的密码是什么样的呢?

正确加密和存储密码

存储不当的安全密码不再安全。上一张截图中的密码是实际的用户密码。在登录屏幕上输入第一个密码 ^tj_Y4$g1!8LkD 将允许用户访问系统。密码应安全地存储在数据库中。实际上,你需要使用加盐密码散列。你应该能够加密用户的密码,但永远不要解密它。

那么你如何解密密码以匹配用户在登录屏幕上输入的密码呢?好吧,你不需要。你总是对用户在登录屏幕上输入的密码进行散列。如果它与数据库中存储的实际密码的散列值匹配,你就允许他们访问系统。

准备工作

本食谱中的 SQL 表仅用于说明,代码中并未写入。数据库可以在与本书源代码一起提供的 _database scripts 文件夹中找到。

如何操作…

  1. 通过在解决方案上右键单击,创建一个新的类库,然后从上下文菜单中选择 添加新建项目如何操作…

  2. 添加新项目对话框屏幕中,从已安装的模板中选择类库,并将你的类命名为Chapter12如何操作…

  3. 你新的类库将以默认名称 Class1.cs 添加到你的解决方案中,我们将其重命名为 Recipes.cs 以便正确区分代码。然而,如果你觉得这样更有意义,你可以将类重命名为你喜欢的任何名称。

  4. 要重命名你的类,只需在 解决方案资源管理器 中单击类名,然后从上下文菜单中选择 重命名如何操作…

  5. Visual Studio 将要求你确认重命名项目中所有对代码元素 Class1 的引用。只需点击 如何操作…

  6. 以下类被添加到你的 Chapter12 库项目中:

    namespace Chapter12
    {
        public class Recipes
        {
    
        }
    }
    
  7. 将以下 using 语句添加到你的类中:

    using System.Security.Cryptography;
    
  8. 接下来,你需要向类中添加两个属性。这些属性将存储盐和散列。通常你会在数据库中将这些值与用户名一起写入,但为了本食谱的目的,我们将简单地将其添加到静态属性中。还要添加两个名为 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)
        {                  
    
        }
    }
    
  9. 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);            
      }
      
  10. 我们需要创建的下一个方法是ValidateLogin()方法。在这里,我们首先验证用户名。如果用户输入的用户名不正确,不要告诉他们。这将警告试图破坏系统的人他们有错误的用户名,并且一旦他们收到错误密码的通知,他们就会知道用户名是正确的。这个方法中的步骤如下:

    1. 从数据库中获取输入用户名的盐值和哈希值。

    2. 使用从数据库中读取的盐对登录屏幕上输入的用户密码进行盐化。

    3. 使用与用户注册时相同的哈希算法对加盐后的密码进行哈希处理。

    4. 将从数据库中读取的哈希值与在方法中生成的哈希值进行比较。如果两个哈希值匹配,则表示密码输入正确,用户已验证。

      注意,我们从未从数据库中解密密码。如果你有解密用户密码并匹配输入密码的代码,你需要重新考虑并重写你的密码逻辑。系统永远不应该能够解密用户密码。

      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.");            
      }
      
  11. 为了测试代码,在你的CodeSamples项目中添加对Chapter12类的引用:如何操作…

  12. 因为我们已经创建了一个静态类,你可以在你的Program.cs文件中添加新的using static

    using static Chapter12.Recipes;
    
  13. 通过调用RegisterUser()方法并传入usernamepassword变量来测试代码。之后,调用ValidateLogin()方法,看看密码是否与哈希值匹配。这显然不会在实际的生产系统中同时发生:

    string username = "dirk.strauss";
    string password = "^tj_Y4$g1!8LkD";
    RegisterUser(password, username);
    
    ValidateLogin(password, username);
    Console.ReadLine();
    
  14. 当你调试代码时,你会看到用户已被验证:如何操作…

  15. 最后,稍微修改一下代码,将password变量设置为其他值。这将模拟用户输入错误密码的情况:

    string username = "dirk.strauss";
    string password = "^tj_Y4$g1!8LkD";
    RegisterUser(password, username);
    
    password = "WrongPassword";
    ValidateLogin(password, username);
    Console.ReadLine();
    
  16. 当你调试应用程序时,你会看到用户没有被验证:如何操作…

它是如何工作的…

在代码的任何地方,我们都没有解密密码。事实上,密码从未在任何地方存储过。我们总是与密码的哈希值一起工作。以下是这个菜谱中需要记住的重要点:

  • 永远不要在 C#中使用Random类来生成你的盐。始终使用RNGCryptoServiceProvider类。

  • 在你的代码中永远不要重复使用相同的盐。所以不要创建一个包含你的盐的常量,并使用它来对系统中所有的密码进行盐化。

  • 如果密码不匹配,不要告诉用户密码错误。同样,也不要告诉用户他们输入了错误的用户名。这可以防止试图破坏系统的人知道他们正确地获得了两个登录凭证中的一个。如果用户名或密码输入错误,最好通知用户他们的登录凭证不正确。这可能意味着用户名或密码(或两者)输入错误。

  • 您无法从数据库中存储的哈希值或盐值中获取密码。因此,如果数据库被破坏,其中存储的密码数据就不会处于风险之中。用户密码的加密是一个单向操作,这意味着它永远不能被解密。同样重要的是要注意,即使源代码被恶意意图的人窃取,您也无法使用该代码来解密数据库中的加密数据。

  • 将之前的方法与强大的密码策略相结合(因为即使在 2016 年,仍然有用户认为使用'l3tm31n'作为密码就足够好了),您就拥有了一个非常好的密码加密流程。

当我们查看用户访问表时,存储用户凭据的正确方式看起来可能像这样:

如何工作…

盐值和哈希值与用户名一起存储,并且是安全的,因为它们不能被解密以暴露实际密码。

小贴士

如果您在互联网上注册一项服务,并且他们通过电子邮件或短信向您发送确认,并在该消息中以纯文本形式显示您的密码,那么您应该认真考虑关闭您的账户。如果一个系统可以读取您的密码并以纯文本形式将其发送给您,那么任何人都可以这样做。永远不要为所有登录使用相同的密码。

在代码中使用 SecureString

保护应用程序免受恶意攻击不是一项容易的任务。这是在编写安全代码的同时最小化错误(黑客通常利用这些错误)和黑帽编写越来越复杂的手段来破坏系统和网络之间的持续斗争。我个人认为,高等教育机构需要教给 IT 学生两件事:

  • 如何使用和集成流行的 ERP 系统

  • 正确的软件安全原则

事实上,我认为安全编程 101 不应该只是某个 IT 课程中的一个模块或主题,而应该是一个独立的课程。它需要得到应有的严肃和尊重,并且最好由能够实际入侵系统或网络的人来教授。

白帽教学生如何妥协系统、利用脆弱的代码和渗透网络,这将大大改变未来软件开发者对待编程的方式。这归结为开发者在进行防御性编程时知道不要做什么。完全有可能,其中一些学生可能会成为黑帽,但这与他们是否参加了关于安全编程黑客的课程无关。

准备工作

代码在某些地方可能看起来有点奇怪。这是因为 SecureString 正在使用非托管内存来存储敏感信息。请放心,SecureString 在 .NET Framework 中得到了很好的支持和使用,这可以从用于创建数据库连接的 SqlCredential 对象的实例化中看出:

准备工作

如何操作…

  1. 首先在您的解决方案中添加一个新的 Windows Forms 项目:如何操作…

  2. 将项目命名为 winformSecure 并单击 确定 按钮:如何操作…

  3. 工具箱 中搜索 TextBox 控件并将其添加到您的表单中:如何操作…

  4. 最后,在您的表单中添加一个按钮控件。您可以随意调整此表单的大小,使其看起来更像登录表单:如何操作…

  5. 在 Windows Forms 上选择文本框控件后,打开 属性 面板并单击事件按钮(看起来像闪电)。在 组中,双击 KeyPress 事件以在代码后创建处理程序:如何操作…

    为您创建的代码是文本框控制的 KeyPress 事件处理程序。这将在用户按下键盘上的任何键时触发:

    private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
    {
    
    }
    
  6. 属性 面板中,展开 行为 组并将 UseSystemPasswordChar 的值更改为 true如何操作…

  7. 在代码后,添加以下 using 语句:

    using System.Runtime.InteropServices;
    
  8. SecureString 变量作为全局变量添加到您的 Windows Forms 中:

    SecureString secure = new SecureString();
    
  9. 然后在 KeyPress 事件中,每次用户按下键时,将 KeyChar 值追加到 SecureString 变量中。您可能想要添加代码来忽略某些按键,但这超出了本食谱的范围:

    private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
    {
        secure.AppendChar(e.KeyChar);
    }
    
  10. 然后在 登录 按钮的事件处理程序中,添加以下代码以从 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();
        }
    }
    
  11. 运行您的 Windows Forms 应用程序并输入一个密码:如何操作…

  12. 然后点击 登录 按钮。您将在消息框中看到您输入的密码:如何操作…

它是如何工作的…

对于许多开发者来说,使用 System.String 来存储敏感信息,如密码,已经成为一种习惯。这种方法的缺点是 System.String 是不可变的。这意味着 System.String 在内存中创建的对象不能被更改。如果你修改了变量,就会在内存中创建一个新的对象。你也不能确定 System.String 创建的对象在垃圾回收期间何时会被从内存中移除。相反,通过使用 SecureString 对象,你可以加密敏感信息,当该对象不再需要时,它就会被从内存中删除。SecureString 在非托管内存中加密和解密你的敏感数据。

现在我需要在这里明确一点。SecureString 绝非万无一失。如果你的系统中存在一个专门用来破坏 SecureString 操作的病毒,使用它帮助不大(无论如何,请确保使用合适的反病毒软件)。在代码执行的过程中,你的密码(或敏感信息)的字符串表示形式是可见的。其次,如果黑客找到了一种方法来检查你的堆或记录你的按键,密码可能会被看到。然而,使用 SecureString 使得黑客的机会窗口变得更小。机会窗口的缩小是因为攻击向量(黑客的入口点)更少,从而减少了你的攻击面(黑客所有攻击点的总和)。

重要的是:SecureString 的存在是有原因的。作为一名关注安全的软件开发者,你应该使用 SecureString

保护 App.config/web.config 的敏感部分

作为一名开发者,你无疑会处理诸如密码之类的敏感信息。你在开发过程中如何处理这些信息非常重要。过去,我曾收到一份客户的实时数据库副本用于测试。这对客户来说确实存在一个很大的安全风险。

通常,我们会在 web.config 文件中保存设置(当与网络应用程序一起工作时)。然而,在这个例子中,我将演示一个使用 App.config 文件的控制台应用程序。同样的逻辑也可以应用于 web.config 文件。

准备工作

创建控制台应用程序是演示这个菜谱的最快方式。如果你想要使用网络应用程序(并保护 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>
    
  3. 为了克服这种漏洞,我们需要将敏感数据从 App.config 文件移到另一个文件中。为此,我们指定一个包含我们想要从 App.config 文件中移除的敏感数据的文件路径:

    <appSettings file="C:\temp\secret\secret.config">
    

    注意

    你可能想知道,为什么不直接加密信息呢?实际上,这确实是既定的。这个值以纯文本形式存在只是为了演示一个概念。在现实世界中,你可能会加密这个值。然而,你肯定不希望这些敏感信息以加密的形式存储在某个代码库的服务器上。请确保安全,将其从你的解决方案中移除。

  4. 当你添加了安全文件的路径后,删除包含敏感信息的密钥:如何操作…

  5. 导航到 App.config 文件属性中指定的路径。创建你的 secret.config 文件并打开它进行编辑:如何操作…

  6. 在此文件中,重复 appSettings 部分,并将 Secret 密钥添加到其中。现在发生的情况是,当你的控制台应用程序运行时,它会读取解决方案中的 appSettings 部分,并找到对秘密文件的引用。然后它会查找秘密文件,并将其与解决方案中的 App.config 文件合并:如何操作…

  7. 为了验证这个合并是否成功,向你的控制台应用程序添加一个引用:如何操作…

  8. 搜索并添加 System.Configuration 到你的引用中:如何操作…

  9. 当你添加了引用后,你的解决方案引用应该看起来像这样:如何操作…

  10. 在你的 Program.cs 文件顶部添加以下 using 语句:

    using System.Configuration;
    
  11. 将以下代码添加到读取 Secret 密钥设置的 App.config 文件中。这次,它将读取合并的文件,该文件由你的 App.configsecret.config 文件组成:

    string sSecret = ConfigurationManager.AppSettings["Secret"];
    Console.WriteLine(sSecret);
    Console.ReadLine();
    
  12. 运行你的控制台应用程序,你会看到敏感数据已经从 secret.config 文件中读取,该文件在运行时与 App.config 文件合并:如何操作…

它是如何工作的…

这里需要指出的是,这种技术也适用于 web.config 文件。如果你需要从配置文件中移除敏感信息,请将其移动到另一个文件,这样它就不会包含在源代码控制检查或部署中。

防止 SQL 注入攻击

SQL 注入攻击是一个非常现实的问题。有太多应用程序仍然容易受到这种攻击。如果你开发了一个网络应用程序或网站,你应该警惕不良的数据库操作。易受攻击的 SQL 注入暴露了数据库容易受到这种攻击。SQL 注入攻击是指攻击者通过网页表单输入框修改 SQL 语句,以产生与最初意图不同的结果。这通常发生在网络应用程序应该访问数据库以验证用户登录的表单上。如果不清理用户输入,你将使你的数据容易受到这种攻击的利用。

缓解 SQL 注入攻击的接受解决方案是创建一个参数化存储过程,并从你的代码中调用它。

准备工作

在继续本菜谱之前,你需要在 SQL Server 中创建 CookbookDB 数据库。你可以在附带的源代码中的 _database scripts 文件夹中找到脚本。

如何操作…

  1. 对于这个菜谱,我使用的是 SQL Server 2012。如果你使用的是更早版本的 SQL Server,概念是相同的。在你创建了 CookbookDB 数据库之后,你会在 Tables 文件夹下看到一个名为 UserDisplayData 的表:如何操作…

  2. UserDisplayData 表仅用于说明使用参数化存储过程查询的概念。在生产数据库中,它没有任何实际的好处,因为它只返回一个屏幕名称:如何操作…

  3. 我们需要创建一个存储过程来从表中选择特定 ID(用户 ID)的数据。点击 Programmability 节点以展开它:如何操作…

  4. 接下来,右键单击 Stored Procedures 节点,并从上下文菜单中选择 New Stored Procedure…如何操作…

  5. 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
    
  6. 给存储过程一个合适的名称,以描述存储过程的行为或意图:

    CREATE PROCEDURE cb_ReadCurrentUserDisplayData
    

    注意

    有很多人在他们的存储过程前加上前缀,我也是其中之一。我喜欢将我的存储过程分组。因此,我按照格式 [prefix][tablename_or_module][stored_procedure_action] 命名我的存储过程。话虽如此,我通常避免在我的存储过程中使用 sp_ 作为前缀。互联网上有许多关于为什么这是一个坏主意的意见。普遍认为,使用 sp_ 作为存储过程前缀会影响性能,因为它在 master 数据库中用作存储过程前缀。

    为了本菜谱的目的,我只为存储过程保留了简单的名称。

  7. 为此存储过程定义一个参数。通过这样做,你是在告诉数据库,当调用此存储过程时,它将通过一个存储在参数调用 @userID 中的整数值传递:

    @userID INT
    
  8. 你现在定义此存储过程要使用的 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 节点拖放到查询编辑器中。这将自动将所有列名插入到查询编辑器中。

  9. 当你完成将代码添加到存储过程后,它看起来会是这样:如何操作…

  10. 要创建存储过程,你需要单击 Execute 按钮。确保在单击 Execute 按钮时已选择正确的数据库:如何操作…

  11. 存储过程将在 SQL Server 的 Stored Procedures 节点下创建:如何操作…

  12. 我们现在已经完成了这个任务的一半。现在是时候构建我们将在应用程序中用于查询数据库的代码了。我们将直接将此代码添加到控制台应用程序的 Program.cs 文件中。虽然这段代码不被认为是最佳实践(硬编码服务器凭据),但它仅仅是为了说明从 C# 调用参数化存储过程的原理。

  13. 首先,将以下 using 语句添加到控制台应用程序的顶部:

    using System.Data.SqlClient;
    
  14. 然后,我们添加变量来包含我们需要登录服务器的凭据:

    int intUserID = 1;
    int cmdTimeout = 15;
    string server = "DIRK";
    string db = "CookbookDB";
    string uid = "dirk";
    string password = "uR^GP2ABG19@!R";
    
  15. 我们现在使用 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};";
    6SqlCredential cred = new SqlCredential(uid, secpw);
    

    注意

    更多关于 SecureString 的信息,请参阅本章的 在代码中使用 SecureString 菜单。

  16. 现在,我们在 using 语句内部创建一个 SqlConnection 对象。这确保了当 using 语句超出作用域时,SQL 连接将被关闭:

    using (SqlConnection conn = new SqlConnection(dbConn, cred))
    {                
        try
        {
    
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
    Console.ReadLine();
    
  17. try块中,添加以下代码以打开连接字符串并创建一个SqlCommand对象,该对象接受打开的连接和存储过程的名称作为参数。你可以使用创建实际 SQL 参数的快捷方法将其传递给存储过程:

    cmd.Parameters.Add("userID", SqlDbType.Int).Value = intUserID;
    

    因为我只是向存储过程传递一个整型参数,所以我没有为这个参数定义长度:

    如何操作…

    然而,如果你需要定义一个类型为VarChar(MAX)的参数,你需要通过添加-1来定义参数类型的大小。例如,如果你需要将学生的论文存储到数据库中,那么对于VarChar(MAX)的代码将如下所示:

    cmd.Parameters.Add("essay", SqlDbType.VarChar, -1).Value = essayValue;
    
  18. 在我们将参数及其值添加到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"]);
    
  19. 在你将所有代码添加到控制台应用程序之后,正确的完整代码将如下所示:

    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();
    
  20. 运行你的控制台应用程序,你将看到显示名称输出到屏幕上:如何操作…

它是如何工作的…

通过创建参数化 SQL 查询,编译器在运行 SQL 语句之前正确地替换了参数。它将防止恶意数据更改你的 SQL 语句以实现恶意结果。这是因为SqlCommand对象不会直接将参数值插入到语句中。

总结一下,使用参数化存储过程意味着不再有“小鲍比·表格”的问题。

使用诊断工具和历史调试

这个可靠的旧虫子已经成为了软件开发者和工程师们超过 140 年的噩梦。是的,你没有看错。实际上,是托马斯·爱迪生在 19 世纪 70 年代末提出了“bug”这个术语。这个词出现在他许多笔记本条目中,例如,他描述了白炽灯泡仍然有许多“bug”未解决。

他调试发明的努力是非常传奇的。考虑一下,一个已经六十多岁的男人需要工作 112 小时的工作周,这需要多大的真金不怕火炼和决心。他和他的七人团队(有一个常见的误解,认为只有六个人,因为第七个人没有出现在团队照片中)在 5 周的艰苦工作中几乎没睡过觉,因此被称为“失眠小队”。

现在,由于技术的进步,软件开发者可以拥有大量的调试工具(在 Visual Studio 内部和外部)可供使用。那么调试真的重要吗?当然很重要。它是我们作为软件开发者所做的一部分。如果我们不调试,那么这里有一些例子:

  • 在 2004 年,英国电子数据系统公司EDS)的儿童抚养金系统向近 200 万人多付了抚养金,向近 100 万人少付了抚养金,导致数十亿美元的抚养金未能收回。EDS 与它依赖的另一个系统之间的不兼容性导致纳税人损失了钱财,并负面影响了许多单身父母的生活。

  • 2012 年苹果地图的首次发布。无需多言。尽管对许多人来说令人困惑,但我仍然在陌生的城市或地区使用谷歌地图进行路线导航。

  • Therac-25 放射治疗机使用电子束靶向患者的肿瘤。不幸的是,软件中的竞争条件导致机器向几位患者提供了致命的过量辐射。

在互联网上可以找到许多影响数百万人的软件缺陷的例子。我们不仅仅是在谈论普通的缺陷。有时我们面临的是看似无法克服的问题。知道如何使用一些可用工具的安慰,是稳定应用程序和完全无法使用应用程序之间的区别。

准备中

到我写这篇文章的时候,IntelliTrace 仅在 Visual Studio 2015 Enterprise 中可用。然而,IntelliTrace 并不是 Visual Studio 的新特性。自 Visual Studio 2010 以来,它已经随着时间的推移发展成我们今天所拥有的功能。

如何操作…

  1. 首先,转到工具 | 选项如何操作…

  2. 展开 IntelliTrace 节点并点击常规。确保启用 IntelliTrace被勾选。同时,确保IntelliTrace 事件和调用信息选项被选中。点击确定如何操作…

  3. Recipes.cs文件中,你可能需要添加以下using语句:

    using System.Diagnostics;
    using System.Reflection;
    using System.IO;
    
  4. 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)
    {
    
    }
    
  5. 在确定完整路径后,将以下代码添加到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);
    }
    
  6. 当你添加了所有代码后,你的代码应该看起来像这样:

    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)
    {
    
    }
    
  7. Program.cs文件中,调用ErrorInception()方法。紧接着,执行Console.ReadLine(),以便我们的控制台应用程序在此处暂停。不要在你的代码中添加任何断点:

    ErrorInception();
    Console.ReadLine();
    
  8. 开始调试你的应用程序。异常被抛出,应用程序继续运行,这是在更复杂的应用程序中经常遇到的情况。在这个时候,你可能会期望日志文件附加了应用程序的虚构数据,但什么也没有发生。正是在这个时候,你停止应用程序,并在你的代码中到处添加断点,进行一种试错练习。我说试错,因为你可能不知道错误的确切位置。如果你的代码文件包含几千行代码,这种情况尤其如此。

    好吧,现在,有了 IntelliTrace 和历史调试,你只需要点击Break All按钮:

    如何操作…

  9. 你的应用程序现在基本上是暂停的。如果你看不到诊断工具窗口,请转到调试并点击显示诊断工具(或Ctrl + Alt + F2):如何操作…

  10. Visual Studio 现在显示诊断工具窗口。立即你可以看到在事件部分由红色菱形图标指示的问题。在底部的事件选项卡中,你可以点击异常:如何操作…

  11. 做这件事可以扩展异常详细信息,你可以看到日志文件未找到。然而,Visual Studio 在历史调试方面更进一步:如何操作…

  12. 你会在异常详细信息底部看到一个链接,上面写着激活历史调试。点击此链接。这允许你在代码编辑器中看到导致此异常的实际代码行。它还允许你在局部变量窗口、调用堆栈和其他窗口中查看应用程序的状态历史。现在你可以在代码编辑器中看到导致异常的具体代码行。在局部变量窗口中,你还可以看到应用程序用来查找日志文件的路径。这种调试体验非常强大,允许开发者直接找到错误的源头。这导致生产力的提高和更好的代码:如何操作…

它是如何工作的…

那么,这里的关键点是什么?如果你只能记住一件事,那就记住这一点。一旦你的系统用户因为 bug 而失去对该系统能力和潜力的信心,这种信心几乎无法恢复。即使你从 bug 和其他问题中恢复你的系统,重新启动它,并生产出一个无瑕疵的产品,用户也不会轻易被说服。这是因为在他们心中,系统是有 bug 的。

我曾经接管了一个由即将离职的高级开发者部分开发完成的系统。她有一个优秀的规范和一个展示给客户的良好展示的原型。唯一的问题是她在系统第一阶段实施后不久就离开了公司。当 bug 开始出现时,客户自然要求她提供帮助。

告诉客户,负责与客户建立关系的开发者(一直负责构建这种关系)已经离开公司,这对建立信心并没有好处。在这个特定项目中,只有一个开发者参与本身就是第一个错误。

其次,第二阶段将由我亲自开发,我也是唯一被分配给这个客户的开发者。这必须在构建在存在问题的第一阶段之上时完成。所以我一边修复错误,一边为系统开发新功能。幸运的是,这次我有一个叫罗里·谢尔顿的出色项目经理作为我的助手。我们一起跳进了深水区,罗里在管理客户的期望方面做得非常出色,同时对我们面临挑战的透明度也做得很好。

不幸的是,用户已经对提供的系统感到失望,并且不相信软件。这种信任从未完全恢复。如果我们早在 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();
    
  2. 接下来,在循环内的 Console.WriteLine(num) 代码行上放置一个断点:如何做…

  3. 右键单击断点并从上下文菜单中选择 条件…如何做…

  4. 现在,你会看到 Visual Studio 打开一个 断点设置 窗口。在这里,我们指定只有当 num 的值为 9 时,断点才需要被触发。你可以添加多个条件并指定不同的条件。条件逻辑非常灵活:如何做…

  5. 调试你的控制台应用程序。你会看到当断点被触发时,num 的值是 9如何做…

它是如何工作的…

条件在每次循环时都会被评估。当条件为真时,断点将被触发。在本食谱中展示的示例中,条件断点的真正好处有些被忽略了,因为它是一个非常小的列表。但考虑一下。您正在绑定一个数据网格。网格中的项目根据项目状态被赋予特定的图标。由于这是一个分层网格,您的网格包含数百个项目。您识别出绑定到网格的项目的主 ID。然后,将此主 ID 传递给其他代码逻辑以确定状态,该状态决定了显示的图标。

通过数百次循环来调试并按下 F10 键在任何情况下都是没有效率的。使用条件断点,您可以为主 ID 指定一个值,并且只有当循环达到该值时才会中断。然后您可以直接跳转到显示不正确的项目。

使用 PerfTips 识别代码中的瓶颈

PerfTips 一定是 Visual Studio 2015 中我最喜欢的功能之一。解释它们的作用并不能真正公正地对待它们。您必须看到它们在实际操作中的效果。

准备工作

不要将 PerfTips 与 CodeLens 混淆。PerfTips 是 Visual Studio 中与 CodeLens 不同的独立选项。

如何操作...

  1. PerfTips 默认启用。但以防您没有看到任何 PerfTips,请转到 工具 | 选项,并展开 调试 节点。在 常规 下,设置页面底部,您将看到一个名为 在调试时显示已过时间 PerfTip 的选项。确保此选项被选中:如何操作…

  2. 我们将创建一些简单的模拟长时间运行任务的方法。为此,我们只需让线程休眠几秒钟。在 Recipes.cs 文件中,添加以下代码:

    public static void RunFastTask()
    {
        RunLongerTask();
    }
    
    private static void RunLongerTask()
    {
        Thread.Sleep(3000);
        BottleNeck();
    }
    
    private static void BottleNeck()
    {
        Thread.Sleep(8000);
    }
    
  3. 在您的控制台应用程序中,调用静态方法 RunFastTask() 并在此行代码上放置断点:

    RunFastTask();
    Thread.Sleep(1000);
    
  4. 开始调试您的控制台应用程序。您的断点将停在 RunFastTask() 方法上。按 F10 跳过此方法:如何操作…

  5. 您会注意到 11 秒后,下一行将被突出显示,并显示 PerfTip。PerfTip 显示了上一行代码执行所需的时间。因此,现在位于 Thread.Sleep 上的调试器显示 RunFastTask() 方法花费了 11 秒来完成。显然,这个任务并不快:如何操作…

  6. 步入 RunFastTask() 方法,您可以放置更多的断点并逐个跳过它们以找到导致最长延迟的方法。正如您所看到的,PerfTips 允许开发者快速轻松地识别代码中的瓶颈:如何操作…

它是如何工作的…

市面上有许多工具可以进行这项操作以及更多,让开发者能够查看各种代码指标。然而,PerfTips 允许你在按照常规调试任务逐步执行代码时即时查看问题。在我看来,它是一个不可或缺的调试工具。

第十三章。在 Azure 中创建 Web 应用程序

本章将向您介绍 Azure。如果您以前从未使用过 Azure,界面可能看起来有些令人畏惧。然而,Azure 实际上并不那么复杂,并且对任何开发者来说都是巨大的好处。在本章中,我们将探讨以下内容:

  • 在 Azure 中创建用于测试的数据库

  • 在 Azure 上创建 Web 应用程序并进行托管

  • 在 Azure 上使用虚拟机

简介

Azure 允许开发者提高生产力。这是一系列开发者可以用来构建应用程序、保护数据和为您的应用程序提供高可用性的云服务,无论您正在构建什么。移动应用程序、企业应用程序、Web、物联网IoT)……所有这些都欢迎在 Azure 上。

您可以使用 .NET、JavaScript、PHP、Node.js、Python 创建应用程序,甚至运行 Linux 容器。Azure 还允许您只为您实际使用的部分付费,使您能够随着需求的增长轻松扩展。

在 Azure 中创建用于测试的数据库

在 Azure 中创建数据库实际上是一个非常直接的过程。为了使过程更加流畅,已经投入了大量工作,使其具有一个过于熟悉的向导式界面。

准备工作

要开始使用 Azure,您需要一个 Azure 账户。您可以创建一个免费试用账户。有关 Azure 定价的更多信息,请查看以下 URL:azure.microsoft.com/en-us/pricing/

如何操作…

  1. 登录到您的 Azure 账户后,您将被带到您的仪表板。从这里您可以查看您可能已固定的任何项目。在左侧,您将看到菜单。我们只想创建一个 SQL 数据库,因此点击SQL 数据库菜单项:如何操作…

  2. 如果这是您第一次使用 Azure,您在默认目录中将没有任何可用的数据库。点击添加以创建一个新的数据库:如何操作…

  3. 现在,您将看到一个表单,您可以使用它来指定数据库详细信息。如您所见,您可能没有选择服务器。这可能是因为您可能还没有服务器。点击服务器配置所需设置如何操作…

  4. 您将有机会创建一个新的服务器。这也是您创建服务器管理员登录的地方:如何操作…

  5. 当您创建了自己的服务器后,您将被带回到数据库设置屏幕。您创建的服务器现在已被选中:如何操作…

  6. 当您点击创建按钮时,Azure 将开始将您的数据库部署到您创建的服务器上:如何操作…

  7. 此过程可能需要几分钟,所以在它完成时请耐心等待。当数据库部署完成后,你将在 Azure 门户顶部的通知标签页中看到一个通知:如何操作…

  8. 你可能需要刷新你的 SQL 数据库屏幕以查看新部署的数据库。为了完成下一步,点击数据库列表中创建的数据库:如何操作…

  9. 创建的数据库的属性和设置随后将显示出来。在这里,你可以看到你选择的资源组,以及服务器名称和其他属性。特别值得注意的是连接字符串属性:如何操作…

  10. 点击连接字符串将显示你创建的数据库的不同提供者的连接字符串。记下ADO.NET(SQL 身份验证)字符串:如何操作…

  11. 返回到上一个屏幕,点击服务器名称属性。在这里,你会找到防火墙设置。你需要向防火墙添加一个规则,以允许你的计算机连接到此数据库:如何操作…

  12. 点击显示防火墙设置,你会看到你可以添加客户端 IP。默认情况下,你访问防火墙设置时显示的机器 IP 地址将在客户端 IP 地址字段中。点击添加客户端 IP以将你当前机器的 IP 地址添加到防火墙中允许通过。你可以将规则名称改为更友好的名称:如何操作…

    注意

    你也可以在此处定义不同的 IP 地址,以便同事可以从他们的机器访问此数据库。

  13. 在你已经添加了防火墙规则后,在你的本地开发机器上打开 SQL Server Management Studio。在对象资源管理器中,点击连接对象资源管理器按钮:如何操作…

  14. 看一下你之前记下的连接字符串。在连接字符串的服务器部分之后,我将服务器名称复制为 tcp:srvcookbook.database.windows.net。将其粘贴到连接到服务器屏幕中的服务器名称字段。最后,输入你在 Azure 上创建服务器时定义的登录名密码。点击连接按钮:如何操作…

  15. SQL Server Management Studio 将现在连接到 Azure 上的 CookbookDb 数据库:如何操作…

它是如何工作的…

Azure 是存储数据库的完美之地。它安全,并且只有你选择的开发者可以访问。你需要意识到,如果你的 IP 地址发生变化,你可能需要重新配置 Azure 数据库上的防火墙规则。然而,这对安全性来说是个好兆头,因为你可以确信你的数据是安全的。有关 Azure 中数据库的更多信息,请参阅以下文档:azure.microsoft.com/en-us/documentation/services/sql-database/

在 Azure 上创建网络应用并进行托管

网络开发者经常要做的事情之一是为(用户验收测试UAT或开发者测试目的部署网络应用。Azure 通过在 Visual Studio 内提供无缝的发布体验,使这个过程对你来说非常方便。要将网络应用或网站发布到 Azure,你首先需要在 Azure 上创建一个网络应用来发布你的网站。

准备工作

要开始使用 Azure,你需要有一个 Azure 账户。你可以创建一个免费试用账户。有关 Azure 定价的更多信息,请查看以下 URL:azure.microsoft.com/en-us/pricing/

如何操作…

  1. 登录到你的 Azure 账户后,你将被带到你的仪表板。从这里你可以看到你可能已经固定的任何项目。在左侧,你会看到一个菜单。我们想要创建一个网站,所以点击应用服务菜单项:如何操作…

  2. 如果你第一次使用 Azure,你可能不会显示任何应用服务。点击添加来创建一个新的:如何操作…

  3. 给应用服务起一个名字,并选择或创建一个资源组如何操作…

  4. 当你点击创建按钮时,Azure 开始部署。这个过程可能需要几分钟才能完成,所以你可以趁等待的时候去拿一杯咖啡:如何操作…

  5. 部署完成后,你将在通知菜单中收到通知:如何操作…

  6. 刷新应用服务部分,你会看到我们刚刚创建的cookbookdemo网络应用。你可能需要点击刷新按钮,cookbookdemo网络应用才会变得可见:如何操作…

  7. 点击cookbookdemo网络应用将显示属性。特别注意右上角的URL如何操作…

  8. 如果你点击这个 URL,你将被带到你的网络应用的默认占位符网站。是时候将我们的网站发布到 Azure 了:如何操作…

  9. 启动 Visual Studio 并创建或打开你想要发布的网站。我已经创建了一个简单的网站,想要发布:如何操作…

  10. 右键单击你的网站项目,然后从上下文菜单中选择发布 Web 应用程序如何做…

  11. 发布 Web屏幕将显示。在这里,你可以看到可用的各种发布目标。我们只想将我们的应用程序发布到 Azure。选择Microsoft Azure Web Apps作为发布目标:如何做…

  12. 点击下一步将要求你连接到 Azure,或者如果你之前已经连接,则从下拉选择中选择你的订阅。任何现有的 Web 应用程序都将显示在现有 Web 应用程序列表中。我们之前创建的cookbookdemo Web 应用程序将显示:如何做…

  13. 选择cookbookdemo Web 应用程序,你现在必须定义你的 Web 应用程序连接和发布方法。目前,我们将仅选择Web Deploy作为将我们的 Web 应用程序发布到 Azure 的方法:如何做…

  14. 你还可以点击验证连接按钮,确保一切就绪后再继续:如何做…

  15. 点击下一步将带你进入设置屏幕。在这里,你可以选择要部署的配置,以及定义数据库连接:如何做…

    注意

    正如你所注意到的,我正在使用的数据库连接是我们在本章第一个菜谱中创建的数据库的连接,在 Azure 中创建数据库进行测试

  16. 当你点击下一步时,你会看到你可以预览要发布到 Azure Web 应用程序上的文件。初始发布显然包含最多的文件,因为 Web 应用程序上还没有任何内容:如何做…

  17. 当你准备好时,点击发布按钮将开始将你的网站发布到 Azure 的过程。这可能需要几分钟才能完成。发布完成后,Visual Studio 将自动打开已发布的网站:如何做…

  18. 回到 Azure,你会看到监控窗口活跃起来。你还可以在此处添加其他部分,以提供更多关于你的 Web 应用程序状态的信息:如何做…

它是如何工作的…

当你在 Azure 上部署时,Azure 将大量权力交到开发者手中。你可以添加额外的部分来帮助管理你的 Web 应用程序。这些部分以瓷砖的形式显示给你。一些可用的部分包括:

  • 进程资源管理器

  • Web 作业

  • 流量路由

  • 请求和错误

  • Http 4xx

  • Http 5xx

  • 文件系统存储

  • Web 测试

  • 用户和角色

  • 性能测试

  • 即使是你的预估 Azure 支出

Azure 确实是一个非常灵活的云解决方案,无论你的个人需求如何。

在 Azure 上使用虚拟机

虚拟机VM)对于开发者来说是必不可少的。我自己有几个用于个人使用和尝试新软件。其背后的技术相当令人难以置信,因为它为您虚拟化了一个特定的环境,以便您可以准确测试您的应用程序。在我之前的工作单位,我们会创建特定客户端环境的 VM,以便测试应用程序部署,基本上是看看应用程序是否正确运行。

使用 Azure,开发者在决定特定的 VM 平台时有很多选择。您几乎可以启动任何类型的 VM。如果您想尝试新的 Visual Studio,您可以。Windows 的新版本?您可以在 Azure 上找到相应的 VM。想稍微玩一下 Linux 和 WordPress?没问题。Azure 可以做到这一切,并且设置起来非常简单。

考虑一下替代方案。如果不存在 VM,您想在 Windows 10 这样的操作系统上测试应用程序,您可能需要留出一台特定的机器来测试不同的操作系统版本。这样,您就失去了一台机器,不能再用于其他工作了。然后您需要花上一两个小时设置这台 PC,安装正确的操作系统。然后您需要设置这台 PC,使其在您的办公室的本地网络中可用。如果您想从远程位置(例如家中)访问这台测试机器怎么办?您需要配置它以便远程访问,同时保持安全性。因此,您设置了 VPN,以便在开发者需要加班工作时远程访问这台机器。这必须为正在开发中的应用程序的单个实例完成。如果您有可用的服务器,当然,这可能不是问题。然而,如果您是一家资源有限的小到中型公司,您可能会很快重新安装这台机器,为不同的客户端设置进行测试。

这就是 Azure 在创造差异方面表现得非常出色的地方。设置过程只需几分钟,与在办公室设置一台 PC 相比,这只是瞬间的事情。远程访问、安全性、事件监控、警报以及许多其他功能立即对您团队中的所有开发者可用。

准备工作

要开始使用 Azure,您需要一个 Azure 账户。您可以创建一个免费试用账户。有关 Azure 定价的更多信息,请查看以下网址:azure.microsoft.com/en-us/pricing/.

如何操作...

  1. 登录您的 Azure 账户后,您将被带到您的仪表板。从这里您可以查看您可能已固定的任何项目。在左侧您将看到菜单。点击虚拟机菜单项:如何操作…

  2. 然后,您将被带到 Azure 订阅中虚拟机的默认目录。我们需要通过点击添加按钮来创建一个新的虚拟机:如何操作…

  3. Azure 会显示所有可供您使用的虚拟机类型。可供选择的主机类型相当多。从服务网格到 Linux,再到 Ubuntu,选择范围很广:如何操作…

  4. 对于我们的目的,我们只需创建一个 Windows 虚拟机:如何操作…

  5. 点击Windows组,您可以看到我们有两个选择:Windows 10 企业版或 Windows 8.1 企业版 64 位虚拟机。我们将简单地选择 Windows 10 虚拟机以开始操作:如何操作…

  6. 您现在将被要求输入各种设置以配置您的 Windows 10 虚拟机。为您的虚拟机命名并定义登录用户名和密码。资源组将允许您选择一个现有的资源组或创建一个新的资源组:如何操作…

  7. 在下一个配置屏幕上,您将看到推荐的虚拟机大小,每个大小都有其不同的定价选项以您所在地区的货币显示。选择最适合您、您的需求或/和预算的选项。

  8. 下一屏允许您配置可选功能。您可用的功能如下:

    • 存储账户是创建虚拟机磁盘的地方。

    • 虚拟网络与传统网络类似。同一虚拟网络中的任何虚拟机都可以相互访问,并且默认情况下已配置。

    • 子网允许您将虚拟机与其他虚拟机或互联网隔离。

    • 公共 IP 地址允许您与之前定义的虚拟网络之外的机器进行通信。

    • 网络安全组是在您的防火墙上定义的一组规则,用于控制谁可以访问您的 Windows 10 虚拟机。

    • 扩展相当不错。这些是额外的附加组件,例如防病毒软件包。

    • 监控默认开启,允许您收集有关虚拟机的信息,并根据监控信息定义警报。这使您始终处于控制状态并保持信息畅通。

    • 诊断存储账户是监控指标存储的地方。如果需要,您可以使用自己的工具分析这些指标。

    • 可用性设置允许您将两个或更多虚拟机集群在一起,以便在其中一个虚拟机需要离线进行维护时提供故障转移。此步骤需要配置,因为创建虚拟机后无法更改可用性设置:

    如何操作…

  9. 一旦您配置了虚拟机,您将看到在设置过程中选择的配置选项的摘要:如何操作…

  10. 当您确认虚拟机已正确设置后,Azure 将开始部署过程:如何操作…

  11. 部署过程的进度也可见于仪表板,并需要几分钟才能完成:如何操作…

  12. 几分钟后,虚拟机将可用并准备就绪。您将被带到cookbookvm的虚拟机页面:如何操作…

  13. 要连接到您的虚拟机,请点击连接按钮:如何操作…

  14. 然后将下载一个.rdp文件。您可以直接点击该文件以启动远程桌面连接会话:如何操作…

  15. 你可能会被问到是否信任远程连接的出版商。显然你是信任的,所以只需点击连接按钮:如何操作…

  16. 现在,您将能够输入之前在虚拟机设置期间定义的登录凭据:如何操作…

  17. 现在,您将通过远程桌面连接连接到 Windows 10 虚拟机:如何操作…

  18. 在 Azure 中,如果您在虚拟机设置期间选择了监控选项,您会看到有一个默认的监控选项卡用于CPU 百分比。Azure 允许您添加更多监控事件并为每个事件创建警报:如何操作…

它是如何工作的…

虚拟机可以通过远程连接访问。您可以使用传统的远程桌面连接,或者如果您需要访问多个远程机器,可以使用更复杂的工具如 mRemoteNG。Azure 非常适合开发测试的原因在于,资源根本不需要存在于您的本地网络中。这意味着维护开发虚拟机备份的开销在很大程度上被避免了。

我记得在我之前雇主那里工作的网络管理员经常询问我们还在使用哪些虚拟机。其中一些虚拟机可能一年只用上一两次,所以不能删除。有多个开发者访问这些虚拟机也意味着这些虚拟机上有很多垃圾文件,这使得备份这些虚拟机变得痛苦不堪(通常在周末进行)。

Azure 为 IT 专业人员解决了许多问题和难题。在本章中,我们只探讨了您作为开发者可用的三种解决方案。不要被误导,Azure 的功能远不止为开发者提供一个强大的测试平台。详细探讨 Azure 可能需要一本单独的书。

posted @ 2025-10-22 10:34  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报