C--七天学习手册-全-

C# 七天学习手册(全)

原文:zh.annas-archive.org/md5/2057FAEAB3B9AE161438DDC8A687CA7E

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

学习一门新语言或转换到完全不同的技术是行业的常见需求。作为学生,一个人应该使自己跟上市场趋势,作为专业人士,一个人应该了解新技术带来的新事物。为了满足这一需求,有很多书籍长达数千页,旨在成为 C#编程语言的全面参考。

这本书完全不同,是为了让那些对 C#语言有基本了解的人,或者是专业人士,正在使用其他语言但想要转换的人学习 C#。这本书的设计目的是让人们从基础知识开始,逐步进阶到高级水平。书中包含简洁的内容和相关示例来解释一切。

这本书中有很多部分会鼓励你学习更多;有了这些知识,你可以给同事、雇主或同学留下深刻印象。你会第一次听到一些术语 - 没问题,你可以在这本书中了解它们。

在每个章节的结尾,你会找到一个动手练习部分,这将增强你的信心,并给你解决实际问题的想法。在这些练习中,你可以找到各种提示。

对于代码示例,你可以访问 GitHub 存储库(github.com/PacktPublishing/Learn-CSharp-in-7-days/)并下载所有章节的源代码。你可以按照那里提到的说明,在 Visual Studio 2017 Update 3 中轻松使用这些代码示例。

这本书涵盖了什么

第一章,第 01 天 - .NET 框架概述,让你熟悉 C#,包括.NET 框架和.NET Core。

第二章,第 02 天 - 开始学习 C#,通过迭代类型系统和各种构造,让你对 C#有一个基本的了解。使用和重要性的保留关键字,理解语句,类型转换。

第三章,第 03 天 - C#的新特性,让你熟悉 7.0 和 7.1 版本中引入的各种新重要特性。

第四章,第 04 天 - 讨论 C#类成员,解释了类和其成员的基础知识,包括索引器、文件系统、异常处理和使用正则表达式进行字符串操作。

第五章,第 05 天 - 反射和集合概述,涵盖了使用反射处理代码,以及集合、委托和事件的介绍。

第六章,第 06 天 - 深入了解高级概念,教你如何实现属性,使用预处理器,了解泛型及其用法,包括同步和异步编程。

第七章,第 07 天 - 了解面向对象编程

C#,在这一章中,我们将学习 OOP 的所有四个范式,并使用 C# 7.0 进行实现。

第八章,第 08 天 - 测试你的技能 - 构建一个真实的应用程序,帮助你根据本书学到的知识编写一个完整的应用程序。

你需要为这本书做什么

本书中的所有支持代码示例都在.NET Core 2.0 上使用 Visual Studio 2017 更新 3 进行了测试,在 Windows 平台上使用 SQL Server 2008R2 或更高版本的数据库。

这本书适合谁

7 天学会 C#是一本快节奏的指南。在这本书中,我们采用了一种独特的方法来教授 C#给绝对的初学者,他们将能够在七天内学会语言的基础知识。这本实用的书介绍了引入 C#编程语言基础的重要概念。这本书解决了大多数初学者面临的挑战和问题。它涵盖了需要学习 C#、使用 C#设置开发环境的问题,以及数学运算等日常问题。它快节奏的写作风格使读者能够迅速上手。我们从第一章的绝对基础开始(变量、语法、控制流等),然后转向语句、数组、字符串处理、方法、继承、I/O 处理等概念。每一章后面都有一个重点在用语言构建东西的练习。这本书是一个快节奏的指南,让读者迅速掌握语言。它作为一个参考指南,描述了 C#的主要特性。读者将能够使用真实场景构建简单的代码。通过这本书,你将能够将你的技能提升到一个新的水平,对 C#的基础知识有很好的了解。

约定

在这本书中,你会发现一些区分不同信息类型的文本样式。以下是一些这些样式的例子和它们含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“你将在Program.cs类中得到以下代码。这是 Visual Studio 提供的默认代码;你可以根据需要进行修改。”代码块设置如下:

var class1 = newClassExample(); 
var class2 = new Day02New.ClassExample(); 
    class1.Display(); 
    class2.Display(); 

新术语重要单词以粗体显示。例如,屏幕上看到的单词,比如菜单或对话框中的单词,会以这种方式出现在文本中:“从工作负载中选择要安装的选项。对于我们的书,我们需要.NET 桌面开发和.NET Core。”

警告或重要说明会以这种方式出现。

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

第一章:第 1 天 - .NET 框架概述

这是我们学习 C#的七天之旅的第一天。今天,我们将开始介绍一个新的编程世界,并讨论学习这种编程语言所需的所有基本概念。我们还将讨论.NET 框架和.NET Core 框架,涵盖框架的重要概念。我们还将对托管代码和非托管代码有一个基本的了解。在一天结束时,我们将开始一个简单的 Hello World 程序。

今天,我们将学习以下主题:

  • 什么是编程?

  • 什么是.NET Core?

  • .NET 标准是什么?

什么是编程?

可能有各种定义或各种思考来定义单词编程。在我看来,编程是以一种机器(计算机)能够理解并描绘解决方案的方式编写解决方案,这是你可以手动识别的

例如,假设你有一个问题陈述:找出这本书中元音字母的总数。如果你想找到这个陈述的解决方案,你会怎么做?

解决这个问题的可能步骤如下:

  1. 首先,找到正确的书。我假设你知道元音字母(aeiou)。

  2. 你在书中找到了多少个元音字母?--0(零)。

  3. 打开当前页(最初,我们的当前页是 1),开始阅读以找到元音字母。

  4. 如果字母匹配aeiou(请注意大小写不重要,所以字母也可以是AEIOU),则增加一个元音计数。

  5. 当前页完成了吗?

  6. 如果第 5 步的答案是肯定的,那么检查这是否是书的最后一页:

  • 如果是的话,那么我们手头就有了总的元音计数,也就是n,其中n是当前章节中找到的元音字母的总数。移动到第 8 步获取结果。

  • 如果这不是最后一章,那么通过将当前章节编号加 1 来移动到下一章。所以,我们应该移动到 1 + 1 = 2(第二章)。

  1. 在下一章中,重复第 4 到 6 步,直到到达书的最后一章。

  2. 最后,我们有了总的元音计数,也就是nn是找到的元音字母的总数)。

前面的步骤只是描述了我们如何找到问题陈述的完美解决方案。这些步骤展示了我们如何手动找到了书中所有章节中元音字母的总数的答案。

在编程世界中,这些步骤统称为算法

算法只不过是通过定义一组规则来解决问题的过程。

当我们以一种机器/计算机能够遵循指令的方式编写前面的步骤/算法时,这就是编程。这些指令应该用机器/计算机能够理解的语言编写,这就是所谓的编程语言。

在本书中,我们将使用 C# 7.0 作为编程语言,.NET Core 作为框架。

什么是.NET?

当我们提到.NET(发音为点网),它是.NET 完整版,因为我们已经准备好了.NET Core,并且我们在书中的示例中使用了 C# 7.0 作为语言。在继续之前,你应该了解.NET,因为.NET Core 有一个.NET 标准,它是.NET 框架和.NET Core 的 API 服务器。因此,如果你使用.NET 标准创建了一个项目,它对.NET 框架和.NET Core 都是有效的。

.NET 只不过是一种语言、运行时和库的组合,我们可以使用它来开发托管软件/应用程序。在.NET 中编写的软件是托管的,或者处于托管环境中。要理解托管,我们需要深入了解操作系统如何获得二进制可执行文件。这包括三个更广泛的步骤:

  1. 编写代码(源代码)。

  2. 编译器编译源代码。

  3. 操作系统立即执行二进制可执行文件:

更广泛的步骤——二进制可执行文件是如何获得的?

上述过程是一个标准过程,描述了编译器如何编译源代码并创建可执行二进制文件,但在.NET 的情况下,编译器(我们代码的 C#编译器)并不直接提供二进制可执行文件;它提供一个程序集,这个程序集包含元数据和中间语言代码,也称为Microsoft 中间语言MSIL)或中间语言IL)。这个 MSIL 是一种高级语言,机器无法直接理解,因为 MSIL 不是特定于机器的代码或字节码。为了正确执行,它应该被解释。这种从 MSIL 或 IL 到机器语言的解释是通过 JIT 的帮助发生的。换句话说,JIT 将 MSIL、IL 编译成机器语言,也称为本机代码。更多信息,请参阅msdn.microsoft.com/en-us/library/ht8ecch6(v=vs.90).aspx

对于 64 位编译,Microsoft 已经宣布了 RyuJIT(blogs.msdn.microsoft.com/dotnet/2014/02/27/ryujit-ctp2-getting-ready-for-prime-time/)。在即将推出的版本中,32 位编译也将由 RyuJIT 处理(github.com/dotnet/announcements/issues/10)。之后,我们现在可以为 CoreCLR 拥有一个单一的代码库。

中间语言是一种高级的基于组件的汇编语言。

在我们的七天学习中,我们不会专注于框架,而是更加专注于使用.NET Core 的 C#语言。在接下来的章节中,我们将讨论.NET Core 的重要内容,以便在我们使用 C#程序时,了解我们的程序如何与操作系统交互。

什么是.NET Core?

.NET Core 是微软推出的新的通用开发环境,以满足跨平台的需求。.NET Core 支持 Windows、Linux 和 OSX。

.NET Core 是一个开源软件开发框架,采用 MIT 许可证发布,并由 Microsoft 和.NET 社区在 GitHub(github.com/dotnet/core)存储库中进行维护。

.NET Core 特性

以下是.NET Core 的一些重要特性,使.NET Core 成为软件开发中的重要进化步骤:

是什么构成了.NET Core?

.NET Core 是coreclrcorefxcli 和 roslyn的组合。这些是.NET Core 组成的主要组件。

  • Coreclr:这是一个.NET 运行时,提供程序集加载、垃圾回收器等。您可以在 coreclr 上查看更多信息github.com/dotnet/coreclr

  • Corefx:这是一个框架库;您可以在 corefx 上查看更多信息github.com/dotnet/corefx

  • Cli: 这只是一个命令行界面工具,roslyn 是语言编译器(在我们的案例中是 C#语言)。请参阅 cli(github.com/dotnet/cli)和 Roslyn 以获取更多信息,网址为github.com/dotnet/roslyn

什么是.NET 标准?

.NET Standard 是一组 API,解决了在尝试编写跨平台应用程序时代码共享的问题。目前,微软正在努力使.NET Standard 2.0 变得更加流畅,并且所有人都将实施这些标准,即.NET Framework、.NET Core 和 Xamarin。通过使用.NET Standard(一组 API),您确保您的程序和类库将适用于所有目标.NET Framework 和.NET Core。换句话说,.NET Standard 将取代可移植类库PCL)。有关更多信息,请参阅blogs.msdn.microsoft.com/dotnet/2016/09/26/introducing-net-standard/

.NET Standard 2.0 存储库位于github.com/dotnet/standard

到目前为止,您已经了解了.NET Core 和其他一些有助于构建跨平台应用程序的内容。在接下来的章节中,我们将准备环境,以便开始学习使用 Visual Studio 2017(最好是社区版)的 C#语言。

可用的 C# IDE 和编辑器

集成开发环境IDE)只是一种便利应用程序开发的软件。另一方面,编辑器基本上是用来添加/更新预定义或新内容的。当我们谈论 C#编辑器时,我们指的是一个帮助编写 C#程序的编辑器。一些编辑器带有许多附加组件或插件,并且可以编译或运行程序。

我们将使用 Visual Studio 2017 作为我们首选的 C# IDE;但是,您还可以选择其他一些 C# IDE 和编辑器:

  1. Visual Studio Code: VS Code 是一个编辑器,您可以从code.visualstudio.com/开始下载。要开始使用 VS Code,您需要从marketplace.visualstudio.com/items?itemName=ms-vscode.csharp安装 C#扩展。

  2. Cloud9: 这是一个基于 Web 浏览器的 IDE。您可以通过在c9.io/signup注册免费开始使用它。

  3. JetBrain Rider: 这是 JetBrains 的跨平台 IDE。有关更多信息,请访问www.jetbrains.com/rider/

  4. Zeus IDE: 这是一个专为 Windows 平台设计的 IDE。您可以从www.zeusedit.com/index.html开始使用 Zeus。

  5. 文本编辑器: 这是您可以在不安装任何软件的情况下使用的方式;只需使用您选择的文本编辑器。我使用 Notepad++(notepad-plus-plus.org/download/v7.3.3.html)和命令行界面CLI)来构建代码。请参阅docs.microsoft.com/en-us/dotnet/articles/core/tools/了解有关如何使用 CLI 开始的更多信息。

可能会有更多的替代 IDE 和编辑器,但对我们来说它们并不重要。

设置环境

在本节中,我们将逐步了解如何在 Windows 10 上启动安装 Visual Studio 2017(最好是社区版):

  1. 转到www.visualstudio.com/downloads/(您还可以从www.visualstudio.com/dev-essentials/获得 Dev Essentials 的好处)。

  2. 下载 Visual Studio Community (https😕/www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15):

  1. 开始 Visual Studio 设置。

  2. 从工作负载中选择你想要安装的选项。对于我们的书籍,我们需要.NET 桌面开发和.NET Core:

  1. 点击安装开始安装:

  1. 安装完成后点击启动。

  2. 使用你的 Live ID 注册 Visual Studio。

  3. 选择 Visual C#作为你的开发设置。

  4. 你会看到以下的起始页面:

我们已经准备好开始我们的第一步了。

实践练习

通过涵盖今天学习的概念来回答以下问题。

  • 什么是编程?写下一个算法,从书籍《7 天学会 C#》的所有页面中找出元音字母的数量。

  • 什么是.NET Core 和.NET Standard?

  • 是什么让.NET Core 成为一种进化的软件?

回顾第一天

今天,我们向你介绍了.NET Core 和.NET Standard 的一些重要概念。你学会了编程世界中的程序和算法是什么。

第二章:第二天 - 开始学习 C#

今天,我们是在七天学习系列的第二天。昨天,我们已经了解了.NET Core 及其重要方面的基本理解。今天,我们将讨论 C#编程语言。我们将从理解典型的 C#程序的基本概念开始,然后我们将开始涵盖保留关键字、类型和运算符等其他内容;在一天结束时,我们将能够在涵盖以下主题后编写完整的 C#程序:

  • 介绍 C#

  • 理解典型的 C#程序

  • C#保留关键字、类型和运算符概述

  • 类型转换概述

  • 理解语句

  • 数组和字符串操作

  • 结构与类

C#简介

简而言之,C#(发音为See-Sharp)是由微软开发的一种编程语言。C#已获得国际标准化组织ISO)和欧洲计算机制造商协会ECMA)的批准。

这是官方网站上的定义(docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/index):

C#是一种简单、现代、面向对象和类型安全的编程语言。C#源自 C 系列语言,对于 C、C++、Java 和 JavaScript 程序员来说会立即感到熟悉。

C#语言被设计为遵守公共语言基础设施CLI),这是我们在第一天讨论过的。

C#是最受欢迎的专业语言,原因如下:

  • 它是一种面向对象的语言

  • 它是面向组件的

  • 它是一种结构化语言

  • 使其最受欢迎的主要部分:这是.NET Framework 的一部分

  • 它具有统一的类型系统,这意味着 C#语言的所有类型都继承自单一类型对象(这也被称为母类型)

  • 它是构建坚固耐用的应用程序,如垃圾回收(在第一天讨论过)

  • 它有能力处理程序中的未知问题,这被称为异常处理(我们将在第四天讨论异常处理)

  • 强大的反射支持,可以实现动态编程(我们将在第四天讨论反射)

C#语言的历史

C#语言是由Anders Hejlsberg及其团队开发的。语言名称受到了音乐符号sharp#)的启发,该符号表示音符应该提高一个半音。

第一个发布的版本是 C# 1.0,于 2002 年 1 月推出,当前版本是 C# 7.0。

以下表格描述了 C#语言的所有版本。

C#版本 发布年份 描述
1.0 2002 年 1 月 使用 Visual Studio 2002 - .NET Framework 1.0
1.2 2003 年 4 月 使用 Visual Studio 2003 - .NET Framework 1.1
2.0 2005 年 11 月 使用 Visual Studio 2005 - .NET Framework 2.0
3.0 2007 年 11 月 Visual Studio 2008, Visual Studio 2010 - .NET Framework 3.0 和 3.5
4.0 2010 年 4 月 Visual Studio 2010 - .NET Framework 4
5.0 2012 年 8 月 Visual Studio 2012, 2013 - .NET Framework 4.5
6.0 2015 年 7 月 Visual Studio 2015 - .NET Framework 4.6
C# 7.0 2017 年 3 月 Visual Studio 2017 - .NET Framework 4.6.2
C# 7.1 2017 年 8 月 Visual Studio 2017 更新 3 - .NET Framework 4.7

在接下来的部分中,我们将详细讨论这种语言,包括代码示例。我们将讨论 C#语言的关键字、类型、运算符等。

理解典型的 C#程序

在我们开始用 C#编写程序之前,让我们先回到第一天,我们在那里讨论了各种有助于使用 C#语言编写程序/应用程序的集成开发环境和编辑器。回顾第一天,了解各种编辑器和 IDE,并检查为什么我们应该选择其中之一。我们将在本书的所有示例中使用 Visual Studio 2017 更新 3。

要了解安装 Visual Studio 2017 的步骤,请参阅docs.microsoft.com/en-us/visualstudio/install/install-visual-studio

要开始一个简单的 C#程序(我们将创建一个控制台应用程序),请按照以下步骤进行:

  1. 启动你的 Visual Studio。

  2. 转到文件 | 新建 | 项目(或ctrl +Shift + N)。

  3. 在 Visual C#节点下,选择.NET Core,然后选择控制台应用程序。

  4. 给你的程序取一个名字,比如Day02,然后点击确定(见下图中的突出显示的文本):

你将在Program.cs类中得到以下代码-这是 Visual Studio 提供的默认代码;你可以根据需要进行修改:

using System; 

namespace Day02 
{ 
class Program 
    { 
     static void Main(string[] args) 
        { 
         Console.WriteLine("Hello World!"); 
        } 
    } 
} 

通过在键盘上按下F5键,您将以 Debug 模式运行程序。

通常,每个程序都有两种不同的配置或模式,即 Debug 和 Release。在 Debug 模式下,将加载所有编译文件和符号,这些对于在应用程序执行过程中遇到的任何问题进行深入分析是有帮助的。另一方面,Release 是一种干净的运行,只有没有 Debug 符号的二进制文件加载并执行操作。有关更多信息,请参阅stackoverflow.com/questions/933739/what-is-the-difference-between-release-and-debug-modes-in-visual-studio

当程序运行时,您将看到以下输出:

在继续之前,让我们分析一下在 Visual Studio 上的控制台应用程序的下图:

上述图示了一个典型的 C#程序;我们使用的是 Visual Studio,但控制台程序在不同的 IDE 或编辑器中保持不变。让我们更详细地讨论一下。

1(System)

这是我们在程序/应用程序中定义要使用的命名空间的地方。通常,这被称为使用语句,其中包括外部、内部或任何其他命名空间的使用。

System 是一个包含许多基本类的典型命名空间。有关更多信息,请参阅docs.microsoft.com/en-us/dotnet/api/system?view=netcore-2.0

3(Day02)

这是我们现有控制台应用程序的命名空间。

命名空间是将一组名称与另一组名称分开的一种方式,这意味着您可以创建尽可能多的命名空间,而不同命名空间下的类将被视为不同的,尽管它们具有相同的名称;也就是说,如果在namespace Day02中声明了一个ClassExample类,它将与在Day02New namespace中声明的ClassExample类不同,并且将在没有任何冲突的情况下工作。

这是一个典型的示例,显示了同名的两个类,它们有两个不同的namespaces

namespace Day02 
{ 
public class ClassExample 
    { 
public void Display() 
        { 
Console.WriteLine("This is a class 'ClassExample' of namespace 'Day02'. "); 
        } 
    } 
} 

namespace Day02New 
{ 

public class ClassExample 
    { 
public void Display() 
        { 
Console.WriteLine("This is a class 'ClassExample' of namespace 'Day02New'. "); 
        } 
    } 
} 

上述代码将被调用如下:

private static void SameClassDifferentNamespacesExample() 
{ 
var class1 = new ClassExample(); 
var class2 = new Day02New.ClassExample(); 
    class1.Display(); 
    class2.Display(); 
} 

这将返回以下输出:

2(Program)

这是在命名空间-day two 中定义的类名。

C#中的类是对象的蓝图。对象是类的动态创建实例。在我们的控制台程序中,我们有一个包含名为Main的方法的程序类。

4(Main)

这是我们程序的入口点。对于我们的 C#程序,至少需要一个Main方法,并且它应该是静态的。我们将在接下来的部分“C#保留关键字概述”中详细讨论staticMain也是一个保留关键字。

入口是让 CLR 知道 DLL 中函数的何时何地的方式。例如,每当我们运行我们的控制台应用程序时,它告诉 CLRMain是入口点,以及周围的一切。有关更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/framework/interop/specifying-an-entry-pointdocs.microsoft.com/en-us/dotnet/framework/interop/specifying-an-entry-point

5(Day02)

这是我们控制台应用程序的解决方案的名称。

一个解决方案可以包含许多库、应用程序、项目等。例如,我们的解决方案 Day02 将包含另一个名为 Day03 或 Day04 的项目。我们控制台应用程序的 Visual Studio 解决方案文件名是Day02.sln

请参考stackoverflow.com/questions/30601187/what-is-a-solution-in-visual-studio以了解 Visual Studio 解决方案。

要查看解决方案文件,请打开Day02.sln解决方案文件所在的文件夹。您可以直接使用任何文本编辑器/记事本打开此文件。我使用 Notepad++ (notepad-plus-plus.org/)来查看解决方案文件。

以下截图描述了我们的解决方案文件:

6(Day02)

这是我们控制台应用程序的一个项目。

一个项目是一个包,其中包含了程序所需的一切。这是官方网站上对项目的定义:docs.microsoft.com/en-us/visualstudio/ide/solutions-and-projects-in-visual-studio

在逻辑上和文件系统中,一个项目包含在一个解决方案中,该解决方案可能包含一个或多个项目,以及构建信息、Visual Studio 窗口设置和与任何项目无关的任何杂项文件。从字面上讲,解决方案是一个具有自己独特格式的文本文件;通常不打算手动编辑。

我们的项目文件名是Day02.csproj

您不需要为您的应用程序创建一个项目。您可以直接开始在您的 C#文件上工作。

以下截图描述了我们的项目文件:

7(依赖项)

这指的是运行特定应用程序所需的所有引用和二进制文件。

依赖项是我们的应用程序依赖的程序集或 dll,或者我们的应用程序正在使用所引用程序集的函数。例如,我们的控制台应用程序需要.NET Core 2.0 SDK,因此将其包含为依赖项。请参阅以下截图:

8(Program.cs)

这是物理类文件名。

这是一个在我们的磁盘驱动器上物理可用的类文件的名称。类名和文件名可以不同,这意味着如果我的类名是Program,那么我的类文件名可以是Program1.cs。然而,用不同的名称来命名类和文件名是不好的做法,但你可以这样做,编译器不会抛出任何异常。有关更多信息,请参阅stackoverflow.com/questions/2224653/c-sharp-cs-file-name-and-class-name-need-to-be-matched

使用 Visual Studio 深入了解应用程序

在前一节中,您了解了我们的控制台应用程序可以包含的各种内容。在本节中,让我们通过使用 Visual Studio 进行更深入的了解。

要开始,请转到项目属性。从解决方案资源管理器中进行此操作(右键单击项目,然后单击“属性”),或者从菜单中进行此操作(项目 | Day02 属性);您将获得项目属性窗口,如下截图所示:

在应用程序选项卡上,我们可以设置程序集名称、默认命名空间、目标框架和输出类型(输出类型为控制台应用程序Windows 应用程序类库)。

以下截图是构建选项卡的截图:

从构建选项卡中,我们可以设置条件编译符号、平台目标和其他可用选项。

条件编译就是预处理器,我们将在第六天讨论。

以下截图显示了包选项卡:

包选项卡帮助我们直接创建 NuGet 包。在早期版本中,我们需要大量配置设置来构建 NuGet 包。在当前版本中,我们只需要在包选项卡上提供信息,Visual Studio 将根据我们的选项生成 NuGet 包。调试选项卡、签名和资源选项卡都是不言自明的,并为我们提供了一种签署程序集和支持在程序中嵌入资源的方法。

讨论代码

我们已经讨论了控制台应用程序,并讨论了典型控制台应用程序包含的内容以及如何使用 Visual Studio 设置各种内容。现在让我们讨论我们在上一节“理解典型的 C#程序”中编写的代码。

ConsoleSystem命名空间的静态类,不能被继承。

在上述代码中,我们指示程序使用WriteLine()方法向控制台输出一些内容。

Console类的官方定义如下(docs.microsoft.com/en-us/dotnet/api/system.console?view=netcore-2.0):

表示控制台应用程序的标准输入、输出和错误流。此类不能被继承。

Console只是操作系统的终端窗口(也称为控制台用户界面CUI))与用户交互。Windows 操作系统有控制台,即接受 MS-DOS 命令的命令提示符。Console类提供了基本支持来实现这一点。

以下是我们可以在控制台上执行的一些重要操作。

颜色

可以使用设置器和获取器属性来更改控制台背景和/或前景颜色,这些属性接受ConsoleColor枚举的值。有一个Reset方法可以将其设置为默认颜色。让我们使用以下代码演示所有颜色组合:

private static (int, int) DisplayColorMenu(ConsoleColor[] colors) 
{ 
var count = 0; 

foreach (var color in colors) 
    { 
        WriteLine($"{count}{color}"); 
        count += 1; 
    } 
WriteLine($"{count + 1} Reset"); 
WriteLine($"{count + 2} Exit"); 

Write("Choose Foreground color:"); 
var foreground = Convert.ToInt32(ReadLine()); 
Write("Choose Background color:"); 
var background = Convert.ToInt32(ReadLine()); 

return new ValueTuple<int, int>(background, foreground); 
} 

上述代码是来自 GitHub 存储库中可用的完整源代码的一部分。完整的代码将提供以下输出:

蜂鸣

Beep是通过控制台扬声器生成系统声音的方法。以下是最简单的示例:

private static void ConsoleBeepExample() 
{ 
for (int i = 0; i &lt; 9; i++) 
Beep(); 
} 

在控制台应用程序中,还有一些有用的方法。有关这些方法的更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/api/system.console?view=netcore-2.0

到目前为止,我们已经使用 Visual Studio 2017 的代码示例讨论了典型的 C#程序;我们已经讨论了控制台程序的各个部分。您可以再次查看本节,或者继续阅读。

C#保留关键字、类型和运算符概述

保留关键字只是编译器的预定义单词,具有特殊含义。除非您明确告诉编译器该单词不是为编译器保留的,否则不能将这些保留关键字用作普通文本或标识符。

在 C#中,您可以通过在保留关键字前加上@符号来将其用作普通单词。

C#关键字分为以下几类:

  • 类型:在 C#中,类型系统分为值类型、引用类型和指针类型。

  • 修饰符:从其名称就可以自解释,修饰符用于修改特定类型的声明和成员。

  • 语句关键字:这些是按顺序执行的编程指令。

  • 方法参数:这些可以声明为值类型或引用类型,并且可以使用outref关键字传递值。

  • 命名空间关键字:这些是属于命名空间的关键字。

  • 运算符关键字:这些运算符通常用于执行各种操作,例如类型检查,获取对象的大小等。

  • 转换关键字:这些是explicitimplicitoperator关键字,将在接下来的部分中讨论。

  • 访问关键字:这些是帮助从属于其父类或自身的类中访问事物的常见关键字。这些关键字是thisbase

  • 文字关键字:关键字具有一些用于赋值的值,即nulltruefalsedefault

  • 上下文关键字:这些在代码中具有特定含义。这些是 C#中未保留的特殊关键字。

  • 查询关键字:这些是可以在查询表达式中使用的上下文关键字,例如,from关键字可以用于 LINQ。

在接下来的部分中,我们将使用代码示例更详细地讨论 C#关键字。

标识符

这些关键字在 C#程序的任何部分中使用,并且是保留的。标识符是特殊关键字,编译器对其进行不同处理。

这些是 C#保留的标识符:

  • abstract:这告诉您带有抽象修饰符的事物尚未完成或缺少定义。我们将在第四天详细讨论这个。

  • as:这可以用于转换操作。换句话说,我们可以说这检查两种类型之间的兼容性。

as关键字属于运算符类别的关键字;参考docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/operator-keywords

as identifier:
public class Stackholder 
{ 
public void GetAuthorName(Person person) 
    { 
var authorName = person as Author; 
Console.WriteLine(authorName != null ? $"Author is {authorName.Name}" :"No author."); 
    } 

} 

//Rest code is omitted 
as operator, it is called by the following code:
private static void ExampleIsAsOperator() 
{ 
WriteLine("isas Operator"); 
var author = new Author{Name = "Gaurav Aroraa"}; 

WriteLine("Author name using as:\n"); 
stackholder.GetAuthorName(author); 

} 

这将产生以下结果:

public class TeamMember :Person 
{ 
public override string Name { get; set; } 
public void GetMemberName() 
    { 
     Console.WriteLine($"Member name:{Name}"); 
    } 
} 

public class ContentMember :TeamMember 
{ 
public ContentMember(string name) 
    { 
     base.Name = name; 
    } 
public void GetContentMemberName() 
    { 
     base.GetMemberName(); 
    } 
} 

这是一个用于展示base功能的非常简单的示例。在这里,我们只是使用基类成员和方法来获得预期的输出:

  • bool:这是structureSystem.Boolean的别名,有助于声明变量。它有两个值:true 或 false。我们将在即将到来的部分数据类型中详细讨论这个。

  • break:这个关键字很容易理解;它中断特定代码执行中的某些内容,可以是流程语句(for循环)或代码块的终止(switch)。我们将在循环和语句的即将到来的部分详细讨论这个。

  • byte:这有助于声明无符号整数的变量。这是System.Byte的别名。我们将在即将到来的部分详细讨论这个。

  • 案例:这与Switch语句一起使用,然后根据某些条件执行代码块。我们将在第三天讨论switch案例。

  • catch:这个关键字是异常处理块的 catch 块,即try..catch..finally。我们将在第六天详细讨论异常处理。

  • char:当我们声明一个变量来存储属于结构System.Char的字符时,这个关键字很有用。我们将在数据类型部分详细讨论这个。

  • checked:有时,您的程序可能会遇到溢出值。溢出异常意味着您分配了一个比被分配数据类型的最大值还要大的值。编译器会引发溢出异常,程序终止。关键字 checks 强制编译器确保在编译器忽略的情况下不会发生溢出。为了更好地理解这一点,请看下面的代码片段:

int sumWillthrowError = 2147483647 + 19; //compile time error

这将生成一个编译时错误。一旦您写下前面的语句,您将得到以下错误:

以下代码片段是一个修改后的代码,如前图所示。通过这种修改,新代码将不会生成编译时错误:

Private static void CheckOverFlowExample()
{
var maxValue = int.MaxValue;
var addSugar = 19;
var sumWillNotThrowError = maxValue + addSugar;
WriteLine($"sum value:{sumWillNotThrowError} is not the correct value because it is larger than {maxValue}.");
} 

前面的代码永远不会引发溢出异常,但它不会给出正确的总和;它会给出-2147483647 作为2147483647 + 19 的结果,因为实际总和将超过整数的最大正值,即2147483647。它将产生以下输出:

在现实世界的程序中,我们不能冒错计算的风险。我们应该使用 checked 关键字来克服这种情况。让我们使用 checked 关键字重写前面的代码:

private static void CheckOverFlowExample() 
{ 
const int maxValue = int.MaxValue; 
const int addSugar = 19; 
var sumWillNotThrowError = checked(maxValue+addSugar); //compile time error 
WriteLine( 
$"sum value:{sumWillNotThrowError} is not the correct value because it is larger than {maxValue}."); 
} 

一旦您使用 checked 关键字编写代码,您将看到以下编译时错误:

现在让我们讨论 C#的更多关键字:

  • class:这个关键字帮助我们声明类。C#类将包含成员、方法、变量、字段等(我们将在第四天详细讨论这些)。类与结构不同;我们将在类与结构部分详细讨论这个。

  • const:这个关键字帮助我们声明常量字段或常量局部变量。我们将在第三天详细讨论这个。

  • continue:这个关键字是break的对手。它将控制传递给流程语句中的下一次迭代,即whiledoforforeach。我们将在接下来的部分详细讨论这个。

  • decimal:这有助于我们声明一个 128 位的数据类型。我们将在数据类型部分详细讨论这个。

  • default:这是告诉我们switch语句中的默认条件的关键字。我们也可以使用默认作为文字来获取默认值;我们将在第三天讨论这个。

  • delegate:这有助于声明类似于方法签名的委托类型。我们将在第六天详细讨论这个。

  • do:这会重复执行一个语句,直到满足假的表达式条件。我们将在接下来的部分讨论这个。

  • double:这有助于声明简单的 64 位浮点值。我们将在接下来的部分详细讨论这个。

  • else:这与if语句一起,并执行不符合if条件的code语句。我们将在接下来的部分详细讨论这个。

  • enum:这有助于创建枚举。我们将在第四天讨论这个。

  • event:这有助于在发布者类中声明事件。我们将在第六天详细讨论这个。

  • explicit:这是转换关键字之一。此关键字声明用户定义的类型转换运算符。我们将在接下来的部分详细讨论这个。

  • false:布尔值表示false条件,resultOperator。我们将在接下来的部分详细讨论这个。

  • finally:这是异常处理块的一部分。最终,块总是被执行。我们将在第四天详细讨论这个。

  • fixed:这在不安全的代码中使用,有助于防止 GC 分配或重定位。我们将在第六天详细讨论这个。

  • float:这是一个简单的数据类型,用于存储 32 位浮点值。我们将在接下来的部分详细讨论这个。

  • 对于:for关键字是流程语句的一部分。通过使用for循环,可以重复运行一个语句,直到达到特定的表达式。我们将在接下来的章节中详细讨论这个问题。

  • 对于每个:这也是一个流程语句,但它只适用于集合或数组的元素。可以使用gotoreturnbreakthrow关键字退出。我们将在接下来的章节中详细讨论这个问题。

  • 转到:这将通过标签将控制转移到另一个部分。在 C#中,goto 通常与switch..case语句一起使用。我们将在接下来的章节中详细讨论这个问题。

  • 如果:这是一个条件语句关键字。它通常与if...else语句一起使用。我们将在接下来的章节中详细讨论。

  • 隐式:类似于显式关键字,这有助于声明隐式用户定义的转换。我们将在接下来的章节中详细讨论这个问题。

  • 在:一个关键字,有助于检测我们需要在foreach循环中迭代的集合。我们将在接下来的章节中详细讨论这个问题。

  • 整数:这是结构System.Int32的别名,是一个存储有符号 32 位整数值的数据类型。我们将在接下来的章节中详细讨论这个问题。

  • 接口:这个关键字有助于声明一个只能包含方法、属性、事件和索引器的接口(我们将在第四天讨论这个问题)。

  • 内部:这是一个访问修饰符。我们将在第四天详细讨论。

  • 是:类似于as操作符,is也是一个关键字操作符。

  • 这是一个显示is操作符的代码示例:

public void GetStackholdersname(Person person) 
{ 
if (person is Author) 
    { 
     Console.WriteLine($"Author name:{((Author)person).Name}"); 
    } 
elseif (person is Reviewer) 
    { 
     Console.WriteLine($"Reviewer name:{((Reviewer)person).Name}"); 
    } 
elseif(person is TeamMember) 
    { 
     Console.WriteLine($"Member name:{((TeamMember)person).Name}"); 
    } 
else 
    { 
     Console.Write("Not a valid name."); 
    } 

} 

有关isas操作符的完整解释,请参阅goo.gl/4n73JC

  • 锁定:这表示代码块的临界区。通过使用lock关键字,我们将获得对象的互斥锁,并在语句执行后释放。这通常与线程一起使用。线程超出了本书的范围。有关更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statementdocs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/threading/index

  • 长:这有助于声明变量来存储有符号的 64 位整数值,并且它指的是结构System.Int64。我们将在接下来的章节中详细讨论这个问题。

  • 命名空间:这有助于定义声明一组相关对象的命名空间。我们将在第四天详细讨论这个问题。

  • 新:new关键字可以是操作符、修饰符或约束。我们将在第四天详细讨论这个问题。

  • 空:这表示空引用。它不指向任何对象。引用类型的默认值是 null。在使用可空类型时很有帮助。

  • 对象:这是System.Object的别名,在.NET 世界中是通用类型。它接受任何数据类型而不是 null。

  • 操作符:这有助于重载内置操作符。我们将在接下来的章节中详细讨论这个问题。

  • 输出:这是一个上下文关键字,将在第四天详细讨论。

  • 覆盖:这个关键字有助于覆盖或扩展抽象或虚拟成员、方法、属性、索引器或事件的实现。我们将在第四天详细讨论这个问题。

  • 参数:这有助于使用可变数量的参数定义方法参数。我们将在第四天详细讨论这个问题。

  • 私人:这是一个访问修饰符,将在第四天讨论。

  • 受保护的:这是一个访问修饰符,将在第四天讨论。

  • 公共:这是一个访问修饰符,设置了整个应用程序的可用性,将在第四天讨论。

  • readonly:这有助于我们将字段声明为只读。我们将在第四天详细讨论这个问题。

  • ref:这有助于通过引用传递值。我们将在第四天详细讨论这个问题。

  • return:这有助于终止方法的执行,并返回给调用方法的结果。我们将在第四天详细讨论这个问题。

  • sbyte:这表示System.SByte并存储带符号的 8 位整数值。我们将在接下来的部分中详细讨论这个问题。

  • sealed:这是一个修饰符,防止进一步使用/扩展。我们将在第四天详细讨论这个问题。

  • :这表示System.Int16并存储带符号的 16 位整数值。我们将在接下来的部分中详细讨论这个问题。

  • sizeof:这有助于获取内置类型和/或不受管理类型的字节大小。对于不受管理和除内置数据类型之外的所有其他类型,需要使用unsafe关键字。

  • 以下代码解释了使用内置类型的sizeof

private static void SizeofExample() 
{ 
WriteLine("Various inbuilt types have size as mentioned below:\n"); 
WriteLine($"The size of data type int is: {sizeof(int)}"); 
WriteLine($"The size of data type long is: {sizeof(long)}"); 
WriteLine($"The size of data type double is: {sizeof(double)}"); 
WriteLine($"The size of data type bool is: {sizeof(bool)}"); 
WriteLine($"The size of data type short is: {sizeof(short)}"); 
WriteLine($"The size of data type byte is: {sizeof(byte)}"); 
} 

前面的代码产生了以下输出:

让我们讨论更多的 C#关键字;这些关键字在编写现实世界程序时非常重要,并在其中发挥着重要作用:

  • static:这有助于我们声明静态成员,并将在第四天详细讨论。

  • 字符串:这有助于存储 Unicode 字符。这是一个引用类型。我们将在接下来的部分中更详细地讨论这个问题,字符串

  • 结构:这有助于我们声明一个struct类型。结构类型是一个值类型。我们将在接下来的部分中更详细地讨论这个问题,类与结构

  • switch:这有助于声明一个switch语句。Switch 是一个选择语句,我们将在第三天讨论它。

  • this:这个this关键字帮助我们访问当前类实例的成员。它也是一个修饰符,我们将在第四天讨论。请注意,this关键字对于extension方法有特殊含义。扩展方法超出了本书的范围;有关更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

  • throw:这有助于抛出系统或自定义异常。我们将在第六天详细讨论这个问题。

  • true:与 false 类似,我们之前讨论过这个问题。它表示一个布尔值,可以是文字或操作符。我们将在接下来的部分中更详细地讨论这个问题。

  • try:这表示异常处理的try块。Try 块是帮助处理任何不可避免的错误或程序实例的另外三个块之一。所有三个块共同称为异常处理块。try 块总是首先出现。这个块包含可能引发异常的代码。我们将在第六天详细讨论这个问题。

  • typeof:这有助于获取所需类型的类型对象。此外,在运行时,您可以使用GetType()方法获取对象的类型。

以下代码片段显示了typeof()方法的运行情况:

private static void TypeofExample() 
{ 
var thisIsADouble = 30.3D; 
WriteLine("using typeof()"); 
WriteLine($"System.Type Object of {nameof(Program)} is {typeof(Program)}\n"); 
var objProgram = newProgram(); 
WriteLine("using GetType()"); 
WriteLine($"Sytem.Type Object of {nameof(objProgram)} is {objProgram.GetType()}"); 
WriteLine($"Sytem.Type Object of {nameof(thisIsADouble)} is {thisIsADouble.GetType()}"); 
} 

前面的代码将生成以下结果:

这些是无符号数据类型,这些数据类型存储没有符号的值(+/-):

  • uint:这有助于声明一个无符号 32 位整数的变量。我们将在接下来的部分中详细讨论这个问题。

  • ulong:这有助于声明一个无符号 65 位整数的变量。我们将在接下来的部分中详细讨论这个问题。

  • unchecked:这个关键字的作用与checked完全相反。使用checked关键字抛出编译时错误的代码块,使用unchecked关键字不会生成任何编译时异常。

让我们重写使用checked关键字编写的代码,并看看unchecked关键字如何与checked完全相反:

private static void CheckOverFlowExample() 
{ 
const int maxValue = int.MaxValue; 
const int addSugar = 19; 
//int sumWillthrowError = 2147483647 + 19; //compile time error 
var sumWillNotThrowError = unchecked(maxValue+addSugar); 
//var sumWillNotThrowError = checked(maxValue + addSugar); //compile time error 
WriteLine( 
$"sum value:{sumWillNotThrowError} is not the correct value because it is larger than {maxValue}."); 
} 

前面的代码将顺利运行,但会给出错误的结果,即-2147483647

您可以通过参考docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/checked来了解有关 checked 和 unchecked 关键字的更多详细信息。

  • unsafe:这有助于执行通常使用指针的不安全代码块。我们将在第六天详细讨论这个问题。

  • ushort:这有助于声明一个无符号的 16 位整数变量。我们将在接下来的数据类型部分更详细地讨论这个问题。

  • 使用:using关键字的作用类似于指令或语句。让我们考虑以下代码示例:

using System;

前面的指令提供了属于System命名空间的所有内容:

using static System.Console;

前面的指令帮助我们调用静态成员。在程序中包含了前面的指令后,我们可以直接调用静态成员、方法等,如下面的代码所示:

Console.WriteLine("This WriteLien is without using static directive");
WriteLine("This WriteLien is called after using static directive");
Console.WriteLine, but in the second statement, there is no need to write the class name, so we can directly call the WriteLine method.
IDisposable interface):
public class DisposableClass : IDisposable 
{ 
public string GetMessage() 
    { 
     return"This is from a Disposable class."; 
    } 
protected virtual void Dispose(bool disposing) 
    { 
     if (disposing) 
        { 
         //disposing code here 
        } 
    } 

public void Dispose() 
    { 
       Dispose(true); 
       GC.SuppressFinalize(this); 
    } 
} 
private static void UsingExample() 
{ 
using (var disposableClass = new DisposableClass()) 
    { 
     WriteLine($"{disposableClass.GetMessage()}"); 
    } 
} 

前面的代码产生以下输出:

C#关键字 virtual 和 void 有特殊含义:一个允许另一个覆盖它,而另一个在方法返回无内容时用作返回类型。让我们详细讨论这两个:

  • virtual:如果使用了 virtual 关键字,这意味着它允许在派生类中重写方法、属性、索引器或事件。我们将在第四天更详细地讨论这个问题。

  • void:这是System.Void类型的别名。当 void 用于方法时,意味着该方法没有任何返回类型。例如,看一下以下代码片段:

public void GetAuthorName(Person person) 
{ 
var authorName = person as Author; 
Console.WriteLine(authorName != null ? $"Author is {authorName.Name}" :"No author."); 
} 
getAuthorName() method is of void type; hence, it does not return anything.
  • while:While 是一个流程语句,执行特定的代码块,直到指定的表达式求值为 false。我们将在接下来的流程语句部分更详细地讨论这个问题。

上下文

这些不是保留关键字,但它们对程序的有限上下文有特殊含义,并且也可以在该上下文之外用作标识符。

这些是 C#的上下文关键字:

  • add:这用于定义自定义访问器,并在有人订阅事件时调用。add访问器总是后跟remove访问器,这意味着当我们提供add访问器时,应该应用remove访问器。有关更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-implement-interface-events

  • ascending/descending:这个上下文关键字与LINQ语句中的orderby子句一起使用。我们将在第六天更详细地讨论这个问题。

  • async:这用于异步方法、lambda 表达式或匿名方法。要从异步方法中获取结果,需要使用await关键字。我们将在第六天更详细地讨论这个问题。

  • dynamic:这有助于我们绕过编译时类型检查。这会在运行时解析类型。

编译时类型是您用来定义变量的类型。运行时类型是指变量实际所属的类型。

让我们看一下以下代码,以更好地理解这些术语:

internal class Parent
{
//stuff goes here
}
internal class Child : Parent
{
//stuff goes here
}

我们可以这样创建子类的对象:

Parent myObject = new Child();

在这里,myObject的编译时类型是Parent,因为编译器知道变量是Parent类型,而不关心或知道我们用Child类型实例化了这个对象的事实。因此这是一个编译时类型。运行时类型是实际类型,在我们的例子中是Child。因此,我们的变量myObject的运行时类型是Child

看一下以下代码片段:

private static void DynamicTypeExample() 
{ 
dynamic dynamicInt = 10; 
dynamic dynamicString = "This is a string"; 
object obj = 10; 
WriteLine($"Run-time type of {nameof(dynamicInt)} is {dynamicInt.GetType()}"); 
WriteLine($"Run-time type of {nameof(dynamicString)} is {dynamicString.GetType()}"); 
WriteLine($"Run-time type of {nameof(obj)} is {obj.GetType()}"); 

} 

上面的代码产生以下输出:

有关更多信息,请参阅:docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/dynamic

这些是在查询表达式中使用的上下文关键字;让我们详细讨论这些关键字:

  • from:这在查询表达式中使用,将在第六天讨论。

  • get:这定义了访问器,并与属性一起用于检索值。我们将在第六天更详细地讨论这个问题。

  • group:这与查询表达式一起使用,并返回一系列IGroupong<Tkey,TElement>对象。我们将在第六天更详细地讨论这个问题。

  • into:这个标识符在处理查询表达式时帮助存储临时数据。我们将在第六天更详细地讨论这个问题。

有关上下文关键字的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords

类型

在 C#中,7.0 类型也被称为数据类型和变量。这些被归类为以下更广泛的类别。

值类型

这些类型是从System.ValueType类派生的。值类型的变量直接包含它们的数据,或者简单地说,值类型变量可以直接赋值。值类型可以分为更多的子类别:数据类型、自定义类型(Enum类型和Struct类型)。在本节中,我们将详细讨论数据类型。Enum将在第四天讨论,结构将在接下来的章节中讨论。

数据类型

这些也被称为符合值类型、简单值类型和基本值类型。我称这些数据类型是因为它们定义值的性质。以下表包含所有值类型:

性质 类型 CLR 类型 范围 默认值 大小
签名整数 sbyte System.SByte -128 到 127 0 8 位
short System.Short -32,768 到 32,767 0 16 位
int System.Int32 -2,147,483,648 到 2,147,483,647 0 32 位
long System.Int64 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 0L 64 位
无符号整数 byte System.Byte 0 到 255 0 8 位
ushort System.UInt16 0 到 65,535 0 16 位
uint System.UInt32 0 到 4,294,967,295 0 32 位
ulong System.UInt64 0 到 18,446,744,073,709,551,615 0 64 位
Unicode 字符 char System.Char U +0000 到 U +ffff '\0' 16 位
浮点数 float System.Float -3.4 x 10³⁸ 到 + 3.4 x 10³⁸ 0.0F 32 位
double System.Double (+/-)5.0 x 10^(-324) 到 (+/-)1.7 x 10³⁰⁸ 0.0D 64 位
更高精度的十进制 decimal System.Decimal (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 0.0M 128 位
布尔值 bool System.Boolean 真或假 布尔值

我们可以通过以下代码片段证明前表中提到的值:

//Code is omitted 
public static void Display() 
{ 
WriteLine("Table :: Data Types"); 
var dataTypes = DataTypes(); 
WriteLine(RepeatIt('\u2500', 100)); 
WriteLine("{0,-10} {1,-20} {2,-50} {3,-5}", "Type", "CLR Type", "Range", "Default Value"); 
WriteLine(RepeatIt('\u2500', 100)); 
foreach (var dataType in dataTypes) 
WriteLine("{0,-10} {1,-20} {2,-50} {3,-5}", dataType.Type, dataType.CLRType, dataType.Range, 
dataType.DefaultValue); 
WriteLine(RepeatIt('\u2500', 100)); 
} 
//Code is omitted 

在上述代码中,我们显示了数据类型的最大值和最小值,产生了以下输出:

引用类型

实际数据并不存储在变量中,而是包含对变量的引用。简单地说,我们可以说引用类型是指向内存位置的引用。此外,多个变量可以引用一个内存位置,如果这些变量中的任何一个改变了该位置的数据,所有变量都将获得新值。以下是内置的引用类型:

  • 类类型:包含成员、方法、属性等的数据结构。这也被称为对象类型,因为它继承了通用的classSystem.Object。在 C# 7.0 中,类类型支持单继承;我们将在第七天更详细地讨论这个问题。

对象类型可以被赋予任何其他类型的值;对象只是System.Object的别名。在这种情况下,其他类型指的是值类型、引用类型、预定义类型和用户定义类型。

有一个概念叫做装箱拆箱,当我们处理对象类型时会发生。一般来说,当值类型转换为对象类型时,称为装箱,当对象类型转换为值类型时,称为拆箱

看一下以下代码片段:

private static void BoxingUnboxingExample() 
{ 
int thisIsvalueTypeVariable = 786; 
object thisIsObjectTypeVariable = thisIsvalueTypeVariable; //Boxing 
thisIsvalueTypeVariable += 1; 
    WriteLine("Boxing"); 
WriteLine($"Before boxing: Value of {nameof(thisIsvalueTypeVariable)}: {thisIsvalueTypeVariable}"); 
WriteLine($"After boxing: Value of {nameof(thisIsObjectTypeVariable)}: {thisIsObjectTypeVariable}"); 

thisIsObjectTypeVariable = 1900; 
thisIsvalueTypeVariable = (int) thisIsObjectTypeVariable; //Unboxing 
    WriteLine("Unboxing"); 
WriteLine($"Before Unboxing: Value of {nameof(thisIsObjectTypeVariable)}: {thisIsObjectTypeVariable}"); 
WriteLine($"After Unboxing: Value of {nameof(thisIsvalueTypeVariable)}: {thisIsvalueTypeVariable}"); 
 }
thisIsvalueTypeVariable variable is assigned to an object thisIsObjectTypeVariable. On the other hand, unboxing happened when we cast object variable thisIsObjectTypeVariable to our value type thisIsvalueTypeVariable variable with int. This is the output of the code:

在这里,我们将讨论三种重要的类型,它们是接口、字符串和委托类型:

  • 接口类型:这种类型基本上是一个合同,谁要使用它就必须实现它。一个类或结构体可以使用一个或多个接口类型。一个接口类型可以从多个其他接口类型继承。我们将在第七天更详细地讨论这个问题。

  • 委托类型:这是一个表示参数列表中方法的引用的类型。众所周知,委托被称为函数指针(如 C++中定义)。委托是类型安全的。我们将在第四天详细讨论这个问题。

  • 字符串类型:这是System.String的别名。这种类型允许你将任何字符串值赋给变量。我们将在接下来的章节中详细讨论这个问题。

指针类型

这种类型属于不安全的代码。定义为指针类型的变量存储另一个变量的内存地址。我们将在第六天详细讨论这个问题。

空类型

可空类型只是System.Nullable<T>结构的一个实例。可空类型包含与其ValueType相同的数据范围,但额外增加了一个空值。参考数据类型表,其中 int 的范围为 2147483648 到 2147483647,但System.Nullable<int>int?具有相同的范围,另外还可以为 null。这意味着你可以这样做:int? nullableNum = null;

有关可空类型的更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/nullable-types/

运算符

在 C#中,运算符只是告诉编译器执行特定操作的数学或逻辑运算符。例如,乘法(*)运算符告诉编译器进行乘法运算;另一方面,逻辑与(&&)运算符检查两个操作数。我们可以将 C#运算符分为更广泛的类型,如下表所示:

类型 运算符 描述
算术运算符 + 添加两个操作数,例如,var result = num1 +num2;
- 从第二个操作数中减去第一个操作数,例如,var result = num1 - num2;
* 乘以两个操作数,例如,var result = num1 * num2;
/ 除以分子除以分母,例如,var result = num1 / num2;
% 取模,例如,result = num1 % num2;
++ 增量运算符,将值增加 1,例如,var result = num1++;
-- 递减运算符,将值减 1,例如,var result = num1--;
关系运算符 == 确定两个操作数是否具有相同的值。如果表达式成功,则返回 True;否则返回 false,例如,var result = num1 == num2;
!= 执行与==相同的操作,但否定比较;如果两个操作数相等,则返回 false,例如,var result = num1 != num2;
> 确定表达式中左操作数是否大于右操作数,并在成功时返回 True,例如,var result = num1 > num2;
< 确定表达式中左操作数是否小于右操作数,并在成功时返回 true,例如,var result = num1 < num2;
>= 确定表达式中左操作数的值是否大于或等于右操作数的值,并在成功时返回 true,例如,var result = num1 <= num2;
<= 确定在表达式中,左操作数的值是否小于或等于右操作数的值,并在成功时返回 true,例如,var result = num1 <= num2;
逻辑运算符 && 这是逻辑AND运算符。表达式根据左操作数进行评估;如果为 true,则右操作数不会被忽略,例如,var result = num1 && num2;
|| 这是逻辑OR运算符。如果任一操作数为 true,则表达式求值为 true,例如,var result = num1 &#124;&#124; num2;
! 这被称为逻辑NOT运算符。它颠倒评估结果,例如,var result = !(num1 && num2);
位运算符 | 这是位OR运算符,作用于位。如果任一位为 1,则结果为 1,例如,var result = num1 &#124; num2;
& 这是位AND运算符,作用于位。如果任一位为 0,则结果为 0;否则为 1,例如,var result = num1 & num2;
^ 这是位XOR运算符,作用于位。如果位相同,则结果为 0;否则为 1,例如,var result = num1 ^ num2;
~ 这是一元运算符,称为位COMPLEMENT运算符。它作用于单个操作数并颠倒位,这意味着如果位为 0,则返回 1,反之亦然,例如,var result = ~num1;
<< 这是位左移运算符,按表达式中指定的位数向左移动数字,并在最低有效位添加零,例如,var result = num1 << 1;
>> 这是位右移运算符,按表达式中指定的位数向右移动数字,例如,var result = num1 >> 1;
赋值运算符 = 将右侧操作数的值赋给左侧操作数的赋值运算符,例如,var result = nim1 + num2;
+= 加法赋值运算符;它将右操作数的值加上并赋给左操作数,例如,result += num1;
-= 减法赋值运算符;它将右操作数的值减去并赋给左操作数,例如,result -= num1;
*= 乘法赋值运算符;它将右操作数的值乘以并赋给左操作数,例如,result *= num1;
/= 除法赋值运算符;它将右操作数的值除以并赋给左操作数,例如,result /= num1;
%= 取模赋值运算符;它取左右操作数的模并赋给左操作数,例如,result %= num1;
<<= 位左移和赋值,例如,result <<= 2;
>>;= 位右移和赋值,例如,result >>= 2;
&= AND和赋值运算符,例如,result &= Num1;
^= XOR和赋值运算符,例如,result ^= num1;
|= OR和赋值运算符,例如,result &#124;= num1;

看一下以下代码片段,其中实现了先前讨论的所有运算符:

private void ArithmeticOperators() 
{ 
WriteLine("\nArithmetic operators\n"); 
WriteLine($"Operator '+' (add): {nameof(Num1)} + {nameof(Num2)} = {Num1 + Num2}"); 
WriteLine($"Operator '-' (substract): {nameof(Num1)} - {nameof(Num2)} = {Num1 - Num2}"); 
WriteLine($"Operator '*' (multiplication): {nameof(Num1)} * {nameof(Num2)} = {Num1 * Num2}"); 
WriteLine($"Operator '/' (division): {nameof(Num1)} / {nameof(Num2)} = {Num1 / Num2}"); 
WriteLine($"Operator '%' (modulus): {nameof(Num1)} % {nameof(Num2)} = {Num1 % Num2}"); 
WriteLine($"Operator '++' (incremental): pre-increment: ++{nameof(Num1)} = {++Num1}"); 
WriteLine($"Operator '++' (incremental): post-increment: {nameof(Num1)}++ = {Num1++}"); 
WriteLine($"Operator '--' (decremental): pre-decrement: --{nameof(Num2)} = {--Num2}"); 
WriteLine($"Operator '--' (decremental): post-decrement: {nameof(Num2)}-- = {Num2--}"); 
ReadLine(); 
} 
//Code omitted 

完整的代码可在 GitHub 存储库中找到,并且产生以下结果:

讨论 C#中的运算符优先级

任何表达式的计算或评估以及运算符的顺序都非常重要。这就是所谓的运算符优先级。我们都读过数学规则BODMAS,它是括号、指数、乘除、加减的缩写。请参考www.skillsyouneed.com/num/bodmas.html来刷新您的记忆。因此,数学教会我们如何解决表达式;同样,我们的 C#应该遵循规则来解决或评估表达式。例如,3+25的计算结果是13而不是25。因此,在这个等式中,规则是先乘后加。这就是为什么它计算为25 = 10然后3+10 = 13。您可以通过应用括号来设置更高的优先级顺序,所以如果您在前面的语句中这样做(3+2)5,结果是25*。

要了解更多关于运算符优先级的信息,请参考msdn.microsoft.com/en-us/library/aa691323(VS.71).aspx

这是一个简单的代码片段来评估表达式:

private void OperatorPrecedence() 
{ 
Write("Enter first number:"); 
    Num1 = Convert.ToInt32(ReadLine()); 
Write("Enter second number:"); 
    Num2 = Convert.ToInt32(ReadLine()); 
Write("Enter third number:"); 
    Num3 = Convert.ToInt32(ReadLine()); 
Write("Enter fourth number:"); 
    Num4 = Convert.ToInt32(ReadLine()); 
int result = Num1 + Num2 * Num3/Num4; 
WriteLine($"Num1 + Num2 * Num3/Num4 = {result}"); 
    result = Num1 + Num2 * (Num3 / Num4); 
WriteLine($"Num1 + Num2 * (Num3/Num4) = {result}"); 
    result = (Num1 + (Num2 * Num3)) / Num4; 
WriteLine($"(Num1 + (Num2 * Num3)) /Num4 = {result}"); 
    result = (Num1 + Num2) * Num3 / Num4; 
WriteLine($"(Num1 + Num2) * Num3/Num4 = {result}"); 
ReadLine(); 
} 

上面的代码产生了以下结果:

运算符重载

运算符重载是重新定义特定运算符的实际功能的一种方式。当您使用用户定义的复杂类型时,直接使用内置运算符是不可能的。例如,假设您有一个具有许多属性的对象,并且您希望对这些类型的对象进行加法。像这样是不可能的:VeryComplexObject = result = verycoplexobj1 + verycomplexobj2;。为了克服这种情况,重载可以发挥魔力。

您不能重载所有内置运算符;请参考docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/overloadable-operators查看哪些运算符是可重载的。

让我们看一下以下代码片段,以了解运算符重载的工作原理(请注意,此代码不完整;请参考 Github 获取完整的源代码):

public struct Coordinate 
{ 
//code omitted 

public static Coordinateoperator +(Coordinate coordinate1, Coordinate coordinate2) =>; 
new Coordinate(coordinate1._xAxis + coordinate2._xAxis, coordinate1._yAxis + coordinate2._yAxis); 
public static Coordinateoperator-(Coordinate coordinate1, Coordinate coordinate2) => 
new Coordinate(coordinate1._xAxis - coordinate2._xAxis, coordinate1._yAxis - coordinate2._yAxis); 
public static Coordinateoperator *(Coordinate coordinate1, Coordinate coordinate2) => 
new Coordinate(coordinate1._xAxis * coordinate2._xAxis, coordinate1._yAxis * coordinate2._yAxis); 
//code omitted 

public static booloperator ==(Coordinate coordinate1, Coordinate coordinate2) =>; 
        coordinate1._xAxis == coordinate2._xAxis && coordinate1._yAxis == coordinate2._yAxis; 

public static booloperator !=(Coordinate coordinate1, Coordinate coordinate2) => !(coordinate1 == coordinate2); 

//code omitted 

public double Area() => _xAxis * _yAxis; 

public override string ToString() =>$"({_xAxis},{_yAxis})"; 
}

在上面的代码中,我们有一个新类型的坐标,它是x轴和y轴的表面。现在,如果我们想应用一些操作,使用内置运算符是不可能的。通过运算符重载,我们增强了内置运算符的实际功能。以下代码是使用的坐标类型:

private static void OperatorOverloadigExample() 
{ 
WriteLine("Operator overloading example\n"); 
Write("Enter x-axis of Surface1: "); 
var x1 = ReadLine(); 
Write("Enter y-axis of Surface1: "); 
var y1 = ReadLine(); 
Write("Enter x-axis of Surface2: "); 
var x2= ReadLine(); 
Write("Enter y-axis of Surface2: "); 
var y2= ReadLine(); 

var surface1 = new Coordinate(Convert.ToInt32(x1),Convert.ToInt32(y1)); 
var surface2 = new Coordinate(Convert.ToInt32(x2),Convert.ToInt32(y2)); 
WriteLine(); 
Clear(); 
WriteLine($"Surface1:{surface1}"); 
WriteLine($"Area of Surface1:{surface1.Area()}"); 
WriteLine($"Surface2:{surface2}"); 
WriteLine($"Area of Surface2:{surface2.Area()}"); 
WriteLine(); 
WriteLine($"surface1 == surface2: {surface1==surface2}"); 
WriteLine($"surface1 < surface2: {surface1 < surface2}"); 
WriteLine($"surface1 > surface2: {surface1 > surface2}"); 
WriteLine($"surface1 <= surface2: {surface1 <= surface2}"); 
WriteLine($"surface1 >= surface2: {surface1 >= surface2}"); 
WriteLine(); 
var surface3 = surface1 + surface2; 
WriteLine($"Addition: {nameof(surface1)} + {nameof(surface2)} = {surface3}"); 
WriteLine($"{nameof(surface3)}:{surface3}"); 
WriteLine($"Area of {nameof(surface3)}: {surface3.Area()} "); 
WriteLine(); 
WriteLine($"Substraction: {nameof(surface1)} - {nameof(surface2)} = {surface1-surface2}"); 
WriteLine($"Multiplication: {nameof(surface1)} * {nameof(surface2)} = {surface1 * surface2}"); 
WriteLine($"Division: {nameof(surface1)} / {nameof(surface2)} = {surface1 / surface2}"); 
WriteLine($"Modulus: {nameof(surface1)} % {nameof(surface2)} = {surface1 % surface2}"); 
} 
Coordinate and call operators for various operations. Note that by overloading, we have changed the actual behavior of the operator, for instance, the add (*+*) operator, which generally adds two numbers, but with the implementation here, the add (*+*) operator gives the sum of two surfaces. The complete code produces the following result:

类型转换概述

类型转换意味着将一种类型转换为另一种类型。或者,我们称之为强制转换或类型转换。类型转换广泛分为以下几类。

隐式转换

隐式转换是 C#编译器在内部执行的转换,以匹配变量的类型通过给该变量赋值。这个操作是隐式的,不需要编写任何额外的代码来遵守类型安全机制。在隐式转换中,只有从较小的类型到较大的类型和派生类到基类的转换是可能的。

显式转换

显式转换是用户明确执行的转换,使用强制转换运算符;这就是为什么它也被称为类型转换。显式转换也可以使用内置类型转换方法进行。有关更多信息,请参考docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/explicit-numeric-conversions-table

让我们看一下以下代码片段,展示了隐式/显式类型转换的实际操作。

private static void ImplicitExplicitTypeConversionExample() 
{ 
WriteLine("Implicit conversion"); 
int numberInt = 2589; 
double doubleNumber = numberInt; // implicit type conversion 

WriteLine($"{nameof(numberInt)} of type:{numberInt.GetType().FullName} has value:{numberInt}"); 
WriteLine($"{nameof(doubleNumber)} of type:{doubleNumber.GetType().FullName} implicitly type casted and has value:{doubleNumber}"); 

WriteLine("Implicit conversion"); 
doubleNumber = 2589.05D; 
numberInt = (int)doubleNumber; //explicit type conversion 
WriteLine($"{nameof(doubleNumber)} of type:{doubleNumber.GetType().FullName} has value:{doubleNumber}"); 
WriteLine($"{nameof(numberInt)} of type:{numberInt.GetType().FullName} explicitly type casted and has value:{numberInt}"); 
} 
numberInt of int type to a variable doubleNumber of double type, which is called implicit type conversion, and the reverse is an explicit type conversion that requires a casting using int. Note that implicitly, type conversion does not require any casting, but explicitly, conversion requires type casting, and there are chances for loss of data during explicit conversion. For instance, our explicit conversion from double to int would result in a loss of data (all precision would be truncated while a value is assigned to int type variable). This code produces the following result:

两个最重要的语言基础是类型转换和强制转换。要了解更多关于这两个的信息,请参考docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/casting-and-type-conversions

理解语句

在 C#中,你可以评估不同类型的表达式,这些表达式可能会产生结果,也可能不会。每当你说类似于如果结果>0 会发生什么,在这种情况下,我们在陈述某事。这可以是一个决策语句、结果生成语句、赋值语句或任何其他活动语句。另一方面,循环是一个重复执行一些语句的代码块。

在这一部分,我们将详细讨论语句和循环。

语句在返回结果之前应执行某些操作。换句话说,如果你在写一个语句,那个语句应该表达一些东西。为了做到这一点,它必须执行一些内置或自定义的操作。语句可以依赖于决定,也可以是任何现有语句的结果的一部分。官方页面(docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/statements)将语句定义为:

一个语句可以由一行以分号结束的代码组成,或者由一个代码块中的一系列单行语句组成。一个语句块被{}括起来,可以包含嵌套块。

看一下下面的代码片段,展示了不同的语句:

private static void StatementExample() 
{ 
WriteLine("Statement example:"); 
int singleLineStatement; //declarative statement 
WriteLine("'intsingleLineStatement;' is a declarative statment."); 
singleLineStatement = 125; //assignment statement 
WriteLine("'singleLineStatement = 125;' is an assignmnet statement."); 
WriteLine($"{nameof(singleLineStatement)} = {singleLineStatement}"); 
var persons = newList<Person> 
    { 
     newAuthor {Name = "Gaurav Aroraa" } 
    }; //declarative and assignmnet statement 
WriteLine("'var persons = new List&lt;Person&gt;();' is a declarative and assignmnet statement."); 

//block statement 
foreach (var person in persons) 
    { 
      WriteLine("'foreach (var person in persons){}' is a block statement."); 
      WriteLine($"Name:{person.Name}"); 
    } 
} 

在前面的代码中,我们使用了三种类型的语句:声明、赋值和块语句。代码产生了以下结果:

根据官方页面(docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/statements),C#语句可以大致分为以下几类。

声明语句

每当你声明一个变量或常量时,你都在写一个声明语句。你也可以在声明变量的时候给变量赋值。在声明变量的时候给变量赋值是一个可选的任务,但是常量在声明时需要赋值。

这是一个典型的声明语句:

int singleLineStatement; //declarative statement 

表达式语句

在一个表达式语句中,右侧的表达式评估结果并将该结果赋给左侧的变量。表达式语句可以是赋值、方法调用或新对象创建。这是典型的表达式语句示例:

Console.WriteLine($"Member name:{Name}"); 
var result = Num1 + Num2 * Num3 / Num4; 

选择语句

这也被称为决策语句。语句根据条件和它们的评估进行分支。条件可以是一个或多个。选择或决策语句属于if...elseswitch case。在这一部分,我们将详细讨论这些语句。

if 语句

if语句是一个决定语句,可以分支一个或多个语句进行评估。这个语句由一个布尔表达式组成。让我们考虑一下在第一天讨论过的在一本书中找元音字母的问题。让我们使用if语句来写这个问题:

private static void IfStatementExample() 
{ 
WriteLine("if statement example."); 
Write("Enter character:"); 
char inputChar = Convert.ToChar(ReadLine()); 

//so many if statement, compiler go through all if statement 
//not recommended way 
if (char.ToLower(inputChar) == 'a') 
WriteLine($"Character {inputChar} is a vowel."); 
if (char.ToLower(inputChar) == 'e') 
WriteLine($"Character {inputChar} is a vowel."); 
if (char.ToLower(inputChar) == 'i') 
WriteLine($"Character {inputChar} is a vowel."); 
if (char.ToLower(inputChar) == 'o') 
WriteLine($"Character {inputChar} is a vowel."); 
if (char.ToLower(inputChar) == 'u') 
WriteLine($"Character {inputChar} is a vowel."); 
} 
if statements without caring about the scenario where my first if statement got passed. Say, if you enter *a*, which is a vowel in this case, the compiler finds the first expression to be true and prints the output (we get our result), then the compiler checks the next if statement, and so on. In this case, the compiler unnecessarily checks the rest of all four statements that should not have happened. There might be a scenario where our code does not fall into any of the if statements in the preceding code; in that case, we would not get the expected result. To overcome such situations, we have the if...else statement, which we are going to discuss in the upcoming section.

if..else 语句

在这个if语句后面跟着 else,如果 if 块的评估为 false,则执行 else 块。这是一个简单的例子:

private static void IfElseStatementExample() 
{ 
WriteLine("if statement example."); 
Write("Enter character:"); 
char inputChar = Convert.ToChar(ReadLine()); 
char[] vowels = {'a', 'e', 'i', 'o', 'u'}; 

if (vowels.Contains(char.ToLower(inputChar))) 
WriteLine($"Character '{inputChar}' is a vowel."); 
else 
WriteLine($"Character '{inputChar}' is a consonent."); 
} 
else followed by the if statement. When the if statement evaluates to false, then the else block code will be executed.

if...else if...else 语句

if...else语句在需要测试多个条件时非常重要。在这个语句中,if语句首先评估,然后是else if语句,最后是 else 块执行。在这里,if语句可能有也可能没有if...else语句或块;if...else总是在if块之后和else块之前。else语句是if...else...if else...else语句中的最终代码块,表示前面的条件都不为真。

看一下以下代码片段:

private static void IfElseIfElseStatementExample() 
{ 
WriteLine("if statement example."); 
Write("Enter character:"); 
char inputChar = Convert.ToChar(ReadLine()); 

if (char.ToLower(inputChar) == 'a') 
{ WriteLine($"Character {inputChar} is a vowel.");} 
elseif (char.ToLower(inputChar) == 'e') 
{ WriteLine($"Character {inputChar} is a vowel.");} 
elseif (char.ToLower(inputChar) == 'i') 
{ WriteLine($"Character {inputChar} is a vowel.");} 
elseif (char.ToLower(inputChar) == 'o') 
{ WriteLine($"Character {inputChar} is a vowel.");} 
elseif (char.ToLower(inputChar) == 'u') 
{ WriteLine($"Character {inputChar} is a vowel.");} 
else 
{ WriteLine($"Character '{inputChar}' is a consonant.");} 
} 
if...else if...else statements that evaluate the expression: whether inputchar is equivalent to comparative characternot. In this code, if you enter a character other than *a*,*e*,i,*o*,*u* that does not fall in any of the preceding condition, then the case else code block executes and it produces the final result. So, when else executes, it returns the result by saying that the entered character is a consonant.

嵌套的 if 语句

嵌套的if语句只是if语句块内的if语句块。同样,我们可以嵌套else if语句块。这是一个简单的代码片段:

private static void NestedIfStatementExample() 
{ 
WriteLine("nested if statement example."); 
Write("Enter your age:"); 
int age = Convert.ToInt32(ReadLine()); 

if (age < 18) 
    { 
      WriteLine("Your age should be equal or greater than 18yrs."); 
      if (age < 15) 
        { 
         WriteLine("You need to complete your school first"); 
        } 
    } 
} 

Switch 语句

这是一种使用switch语句选择表达式的语句,它使用case块评估条件,当代码不落入任何case块时,然后执行default块(default块是switch...case语句中的可选块)。

Switch 语句也被称为if...else if...else语句的替代方法。让我们重新编写在上一节中使用的示例,以展示if...else if...else语句:

private static void SwitchCaseExample() 
{ 
WriteLine("switch case statement example."); 
Write("Enter character:"); 
charinputChar = Convert.ToChar(ReadLine()); 

switch (char.ToLower(inputChar)) 
{ 
case'a': 
WriteLine($"Character {inputChar} is a vowel."); 
break; 
case'e': 
WriteLine($"Character {inputChar} is a vowel."); 
break; 
case'i': 
WriteLine($"Character {inputChar} is a vowel."); 
break; 
case'o': 
WriteLine($"Character {inputChar} is a vowel."); 
break; 
case'u': 
WriteLine($"Character {inputChar} is a vowel."); 
break; 
default: 
WriteLine($"Character '{inputChar}' is a consonant."); 
break; 
} 
}

在前面的代码中,如果没有一个 case 评估为真,则执行 default 块。switch...case语句将在第三天详细讨论。

在选择switch...caseif...else之间有一点不同。有关更多详细信息,请参阅stackoverflow.com/questions/94305/what-is-quicker-switch-on-string-or-elseif-on-type

迭代语句

这些语句提供了一种迭代集合数据的方法。可能会有这样一种情况,您希望多次执行代码块或需要在相同活动上执行重复操作。有迭代循环可用于实现此目的。循环语句中的代码块按顺序执行,这意味着首先执行第一个语句,依此类推。以下是我们可以将 C#的迭代语句划分为的主要类别。

do...while 循环

这有助于我们重复执行语句或语句块,直到它评估为假。在do...while语句中,首先执行一系列语句,然后在while下检查条件,这意味着至少执行一次语句或语句块。

看一下以下代码:

private static void DoWhileStatementExample() 
{ 
WriteLine("do...while example"); 
Write("Enter repeatitive length:"); 
int length = Convert.ToInt32(ReadLine()); 
int count = 0; 
do 
    { 
        count++; 
        WriteLine(newstring('*',count));  
    } while (count < length); 
} 
do block executes until the statement of the while block evaluates to false.

while 循环

这将执行语句或代码块,直到条件评估为真。在执行代码块之前,此表达式评估,如果表达式评估为假,循环终止,不执行任何语句或代码块。看一下以下代码片段:

private static void WhileStatementExample() 
{ 
WriteLine("while example"); 
Write("Enter repeatitive length:"); 
int length = Convert.ToInt32(ReadLine()); 
int count = 0; 
while (count < length) 
    { 
       count++; 
       WriteLine(newstring('*', count)); 
    } 
}   

前面的代码重复执行while语句,直到表达式评估为假。

for 循环

for循环类似于其他循环,可以帮助重复运行语句或代码块,直到表达式评估为假。for循环有三个部分:initializerconditioniterator,其中initializer部分首先执行一次;这只是一个开始循环的变量。下一部分是条件,如果评估为真,那么只有主体语句才会执行;否则它终止循环。第三部分是增量或迭代器,它更新循环控制变量。让我们看一下以下代码片段:

private static void ForStatementExample() 
{ 
WriteLine("for loop example."); 
Write("Enter repeatitive length:"); 
int length = Convert.ToInt32(ReadLine()); 
for (intcountIndex = 0; countIndex < length; countIndex++) 
    { 
     WriteLine(newstring('*', countIndex)); 
    } 
}
for loop. Here, our code statement within the for loop block will executive repeatedly until the countIndex&lt; length expression evaluates to false.

foreach 循环

这有助于迭代数组元素或集合。它与for循环做的事情相同,但这可用于在不添加或删除集合项的情况下迭代通过集合。

让我们看一下以下代码片段:

private static void ForEachStatementExample() 
{ 
WriteLine("foreach loop example"); 
char[] vowels = {'a', 'e', 'i', 'o', 'u'}; 
WriteLine("foreach on Array."); 
foreach (var vowel in vowels) 
    { 
        WriteLine($"{vowel}"); 
    } 
WriteLine(); 
var persons = new List<Person> 
    { 
     new Author {Name = "Gaurav Aroraa"}, 
     new Reviewer {Name = "ShivprasadKoirala"}, 
     new TeamMember {Name = "Vikas Tiwari"}, 
     new TeamMember {Name = "Denim Pinto"} 
    }; 
WriteLine("foreach on collection"); 
foreach (var person in persons) 
    { 
        WriteLine($"{person.Name}"); 
    } 
}

上述代码是一个foreach语句的工作示例,打印一个人的名字。NamePerson对象集合中的一个属性。foreach块的语句会重复执行,直到表达式person in persons评估为 false。

跳转语句

跳转语句,顾名思义,是一种帮助将控制从一个部分移动到另一个部分的语句。这些是 C#中的主要跳转语句。

中断

这终止了for循环或switch语句的控制流。看下面的例子:

private static void BreakStatementExample() 
{ 
WriteLine("break statement example"); 
WriteLine("break in for loop"); 
for (int count = 0; count &lt; 50; count++ 
{ 
if (count == 8) 
   { 
    break; 
   } 
WriteLine($"{count}"); 
} 
WriteLine(); 
WriteLine("break in switch statement"); 
SwitchCaseExample(); 
} 

在上述代码中,当if表达式评估为 true 时,for循环的执行将中断。

继续

这有助于continue控制到循环的下一次迭代,并且它与whiledoforforeach循环一起使用。看下面的例子:

private static void ContinueStatementExample() 
{ 
WriteLine("continue statement example"); 
WriteLine("continue in for loop"); 
for (int count = 0; count &lt; 15; count++) 
{ 
if (count< 8) 
{ 
 continue; 
} 
 WriteLine($"{count}"); 
} 
} 

if表达式评估为 true 时,上述代码将绕过执行。

默认

这与switch语句和一个default块一起使用,确保如果在任何case块中找不到匹配项,则执行default块。有关更多详细信息,请参考switch...case语句。

异常处理语句

这具有处理程序中未知问题的能力,这被称为异常处理(我们将在第四天讨论异常处理)。

数组和字符串操作

数组和字符串在 C#编程中很重要。可能会有机会需要字符串操作或使用数组处理复杂数据。在本节中,我们将讨论数组和字符串。

数组

数组只是一个存储相同类型的固定大小顺序元素的数据结构。包含数据的数组元素基本上是变量,我们也可以称数组为相同类型变量的集合,这种类型通常称为元素类型。

数组是一块连续的内存。这个块存储了数组所需的一切,即元素、元素排名和数组的长度。第一个元素排名是 0,最后一个元素排名等于数组的总长度-1。

让我们考虑char[] vowels = {'a', 'e', 'i', 'o', 'u'};数组。一个大小为五的数组。每个元素都以顺序方式存储,并且可以使用其元素排名进行访问。以下是显示数组声明包含的内容的图示:

上图是我们用于表示元音字母数组声明的char 数据类型。这里,[ ]表示一个数组,并告诉 CLR 我们正在声明一个字符数组。元音是一个变量名,右侧表示包含数据的完整数组。

以下图显示了这个数组在内存中的样子:

在上图中,我们有一组连续的内存块。这也告诉我们,数组在内存中的最低地址对应于数组的第一个元素,而数组在内存中的最高地址对应于最后一个元素。

我们可以通过其排名(从 0 开始)检索元素的值。因此,在上述代码中,vowels[0]将给我们avowels[4]将给我们u

当我们谈论数组时,我们指的是引用类型,因为数组类型是引用类型。数组类型派生自System.Array,这是一个类。因此,所有数组类型都是引用类型。

或者,我们也可以使用for来获取值,它会迭代直到rankIndex < vowels.Length表达式评估为 false,for循环语句的代码块根据其排名打印数组元素:

private static void ArrayExample() 
{ 
WriteLine("Array example.\n"); 
char[] vowels = {'a', 'e', 'i', 'o', 'u'}; 
WriteLine("char[] vowels = {'a', 'e', 'i', 'o', 'u'};\n"); 
WriteLine("acces array using for loop"); 
for (intrankIndex = 0; rankIndex&lt;vowels.Length; rankIndex++) 
{ 
    Write($"{vowels[rankIndex]} "); 
} 
WriteLine(); 
WriteLine("acces array using foreach loop"); 
foreach (char vowel in vowels) 
    { 
        Write($"{vowel} "); 
    } 
} 

上述代码产生以下结果:

在前面的例子中,我们初始化了数组并为一个语句分配了数据,相当于char[] vowels = newchar[5];。在这里,我们告诉 CLR,我们正在创建一个名为 vowels 的 char 类型数组,最多有五个元素。或者,我们也可以声明相同的char[] vowels = newchar[5] { 'a', 'e', 'i', 'o', 'u' };

在本节中,我们将讨论不同类型的数组,并看看如何在不同的情况下使用数组。

数组类型

早些时候,我们讨论了数组是什么,以及如何声明数组。到目前为止,我们已经讨论了单维数组。考虑一个矩阵,我们有行和列。数组是数据以矩阵的形式排列的表示。然而,数组有更多类型,如此处所述。

单维数组

单维数组可以通过初始化数组类并设置大小来简单声明。以下是一个单维数组:

string[] cardinalDirections = {"North","East","South","West"}; 

多维数组

数组可以声明为多维的,这意味着你可以创建一个行和列的矩阵。多维数组可以是二维数组、三维数组或更多。创建一个典型的 2x2 大小的二维数组有不同的方法,意味着有两行和两列:

int[,] numbers = new int[2,2]; 
int[,] numbers = new int[2, 2] {{1,2},{3,4} }; 

以下是访问二维数组的代码片段:

int[,] numbers = new int[2, 2] {{1,2},{3,4} }; 
for (introwsIndex = 0; rowsIndex< 2; rowsIndex++) 
{ 
for (intcolIndex = 0; colIndex< 2; colIndex++) 
    { 
        WriteLine($"numbers[{rowsIndex},{colIndex}] = {numbers[rowsIndex, colIndex]}"); 
    } 
} 
for loops; the outer loop will work on rows and the inner loop will work on columns, and finally, we can get the element value using number[rowIndex][colIndex].

方阵是行和列相同的矩阵。通常被称为n乘以n矩阵。

这段代码产生以下结果:

交错数组

交错数组是数组的数组或数组中的数组。在交错数组中,数组的元素是一个数组。你也可以设置不同大小/维度的数组元素。交错数组的任何元素都可以有另一个数组。

典型的交错数组声明如下:

string[][,] collaborators = new string[5][,]; 

考虑以下代码片段:

WriteLine("Jagged array.\n"); 
string[][,] collaborators = new string[3][,] 
{ 
new[,] {{"Name", "ShivprasadKoirala"}, {"Age", "40"}}, 
new[,] {{"Name", "Gaurav Aroraa" }, {"Age", "43"}}, 
new[,] {{"Name", "Vikas Tiwari"}, {"Age", "28"}} 
}; 

for (int index = 0; index <collaborators.Length; index++) 
{ 
     for (introwIndex = 0; rowIndex< 2; rowIndex++) 
    { 
         for (intcolIndex = 0; colIndex< 2; colIndex++) 
        { 
            WriteLine($"collaborators[{index}][{rowIndex},
            {colIndex}] = {collaborators[index]  
            [rowIndex,colIndex]}"); 
        } 
    } 
} 

在前面的代码中,我们声明了一个包含两维数组的三个元素的交错数组。执行后,产生以下结果:

你也可以声明更复杂的数组以与更复杂的情况交互。你可以通过参考docs.microsoft.com/en-us/dotnet/api/system.array?view=netcore-2.0获取更多信息。

也可以创建隐式类型的数组。在隐式类型的数组中,数组类型是在数组初始化期间从元素中推断出来的,例如,var charArray = new[] {'a', 'e', 'i', 'o', 'u'};在这里我们声明了一个 char 数组。有关隐式类型数组的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/arrays/implicitly-typed-arrays

字符串

在 C#中,字符串只是表示 UTF-16 代码单元的字符数组,用于表示文本。

内存中字符串的最大大小为 2GB。

字符串对象的声明就像你声明任何变量一样简单,最常用的语句是:string authorName = "Gaurav Aroraa";

字符串对象被称为不可变,这意味着它是只读的。字符串对象的值在创建后无法修改。对字符串对象执行的每个操作都会返回一个新的字符串。由于字符串是不可变的,它们会导致巨大的性能损失,因为对字符串的每个操作都需要创建一个新的字符串。为了克服这一点,System.Text类中提供了StringBuilder对象。

有关字符串的更多信息,请参阅docs.microsoft.com/en-us/dotnet/api/system.string?view=netcore-2.0#Immutability

这些是声明字符串对象的替代方法:

private static void StringExample() 
{ 
WriteLine("String object creation"); 
string authorName = "Gaurav Aroraa"; //string literal assignment 
WriteLine($"{authorName}"); 
string property = "Name: "; 
string person = "Gaurav"; 
string personName = property + person; //string concatenation 
WriteLine($"{personName}"); 

char[] language = {'c', 's', 'h', 'a', 'r', 'p'}; 
stringstr Language = new string(language); //initializing the constructor 
WriteLine($"{strLanguage}"); 
string repeatMe = new string('*', 5); 
WriteLine($"{repeatMe}"); 
string[] members = {"Shivprasad", "Denim", "Vikas", "Gaurav"}; 
string name = string.Join(" ", members); 
WriteLine($"{name}"); 
} 

前面的代码片段告诉我们,声明可以如下进行:

  • 在声明字符串变量时进行字符串文字赋值

  • 在连接字符串时

  • 使用new进行构造函数初始化

  • 返回字符串的方法

有大量的字符串方法和格式化操作可用于字符串操作;请参考docs.microsoft.com/en-us/dotnet/api/system.string?view=netcore-2.0获取更多详细信息。

结构与类

与 C#中的类一样,结构也是由成员、函数等组成的数据结构。类是引用类型,但结构是值类型;因此,它们不需要堆分配,而是需要在堆栈上分配。

值类型数据将分配在堆栈上,引用类型数据将分配在堆上。在结构中使用的值类型存储在堆栈上,但当相同的值类型在数组中使用时,它存储在堆中。

有关堆和栈内存分配的更多详细信息,请参考www-ee.eng.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.8.htmlwww.codeproject.com/Articles/1058126/Memory-allocation-in-Net-Value-type-Reference-type

因此,当您创建struct类型的变量时,该变量直接存储数据,而不是引用,这与类的情况相同。在 C#中,struct关键字(请参阅 C#关键字部分以获取更多详细信息)有助于声明结构。结构有助于表示记录或当您需要呈现一些数据时。

看下面的例子:

public struct BookAuthor 
{ 
public string Name; 
public string BookTitle; 
public int Age; 
public string City; 
public string State; 
public string Country; 

   //Code omitted 
} 

在这里,我们有一个名为BookAuthor的结构,它表示书籍作者的数据。看一下正在使用这个结构的下面的代码:

private static void StructureExample() 
{ 
WriteLine("Structure example\n"); 
Write("Author name:"); 
var name = ReadLine(); 
Write("Book Title:"); 
var bookTitle = ReadLine(); 
Write("Author age:"); 
var age = ReadLine(); 
Write("Author city:"); 
var city = ReadLine(); 
Write("Author state:"); 
var state = ReadLine(); 
Write("Author country:"); 
var country = ReadLine(); 

BookAuthor author = new BookAuthor(name,bookTitle,Convert.ToInt32(age),city,state,country); 
WriteLine($"{author.ToString()}"); 
BookAuthor author1 = author; //copy structure, it will copy only data as this is //not a class 

Write("Change author name:"); 
var name1 = ReadLine(); 
author.Name = name1; 

WriteLine("Author1"); 
WriteLine($"{author.ToString()}"); 
WriteLine("Author2"); 
WriteLine($"{author1.ToString()}"); 
} 

这只是显示作者的详细信息。这里的重要一点是,一旦我们复制了结构,改变结构的任何字段都不会影响复制的内容;这是因为当我们复制时,只有数据被复制。如果您在类上执行相同的操作,那么会复制引用而不是复制数据。这个复制过程称为深复制和浅复制。请参考www.codeproject.com/Articles/28952/Shallow-Copy-vs-Deep-Copy-in-NET以了解有关浅复制与深复制的更多信息。

这是前面代码的结果:

现在,让我们尝试使用相同的操作来操作类;看一下下面消耗我们的类的代码:

private static void StructureExample() 
{ 
WriteLine("Structure example\n"); 
Write("Author name:"); 
var name = ReadLine(); 
Write("Book Title:"); 
var bookTitle = ReadLine(); 
Write("Author age:"); 
var age = ReadLine(); 
Write("Author city:"); 
var city = ReadLine(); 
Write("Author state:"); 
var state = ReadLine(); 
Write("Author country:"); 
var country = ReadLine(); 

ClassBookAuthor author = new ClassBookAuthor(name,bookTitle,Convert.ToInt32(age),city,state,country); 
WriteLine($"{author.ToString()}"); 
ClassBookAuthor author1 = author; //copy class, it will copy reference 

Write("Change author name:"); 
var name1 = ReadLine(); 
author.Name = name1; 

WriteLine("Author1"); 
WriteLine($"{author.ToString()}"); 
WriteLine("Author2"); 
WriteLine($"{author1.ToString()}"); 
} 

现在我们的类变量都将具有相同的值。下面的屏幕截图显示了结果:

结构和类是不同的:

  • 结构是值类型,而类是引用类型。

  • 类支持单继承(可以使用接口实现多继承),但结构不支持继承。

  • 类有一个隐式默认构造函数,但结构没有默认构造函数。

结构的更多功能我们在这里没有讨论。请参考docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/structs获取有关结构的更多内部信息。

动手练习

让我们通过解决以下问题来回顾今天的学习 - 也就是第二天:

  1. 编写一个简短的程序来演示我们可以在不同的命名空间中使用相同的类名。

  2. 定义console类。通过修改书中讨论的代码示例,编写一个console程序来显示所有可用的颜色,以便所有元音显示为绿色,所有辅音显示为蓝色。

  3. 详细说明 C#保留关键字。

  4. 用示例描述 C#关键字的不同类别。

  5. 创建一个小程序来演示isas运算符。

  6. 编写一个简短的程序,使用上下文关键字展示查询表达式。

  7. 编写一个简短的程序来展示thisbase关键字的重要性。

  8. 用一个简短的程序来定义装箱和拆箱。

  9. 编写一个简短的程序来证明指针类型变量存储的是另一个变量的内存而不是数据。

  10. 编写一个简短的程序来展示运算符优先级顺序。

  11. 什么是运算符重载?编写一个简短的程序来展示运算符重载的实际应用。

  12. 哪些运算符不能被重载,为什么?

  13. 用一个简短的程序来定义类型转换。

  14. 编写一个简短的程序,使用所有可用的内置 C#类型,并使用转换方法进行转换(可以使用var result = Convert.ToInt32(5689.25);实现从十进制到整数的转换)。

  15. 定义 C#语句。

  16. 编写一个程序来详细说明每个语句类别。

  17. 什么是jump语句?编写一个小程序来展示所有jump语句。

  18. 在 C#中什么是数组?

  19. 编写一个程序来证明数组是一块连续的内存块。

  20. 参考System.Array类(docs.microsoft.com/en-us/dotnet/api/system.array?view=netcore-2.0)并编写一个简短的程序。

  21. 将数组作为参数传递给一个方法。

  22. 对数组进行排序。

  23. 复制数组。

  24. 参考System.String类,并通过一个简短的程序探索它的所有方法和属性。

  25. 字符串对象是如何不可变的?写一个简短的程序来展示这一点。

  26. 什么是字符串构建器?

  27. 什么是类?

  28. 什么是结构?

  29. 编写一个小程序,展示structclass之间的区别。

  30. 解释编译时类型和运行时类型。

  31. 编写一个程序来展示编译时类型和运行时类型的区别。

  32. 编写一个简短的程序来证明,显式类型转换会导致数据丢失。

重温第二天

因此,我们结束了我们七天学习系列的第二天。今天,我们从一个非常简单的 C#程序开始,然后通过了它的所有部分(一个典型的 C#程序)。然后,我们讨论了关于 C#保留关键字和访问器的一切,我们也了解了什么是上下文关键字。

我们讨论了 C#中各种可用数据类型的类型转换和类型转换。您学会了装箱和拆箱的过程,以及如何使用内置方法进行转换。

我们还了解了各种语句,并学习了各种语句的用法和流程,比如forforeachwhiledo。我们通过代码示例了解了条件语句,比如ifif...elseif...elseif...else以及switch

然后,我们通过代码示例了解了数组,并了解了它们,包括字符串操作。

最后,我们通过讨论结构和类来结束了今天的学习。我们看了这两者的不同之处。

明天,第三天,我们将讨论 C#语言的所有新特性,并通过代码示例讨论它们的用法和功能。

第三章:第三天 - C#中的新功能

今天,我们将学习 C#语言的当前版本中非常新的和新发布的功能,即 C# 7.0(这是本书审阅中最新的适应版本)。其中一些元素是全新的,而其他一些在过去的版本中已经存在,并在当前版本中得到了升级。C# 7.0 将带来许多新功能。其中一些元素,比如元组,是已经存在的概念的扩展,而其他一些是全新的。以下是我们将在第三天学习的基本元素:

  • 元组和解构

  • 模式匹配

  • 本地函数

  • 文字改进

  • 异步主函数

  • 默认表达式

  • 推断元组名称

元组和解构

元组并不是在当前版本中新引入的,而是在.NET 4.0 发布时引入的。在当前版本中,它们得到了改进。

元组

每当特定情况需要从方法返回多个值时,就会使用元组。例如,假设我们需要从给定数字系列中找到奇数和偶数。

元组是一个包含相关数据的不可变数据值。元组用于聚合相关数据,例如一个人的姓名、年龄、性别以及任何你想要的数据作为输入。

为了完成这个问题,我们的方法应该返回或提供一个数字,并说明这是一个奇数还是偶数。对于将返回这些多个值的方法,我们可以使用自定义数据类型、动态返回类型或输出参数,这有时会让开发人员感到困惑。

要使用元组,您需要添加 NuGet 包:

www.nuget.org/packages/System.ValueTuple/

对于这个问题,我们有一个元组对象,在 C# 7.0 中我们有两种不同的东西,元组类型和元组文字,用于从方法返回多个值。

让我们使用一个代码示例详细讨论元组。考虑以下代码片段:

public static (int, string) FindOddEvenBySingleNumber(int number) 
{ 
   string oddOrEven = IsOddNumber(number) ? "Odd" :"Even"; 
   return (number, oddOrEven);//tuple literal 
} 
FindOddEvenBySingleNumber is returning multiple values, which tells us whether a number is odd or even. See the return statement return (number, oddOrEven) of the preceding code: here, we are simply returning two different variables. Now, how are these values accessible from the caller method? In this case, we are returning a tuple value and the caller method will receive a tuple with these values, which are nothing but elements or items of a tuple. In this case, the number will be available as Item1 and oddOrEven as Item2 for the caller method. The following is from the caller method:
var result = OddEven.FindOddEvenBySingleNumber(Convert.ToInt32(number); 
Console.WriteLine($"Number:{result.Item1} is {result.Item2}"); 
result.Item1 represents number and result.Item2 represents oddOrEven. This is fine when someone knows the representation of these tuple items/elements. But think of a scenario where we have numerous tuple elements and the developer who is writing the caller method is not aware of the representation of these items/elements. In that case, it is bit complex to consume these tuple items/elements. To overcome this problem, we can give a name to these tuple items. We call these named tuple items/elements. Let us modify our method FindOddEvenBySingleNumber to return named tuple items:
public static (int number, string oddOrEvent) FindOddEvenBySingleNumber (int number) 
{ 
   string result = IsOddNumber(number) ? "Odd" : "Even"; 
   return (number:number, oddOrEvent: result);//returning
   named tuple element in tuple literal 
} 

在上述代码片段中,我们为元组添加了更具描述性的名称。现在调用方法可以直接使用这些名称,如下面的代码片段所示:

var result = OddEven.FindOddEvenBySingle(Convert.ToInt32(number)); 
Console.WriteLine($"Number:{result.number} is {result.oddOrEvent}"); 

通过为元组添加一些描述性名称,我们可以在调用方法中轻松识别和使用元组的项目/元素。

System.ValueTuple 结构

在 C# 7.0 中,元组需要 NuGet 包System.ValueType。这实际上是一个结构。它包含一些静态和公共方法来进行操作:

  • CompareTo(ValueTuple):比较ValueTuple实例的公共方法。如果比较成功,则该方法返回 0,否则返回 1。

  • 这里有两个方法展示了CompareTo方法的强大之处:

public static bool CompareToTuple(int number) 
{ 
   var oddEvenValueTuple =
   FindOddEvenBySingleNumber(number); 
   var differentTupleValue =
   FindOddEvenBySingleNumberNamedElement(number + 1); 
   var res =
   oddEvenValueTuple.CompareTo(differentTupleValue); 
   return res == 0; // 0 if other is a ValueTuple instance
   and 1 if other is null 
} 
public static bool CompareToTuple1(int number) 
{ 
    var oddEvenValueTuple =
    FindOddEvenBySingleNumber(number); 
    var sameTupleValue =
    FindOddEvenBySingleNumberNamedElement(number); 
    var res = oddEvenValueTuple.CompareTo(sameTupleValue); 
    return res == 0;// 0 if other is a ValueTuple instance
    and 1 if other is null 
} 

以下是调用代码片段,用于从上述代码中获取结果:

Console.Clear(); 
Console.Write("Enter number: "); 
var num = Console.ReadLine(); 
var resultNum = OddEven.FindOddEvenBySingleNumberNamedElement(Convert.ToInt32(num)); 
Console.WriteLine($"Number:{resultNum.number} is {resultNum.oddOrEven}."); 
Console.WriteLine(); 
var comp = OddEven.CompareToTuple(Convert.ToInt32(num)); 
Console.WriteLine($"Comparison of two Tuple objects having different value is:{comp}"); 
var comp1 = OddEven.CompareToTuple1(Convert.ToInt32(num)); 
Console.WriteLine($"Comparison of two Tuple objects having same value is:{comp1}"); 

当我们执行上述代码时,将会得到以下输出:

  • Equals(Object):返回 true/false 的公共方法,指示TupleValue实例是否等于提供的对象。如果成功则返回 true。

  • 以下是实现:

public static bool EqualToTuple(int number) 
{ 
   var oddEvenValueTuple =
   FindOddEvenBySingleNumber(number); 
   var sameTupleValue =
   FindOddEvenBySingleNumberNamedElement(number); 
   var res = oddEvenValueTuple.Equals(sameTupleValue); 
   return res;//true if obj is a ValueTuple instance;
   otherwise, false. 
} 

以下是调用方法的代码片段:

var num1 = Console.ReadLine(); 
var namedElement = OddEven.FindOddEvenBySingleNumberNamedElement(Convert.ToInt32(num1)); 
Console.WriteLine($"Number:{namedElement.number} is {namedElement.oddOrEven}."); 
Console.WriteLine(); 
var equalToTuple = OddEven.EqualToTuple(Convert.ToInt32(num1)); 
Console.WriteLine($"Equality of two Tuple objects is:{equalToTuple}"); 
var equalToObject = OddEven.EqualToObject(Convert.ToInt32(num1)); 
Console.WriteLine($"Equality of one Tuple object with other non tuple object is:{equalToObject}"); 

最后,输出如下:

  • Equals(ValueTuple):始终返回 true 的公共方法,这是设计上的。它是这样设计的,因为ValueTuple是一个零元素元组,因此当两个 ValueTuples 执行相等操作时,由于没有元素,将始终返回零。

  • GetHashCode():返回对象的哈希码的公共方法。

  • GetType():提供当前实例的具体类型的公共方法。

  • ToString():是ValueTuple实例的字符串表示形式的公共方法。然而,根据设计,它总是返回零。

  • Create():创建一个新的ValueTuple(0 元组)的静态方法。我们可以按以下方式创建 0 元组:

public static ValueTuple CreateValueTuple() => ValueTuple.Create();
  • Create(T1) ... Create<T1, T2, T3, T4, T5, T6, T7, T8>(T1, T2, T3, T4, T5, T6, T7, T8):所有这些都是创建具有 1 个组件(单例)到 8 个组件(八元组)的 Value Tuples 的静态方法。

  • 请参见以下代码片段,显示了单例和八元组示例:

public static ValueTuple<int> CreateValueTupleSingleton(int number) => ValueTuple.Create(number); 
public static ValueTuple<int, int, int, int, int, int, int, ValueTuple<int,string>> OctupleUsingCreate() => ValueTuple.Create(1, 2, 3, 4, 5, 6, 7, ValueTuple.Create(8, IsOddNumber(8) ? "Odd" : "Even")); 

如果出现编译警告,您需要将 NuGet 包更新到 Microsoft.Net.Compilers 2.0 预览版。要这样做,只需选择预览并从 NuGet 包管理器中搜索 Microsoft.Net.Compilers 到 2.0[www.nuget.org/packages/Microsoft.Net.Compilers/]。

解构

在前面的部分中,我们看到了使用ValueTuple的多个返回值可以通过其项/元素进行访问。现在想象一种情况,我们想要直接将这些元素值分配给变量。在这里,解构帮助了我们。解构是一种我们可以拆开方法返回的元组的方式。

主要有两种方法可以解构元组:

  • 显式声明类型:我们明确声明每个字段的类型。让我们看下面的代码示例:
public static string ExplicitlyTypedDeconstruction(int num) 
{ 
   (int number, string evenOdd) =
   FindOddEvenBySingleNumber(num); 
   return $"Entered number:{number} is {evenOdd}."; 
} 
  • 隐式声明类型:我们隐式声明每个字段的类型。让我们看下面的代码示例:
public static string ImplicitlyTypedDeconstruction(int num) 
{ 
   var (number, evenOdd) =
   FindOddEvenBySingleNumber(num); 
   //Following deconstruct is also valid 
   //(int number, var evenOdd) =
   FindOddEvenBySingleNumber(num); 
   return $"Entered number:{number} is {evenOdd}."; 
} 

我们还可以通过使用 out 参数实现解构 UserDefined/Custom 类型;请参阅以下代码示例:

public static string UserDefinedTypeDeconstruction(int num) 
{ 
   var customModel = new UserDefinedModel(num,
   IsOddNumber(num) ? "Odd" : "Even"); 
   var (number,oddEven) = customModel; 
   return $"Entered number:{number} is {oddEven}."; 
} 

在上面的代码中,deconstruct 方法使得从UserDefinedModel到一个 int 和一个 string 的赋值成为可能,它们分别代表了属性numberOddEven

元组 - 需要记住的重要要点

在前面的部分中,我们讨论了元组,并注意到它们如何在需要多个值和复杂数据值(除了自定义类型)的情况下帮助我们。以下是我们在使用元组时应该记住的重要要点:

  • 使用元组,我们需要 NuGet 包System.ValueTuple

  • ValueTuple (System.ValueTuple)是一个结构,而不是一个类。

  • ValueTuple实现了IEquatable<ValueTuple>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple>接口。

  • ValueTuples 是可变的。

  • ValueTuples 是灵活的数据容器,可以是未命名的,也可以是命名的:

  • 未命名:当我们不为字段提供任何名称时,这些是未命名的元组,并且可以使用默认字段Item1Item2等进行访问:

var oddNumber = (3, "Odd"); //Unnamed tuple 
    • 命名:当我们为字段明确提供一些描述性名称时:
var oddNumber = (number: 3, oddOrEven: "Odd"); //Named Tuple 
  • 赋值:当我们将一个元组分配给另一个元组时,只有值被分配,而字段名称不会:
Console.Write("Enter first number: "); 
var userInputFirst = Console.ReadLine(); 
Console.Write("Enter second number: "); 
var userInputSecond = Console.ReadLine(); 
var noNamed = OddEven.FindOddEvenBySingleNumber(Convert.ToInt32(userInputFirst)); 
var named = OddEven.FindOddEvenBySingleNumberNamedElement(Convert.ToInt32(userInputSecond)); 
Console.WriteLine($"First Number:{noNamed.Item1} is {noNamed.Item2} using noNamed tuple."); 
Console.WriteLine($"Second Number:{named.number} is {named.oddOrEven} using Named tuple."); 

Console.WriteLine("Assigning 'Named' to 'NoNamed'"); 
                        noNamed = named; 
Console.WriteLine($"Number:{noNamed.Item1} is {named.Item2} after assignment."); 
Console.Write("Enter third number: "); 
var userInputThird = Console.ReadLine(); 
var noNamed2 = OddEven.FindOddEvenBySingleNumber(Convert.ToInt32(userInputThird)); 
Console.WriteLine($"Third Number:{noNamed2.Item1} is {noNamed2.Item2} using second noNamed tuple."); 
Console.WriteLine("Assigning 'second NoNamed' to 'Named'"); 
named = noNamed2; 
Console.WriteLine($"Second Number:{named.number} is {named.oddOrEven} after assignment."); 

前面代码片段的输出将如下所示:

在前面的代码片段中,我们可以看到分配的元组的输出与分配的元组相同。

模式匹配

一般来说,模式匹配是一种在表达式中比较预定义格式的内容的方法。格式实际上是不同匹配的组合。

在 C# 7.0 中,模式匹配是一个特性。通过使用这个特性,我们可以在对象的类型之外实现方法分派。

模式匹配支持各种表达式;让我们通过代码示例讨论这些。

模式可以是常量模式:类型模式或变量模式。

is 表达式

is表达式使得可以检查对象及其属性,并确定它是否满足模式:

public static string MatchingPatterUsingIs(object character) 
{ 
   if (character is null) 
   return $"{nameof(character)} is null. "; 
   if (character is char) 
   { 
      var isVowel = IsVowel((char) character) ? "is a
      vowel" : "is a consonent"; 
      return $"{character} is char and {isVowel}. "; 
   } 
   if (character is string) 
   { 
      var chars = ((string) character).ToArray(); 
      var stringBuilder = new StringBuilder(); 
      foreach (var c in chars) 
      { 
         if (!char.IsWhiteSpace(c)) 
         { 
         var isVowel = IsVowel(c) ? "is a vowel" : "is a
         consonent"; 
         stringBuilder.AppendLine($"{c} is char of string
         '{character}' and {isVowel}."); 
         } 
       } 

       return stringBuilder.ToString(); 
     } 
     throw new ArgumentException(
     "character is not a recognized data type.", 
     nameof(character)); 
} 

前面的代码没有展示任何花哨的东西,只是告诉我们输入参数是否是特定类型和元音或辅音。在这里我们简单地使用is运算符,告诉对象是否是相同类型。

is运算符(goo.gl/79sLW5)检查对象,如果对象是相同类型,则返回 true;如果不是,则返回 false。

在前面的代码中,当我们检查对象是否为字符串时,我们需要显式地将对象转换为字符串,然后将其传递给我们的实用方法IsVowel()。在前面的代码中,我们正在做两件事:第一是检查传入参数的类型,如果类型相同,我们就将其转换为所需的类型,并根据我们的情况执行操作。有时,当我们需要使用表达式编写更复杂的逻辑时,这会造成混淆。

C# 7.0 以微妙的方式解决了这个问题,使我们的表达式更简单。现在我们可以在表达式中直接声明一个变量,同时检查类型;请参阅以下代码:

if (character is string str) 
{ 
    var chars = str.ToArray(); 
    var stringBuilder = new StringBuilder(); 
    foreach (var c in chars) 
    { 
        if (!char.IsWhiteSpace(c)) 
        { 
            var isVowel = IsVowel(c) ? "is a vowel" : "is
            a consonent"; 
            stringBuilder.AppendLine($"{c} is char of
            string '{character}' and {isVowel}."); 
        } 
    } 

    return stringBuilder.ToString(); 
} 

在前面的代码中,更新了is表达式,它既测试变量又将其分配给所需类型的新变量。有了这个改变,就不需要像在以前的代码中那样显式地转换类型((string) character)

让我们在前面的代码中添加一个条件:

if (character is int number) 
return $"{nameof(character)} is int {number}."; 

在前面的代码中,我们正在检查对象是否为int,这是一个结构。前面的条件完全正常,并产生了预期的结果。

以下是我们完整的代码:

private static IEnumerable<char> Vowels => new[] {'a', 'e', 'i', 'o', 'u'}; 

public static string MatchingPatterUsingIs(object character) 
{ 
    if (character is null) 
    return $"{nameof(character)} is null. "; 
    if (character is char) 
    { 
        var isVowel = IsVowel((char) character) ? "is a 
        vowel" : "is a consonent"; 
        return $"{character} is char and {isVowel}. "; 
    } 
    if (character is string str) 
    { 
        var chars = str.ToArray(); 
        var stringBuilder = new StringBuilder(); 
        foreach (var c in chars) 
        { 
            if (!char.IsWhiteSpace(c)) 
            { 
                var isVowel = IsVowel(c) ? "is a vowel" :
                "is a consonent"; 
                stringBuilder.AppendLine($"{c} is char of
                string '{character}' and {isVo 
            } 
        } 

        return stringBuilder.ToString(); 
    } 

    if (character is int number) 
    return $"{nameof(character)} is int {number}."; 

    throw new ArgumentException( 
    "character is not a recognized data type.", 
    nameof(character)); 
} 

private static bool IsVowel(char character) => Vowels.Contains(char.ToLower(character));

is表达式可以很好地处理值类型和引用类型。

在前面的代码示例中,只有当相应的表达式匹配结果为true时,变量strnumber才会被赋值。

switch 语句

我们已经在第 2 天讨论了switch语句。switch模式非常有用,因为它可以使用任何数据类型进行匹配,此外case提供了一种方式,因此它匹配了条件。

match表达式是相同的,但在 C# 7.0 中,这个特性以三种不同的方式得到了增强。让我们使用代码示例来理解它们。

常量模式

在 C#的早期版本中,switch语句只支持常量模式,在这种模式中,我们在switch中评估一些变量,然后根据常量情况进行条件调用。请参阅以下代码示例,我们试图检查inputChar是否具有特定长度,这是在switch中计算的:

public static string ConstantPatternUsingSwitch(params char[] inputChar) 
{ 
    switch (inputChar.Length) 
    { 

        case 0: 
            return $"{nameof(inputChar)} contains no
            elements."; 
        case 1: 
            return $"'{inputChar[0]}' and
            {VowelOrConsonent(inputChar[0])}."; 
        case 2: 
            var sb = new
            StringBuilder().AppendLine($"'{inputChar[0]}'
            and {VowelOrConsonent(inputChar[0])}."); 
            sb.AppendLine($"'{inputChar[1]}' and
            {VowelOrConsonent(inputChar[1])}."); 
            return sb.ToString(); 
        case 3: 
            var sb1 = new
            StringBuilder().AppendLine($"'{inputChar[0]}'
            and {VowelOrConsonent(inputChar[0])}."); 
            sb1.AppendLine($"'{inputChar[1]}' and
            {VowelOrConsonent(inputChar[1])}."); 
            sb1.AppendLine($"'{inputChar[2]}' and
            {VowelOrConsonent(inputChar[2])}."); 
            return sb1.ToString(); 
        case 4: 
            var sb2 = new
            StringBuilder().AppendLine($"'{inputChar[0]}'
            and {VowelOrConsonent(inputChar[0])}."); 
            sb2.AppendLine($"'{inputChar[1]}' and
            {VowelOrConsonent(inputChar[1])}."); 
            sb2.AppendLine($"'{inputChar[2]}' and
            {VowelOrConsonent(inputChar[2])}."); 
            sb2.AppendLine($"'{inputChar[3]}' and
            {VowelOrConsonent(inputChar[3])}."); 
            return sb2.ToString(); 
        case 5: 
            var sb3 = new
            StringBuilder().AppendLine($"'{inputChar[0]}'
            and {VowelOrConsonent(inputChar[0])}."); 
            sb3.AppendLine($"'{inputChar[1]}' and
            {VowelOrConsonent(inputChar[1])}."); 
            sb3.AppendLine($"'{inputChar[2]}' and
            {VowelOrConsonent(inputChar[2])}."); 
            sb3.AppendLine($"'{inputChar[3]}' and
            {VowelOrConsonent(inputChar[3])}."); 
            sb3.AppendLine($"'{inputChar[4]}' and
            {VowelOrConsonent(inputChar[4])}."); 
            return sb3.ToString(); 
            default: 
            return $"{inputChar.Length} exceeds from
            maximum input length."; 
    } 
} 

在前面的代码中,我们的主要任务是检查inputChar是元音还是辅音,我们在这里要做的是首先评估inputChar的长度,然后根据需要执行操作,这会导致更复杂条件的更多工作/代码。

类型模式

通过引入类型模式,我们可以克服我们在常量模式(在前一节中)中遇到的问题。请考虑以下代码:

public static string TypePatternUsingSwitch(IEnumerable<object> inputObjects) 
{ 
    var message = new StringBuilder(); 
    foreach (var inputObject in inputObjects) 
    switch (inputObject) 
        { 
            case char c: 
                message.AppendLine($"{c} is char and
                {VowelOrConsonent(c)}."); 
                break; 
            case IEnumerable<object> listObjects: 
                foreach (var listObject in listObjects)

                message.AppendLine(MatchingPatterUsingIs(
                listObject)); 
                break; 
            case null: 
                break; 
        } 
    return message.ToString(); 
} 

在前面的代码中,现在根据类型模式执行操作变得更容易。

case表达式中的when子句

通过在case表达式中引入when子句,您可以在表达式中执行特殊操作;请参阅以下代码:

public static string TypePatternWhenInCaseUsingSwitch(IEnumerable<object> inputObjects) 
{ 
    var message = new StringBuilder(); 
    foreach (var inputObject in inputObjects) 
    switch (inputObject) 
        { 
            case char c: 
                message.AppendLine($"{c} is char and
                {VowelOrConsonent(c)}."); 
                break; 
            case IEnumerable<object> listObjects when
                listObjects.Any(): 
                foreach (var listObject in listObjects) 
                message.AppendLine(MatchingPatterUsingIs
                (listObject)); 
                break; 
            case IEnumerable<object> listInlist: 
                break; 
            case null: 
                break; 
        } 
    return message.ToString(); 
} 

在前面的代码中,casewhen确保只有在listObjects有一些值时才执行操作。

case语句要求每个case都以breakreturngoto结束。

本地函数

在之前的版本中,可以使用匿名方法来实现函数和动作,但仍然存在一些限制:

  • 泛型

  • refout参数

  • params

本地函数的特点是在块范围内声明。这些函数非常强大,并且具有与任何其他普通函数相同的功能,但不同之处在于它们在声明它们的块内部范围内。

考虑以下代码示例:

public static string FindOddEvenBySingleNumber(int number) => IsOddNumber(number) ? "Odd" : "Even";

在前面的代码中,方法FindOddEvenBySingleNumber()只是简单地返回大于 1 的数字为奇数偶数。这使用了一个私有方法IsOddNumber(),如下所示:

private static bool IsOddNumber(int number) => number >= 1 && number % 2 != 0; 

方法IsOddNumber()是一个私有方法,只能在声明它的类中使用。因此,它的范围在类内部,而不是在代码块内部。

让我们看一个本地函数的代码示例:

public string FindOddEvenBySingleNumberUsingLocalFunction(int someInput) 
{ 
    //Local function, scoped within
    FindOddEvenBySingleNumberUsingLocalFunction 
    bool IsOddNumber(int number) 
    { 
        return number >= 1 && number % 2 != 0; 
    } 

    return IsOddNumber(someInput) ? "Odd" : "Even"; 
} 

在前面的代码中,本地函数IsOddNumber()执行的操作与上一节中的private方法相同。但是在这里,IsOddNumber()的范围在方法FindOddEvenBySingleNumberUsingLocalFunction()内。因此,它在此代码块之外将不可用。

文字改进

在使用文字时,我们可以考虑声明各种变量常量,有时这些变量对于方法的生命周期非常重要,因为这些变量对于方法或做出任何决定非常重要。并且会导致错误的决策,因为对数字常量的误读。为了克服这种混淆,C# 7.0 引入了两个新功能,二进制文字和数字分隔符。

二进制文字

二进制数字对于执行复杂操作非常重要。二进制数字的常量可以声明为0b,其中 0b 告诉我们这是一个二进制文字,而二进制值是您的十进制数字的值。以下是一些例子:

//Binary literals
public const int Nineteen = 0b00010011; 
public const int Ten = 0b00001010; 
public const int Four = 0b0100; 
public const int Eight = 0b1000; 

数字分隔符

引入了数字分隔符后,我们可以轻松阅读长数字、二进制数字。数字分隔符可以用于数字和二进制数字。对于二进制数字,数字分隔符,即下划线(_),适用于位模式,对于数字,它可以出现在任何地方,但最好将 1,000 作为分隔符。看一下以下例子:

//Digit separator - Binary numbers 
public const int Hundred = 0b0110_0100; 
public const int Fifty = 0b0011_0010; 
public const int Twenty = 0b0001_0100; 
//Numeric separator 
public const long Billion = 100_000_0000; 

数字分隔符也可以用于十进制、浮点和双精度类型。

以下是作为 C# 7.1 语言特性随 Visual Studio 2017 更新 3 一起发布的新功能,我们将根据:[`github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md`](https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md)讨论所有功能

有关 Visual Studio 2017 新版本的更多信息,请参考:www.visualstudio.com/en-us/news/releasenotes/vs2017-relnotes

如果您想了解如何设置您现有的项目或使用 C# 7.0 的新项目 - 那么您不用担心,Visual Studio 2017 更新 3 会帮助您。每当您开始使用 C# 7.1 的新功能时,您需要遵循以下步骤:

  1. Visual Studio 将警告现有版本的支持,并建议升级您的项目,如果您想使用 C# 7.1 的新功能。

  2. 只需点击黄色灯泡,选择最适合您需求的选项,您就可以使用新的 C# 7.1 了。

以下图片告诉您准备好 C# 7.1 的两个步骤:

让我们开始讨论 C# 7.1 语言的新功能:

异步主函数

C# 7.1 的一项新功能,使应用程序的入口点即Main成为可能。异步主函数使main方法可以等待,这意味着Main方法现在是异步的,可以获得TaskTask<int>。有了这个功能,以下是有效的入口点:

static Task Main()
{
    //stuff goes here
}
static Task<int> Main()
{
    //stuff goes here
}
static Task Main(string[] args)
{
    //stuff goes here
}
static Task<int> Main(string[] args)
{
    //stuff goes here
}

在使用新签名时有一些限制。

  • 您可以使用这些新签名的入口点,如果没有先前签名的重载存在,那么这些标记为有效,这意味着如果您使用现有的入口点。
public static void Main()
{
    NewMain().GetAwaiter().GetResult();
}
private static async Task NewMain()
{
    //async stuff goes here
}
  • 这并不是强制将入口点标记为异步的,这意味着您仍然可以使用现有的异步入口点:
private static void Main(string[] args)
{
    //stuff goes here
}

可能会有更多的入口点用法可以整合到应用程序中 - 请参考此功能的官方文档:github.com/dotnet/csharplang/blob/master/proposals/async-main.md

默认表达式

C# 7.1 引入了一个新的表达式,即默认文字。引入这个新文字后,表达式可以隐式转换为任何类型,并产生类型的默认值。

新的默认文字与旧的default(T)不同。早期的default转换了T的目标类型,但新的可以转换任何类型。

default:
//Code removed
case 8:
    Clear();
    WriteLine("C# 7.1 feature: default expression");
    int thisIsANewDefault = default;
    var thisIsAnOlderDefault = default(int);
    WriteLine($"New default:{thisIsANewDefault}. Old
    default:{thisIsAnOlderDefault}");
    PressAnyKey();
    break;
//Code removed

在上述代码中,当我们写int thisIsANewDefault = default;时,这是 C# 7.1 中有效的表达式,它隐式地将表达式转换为 int 类型,并将默认值 0(零)赋给thisIsANewDefault。这里值得注意的是,默认文字隐式地检测thisIsANewDefault的类型并设置值。另一方面,我们需要明确告诉目标类型在表达式var thisIsAnOlderDefault = default(int);中设置默认值。

上述代码生成以下输出:

有多种实现新的默认文字,因此,您可以在以下情况下使用相同的:

成员变量

新的default表达式可以用于为变量分配默认值,以下是各种方式:

int thisIsANewDefault = default;
int thisIsAnOlderDefault = default(int);
var thisIsAnOlderDefaultAndStillValid = default(int);
var thisIsNotValid = default; //Not valid, as we cannot assign default to implicit-typed variable

常量

与变量类似,使用 default 我们可以声明常量,以下是各种方式:

const int thisIsANewDefaultConst = default; //valid
const int thisIsAnOlderDefaultCont = default(int); //valid
const int? thisIsInvalid = default; //Invalid, as nullable cannot be declared const

还有更多的情景可以使用这个新的默认文字,比如方法中的可选参数,更多信息请参考:github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-03-07.md

推断元组名称

随着这个新功能的引入,您不需要显式声明元组候选名称。我们在之前的Tuples and Deconstructions部分讨论了元组。推断元组名称功能是 C# 7.0 引入的元组值的扩展。

要使用这个新功能,您需要更新之前在Tuple部分安装的ValueTuple的 NuGet 包。要更新 NuGet 包,转到NuGet 包管理器,点击更新选项卡,然后点击更新最新版本。以下截图提供了完整信息:

以下代码片段显示了声明元组的各种方式:

public static void InferTupleNames(int num1, int num2)
{
    (int, int) noNamed = (num1, num2);
    (int, int) IgnoredName = (A:num1, B:num2);
    (int a, int b) typeNamed = (num1, num2);
    var named = (num1, num2);
    var noNamedVariation = (num1, num1);
    var explicitNaming = (n: num1, num1);
    var partialnamed = (num1, 5);
}

上述代码是不言自明的,元组noNamed没有任何成员名称,可以使用item1item2进行访问。同样,在元组IgnoredName中,所有定义的成员名称将被忽略,因为声明中没有定义成员名称。以下代码片段讲述了如何访问各种元组的完整故事:

public static void InferTupleNames(int num1, int num2)
{
    (int, int) noNamed = (num1, num2);
    Console.WriteLine($"NoNamed:{noNamed.Item1},
    {noNamed.Item2}");
    (int, int) ignoredName = (A:num1, B:num2);
    Console.WriteLine($"IgnoredName:{ignoredName.Item1}
    ,{ignoredName.Item2}");
    (int a, int b) typeNamed = (num1, num2);
    Console.WriteLine($"typeNamed using default member-
    names:{typeNamed.Item1}
    {typeNamed.Item2}");
    Console.WriteLine($"typeNamed:{typeNamed.a},
    {typeNamed.b}");
    var named = (num1, num2);
    Console.WriteLine($"named using default member-names
    :{named.Item1},{named.Item2}");
    Console.WriteLine($"named:{named.num1},{named.num2}");
    var noNamedVariation = (num1, num1);
    Console.WriteLine($"noNamedVariation:
    {noNamedVariation.Item1},{noNamedVariation.Item2}");
    var explicitNaming = (n: num1, num1);
    Console.WriteLine($"explicitNaming:{explicitNaming.n},
    {explicitNaming.num1}");
    var partialnamed = (num1, 5);
    Console.WriteLine($"partialnamed:{partialnamed.num1},
    {partialnamed.Item2}");
}

上述代码产生以下输出:

还有更多变化可以使用这个新功能,更多信息,请参考:github.com/dotnet/roslyn/blob/master/docs/features/tuples.md

其他预计发布的功能

在最终发布的 C# 7.1 编程语言中将会有更多功能,除了之前的功能外,以下是遇到 bug 或部分实现的功能。

泛型模式匹配

该模式匹配与泛型提议在这里:github.com/dotnet/csharplang/blob/master/proposals/generics-pattern-match.md 作为 C# 7.1 的新功能,遇到了一个 bug,可以在这里看到:github.com/dotnet/roslyn/issues/16195

该功能的实现将基于as运算符,详细信息请参见:github.com/dotnet/csharplang/blob/master/spec/expressions.md#the-as-operator

引用程序集

引用程序集功能尚未纳入 IDE 中,您可以参考:github.com/dotnet/roslyn/blob/master/docs/features/refout.md 这里获取更多详细信息。

动手练习

回答以下问题,涵盖了今天学习的概念:

  1. 什么是ValueTuple类型?

  2. ValueTuples 是可变的;通过示例证明。

  3. 创建一个包含 10 个元素的ValueTuple

  4. 创建一个如下所示的用户定义类 employee,然后编写一个程序来解构用户定义的类型:

public class employee
{
public Guid EmplId { get; set; }
public String First { get; set; }
public string Last { get; set; }
public char Sex { get; set; }
public string DepartmentId { get; set; }
public string Designation { get; set; }
}
  1. 创建一个使用数字分隔符的各种常量的类,并将这些常量实现到函数ToDecimal()ToBinary()中。

  2. 本地函数是什么?它们与私有函数有什么不同?

  3. 使用通用本地函数重写OddEven程序。

  4. 使用switch语句中的类型模式重写OddEven程序。

  5. 编写一个程序,利用 C# 7.1 语言的推断元组名称特性来找出OddEven

  6. 默认表达式(C# 7.1)是什么,通过程序详细说明?

重温第 03 天

今天,我们讨论了 C# 7.0 中引入的所有新功能,并提供了代码示例。我们还了解了这些功能的重要点和用法。

我们讨论了 ValueTuples 如何帮助我们收集数据信息以及我们期望从方法中获得多个输出的情况。ValueTuple的一个优点是它是可变的和ValueTypeSystem.ValueTuple提供了一些publicstatic方法,我们可以利用这些方法实现许多复杂的场景。

然后我们了解了模式匹配的优势和能力;这有助于编码人员执行各种复杂的条件场景,这在 C#语言的先前版本中是不可能的。类型模式和case语句中的when子句使这个功能非常出色。

本地函数是 C# 7.0 中引入的最重要的功能之一。它们在需要使我们的代码对称的场景中非常有帮助,这样你就可以完美地阅读代码,当我们不需要在方法外部使用方法,或者我们不需要重用在块范围内需要的操作时。

随着字面改进,现在我们可以将二进制数字声明为常量,并像使用其他变量一样使用它们。添加数字分隔符下划线(_)的能力使这个功能更加有用。

最后,我们已经了解了作为 Visual Studio 更新 3 的一部分发布的 C# 7.1 语言的新功能。

早些时候,计划中有更多的功能计划发布,但最终发布的版本却包含了之前的新功能。下一个版本已经计划中,还有更强大的功能尚未推出。您可以在这里查看计划和下一个版本的功能列表:github.com/dotnet/csharplang/tree/master/proposals

第四章:第 04 天 - 讨论 C#类成员

我们正在进行为期七天的学习系列的第四天。在第二天,我们讨论了典型的 C#程序,并了解了如何编译和执行程序。我们讨论了Main方法及其用途。我们还讨论了语言 C#的保留关键字,然后我们对 C#中的类和结构进行了概述。在第三天,我们讨论了 C#7.0 中引入的所有新功能。

在本章中,将解释 C#方法和属性的基础知识,我们还将涵盖 C#中索引器的概念。在第二天讨论的字符串操作将通过 RegEx 进行扩展,并解释为什么它很强大。文件管理将与一些中级文件系统观察者一起讨论。

今天,我们将更深入地讨论 C#类。本章将涵盖以下主题:

  • 修饰符

  • 方法

  • 属性

  • 索引器

  • 文件 I/O

  • 异常处理

  • 讨论正则表达式及其重要性

在第二天,我们讨论了一个典型的 C#程序,并讨论了程序如何编译和执行。Main方法的用途/重要性是什么?我们将继续讨论并开始我们的第四天。

在开始之前,让我们通过字符串计算器程序的步骤(github.com/garora/TDD-Katas/tree/develop/Src/cs/StringCalculator)进行一下。有一个简单的要求,即将作为字符串提供的数字相加。以下是一个简单的代码片段,基于这个一句要求,它没有提到需要在字符串中提供多少个数字:

namespace Day04
{
    class Program
    {
        static void Main(string[] args)
        {
            Write("Enter number1:");
            var num1 = ReadLine();
            Write("Enter number2:");
            var num2 = ReadLine();
            var sum = Convert.ToInt32(num1) +
            Convert.ToInt32(num2);
            Write($"Sum of {num1} and {num2} is {sum}");
            ReadLine();
        }
    }
}

当我们运行上述代码时,我们将获得以下输出:

上述代码运行良好,并给出了预期的结果。我们之前讨论的要求非常有限和模糊。让我们详细说明最初的要求:

  • 使用Add操作创建一个简单的字符串计算器:

  • 此操作应仅接受字符串数据类型的输入。

  • Add操作可以接受零个、一个或两个逗号分隔的数字,并返回它们的总和,例如11,2

  • Add操作应接受空字符串,但对于空字符串,它将返回零。

上述要求在我们之前的代码片段中没有得到解答。为了实现这些要求,我们应该调整我们的代码片段,我们将在接下来的部分中讨论。

修饰符

修饰符只是 C#中用于声明特定方法、属性或变量如何可访问的特殊关键字。在本节中,我们将讨论修饰符,并使用代码示例讨论它们的用法。

修饰符的整个目的是封装。这是关于对象如何通过封装变得简化的,修饰符就像旋钮一样,告诉客户想要展示多少,不想展示多少。要理解封装,请参考第七天,“封装”。

访问修饰符和可访问级别

访问修饰符告诉我们成员、声明类型等如何以及在哪里可以被访问或可用。以下讨论将为您提供对所有访问修饰符和可访问级别的更广泛理解。

public

public修饰符帮助我们定义成员的范围,没有任何限制。这意味着如果我们使用公共访问修饰符定义任何类、方法、属性或变量,该成员可以在其他成员中无限制地访问。

使用公共访问修饰符声明的派生类型的类型或成员的可访问性级别是不受限制的,这意味着它可以在任何地方访问。

要理解无限制的可访问级别,让我们考虑以下代码示例:

namespace Day04
{
    internal class StringCalculator
    {
        public string Num1 { get; set; }
        public string Num2 { get; set; }

        public int Sum() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
     }
}
Num1 and Num2, and one method Sum(), with the access modifier public. This means these properties and the method is accessible to other classes as well. Here is the code snippet that consumes the preceding class:
namespace Day04
{
    class Program
    {
        static void Main(string[] args)
        {
            StringCalculator calculator = new
            StringCalculator();
            Write("Enter number1:");
            calculator.Num1 = ReadLine();
            Write("Enter number2:");
            calculator.Num2 = ReadLine();
            Write($"Sum of {calculator.Num1} and
            {calculator.Num2} is {calculator.Sum()}");
            ReadLine();
        }
    }
}

上述代码片段将完美运行并产生预期的结果。当您运行上述代码时,它将显示结果,如下图所示:

受保护的

protected修饰符帮助我们定义成员的范围,而不是从定义/创建成员的类中定义的类型。换句话说,当我们使用protected访问修饰符定义变量、属性或方法时,这意味着这些成员的可用范围在定义了所有这些成员的类内部。

使用受保护访问修饰符声明的派生类型的类型或成员的可访问性级别是受限制的,这意味着它只能在类内部或从成员类创建的派生类型中访问。受保护修饰符在面向对象编程中的 C#中非常重要和活跃。您应该对继承有所了解。请参考第七天,继承

为了理解受保护的可访问性级别,让我们考虑以下代码示例:

namespace Day04
{
    class StringCalculator
    {

        protected string Num1 { get; set; }
        protected string Num2 { get; set; }

    }

    class StringCalculatorImplementation : StringCalculator
    {
        public void Sum()
        {
            StringCalculatorImplementation calculator =
            new StringCalculatorImplementation();

            Write("Enter number1:");

            calculator.Num1 = ReadLine();

            Write("Enter number2:");

            calculator.Num2 = ReadLine();

            int sum = Convert.ToInt32(calculator.Num1) +
            Convert.ToInt32(calculator.Num2);

            Write($"Sum of {calculator.Num1} and
            {calculator.Num2} is {sum}");
        }
    }
}

在前面的代码中,我们有两个类:StringCalculatorStringCalculatorImplementation。在StringCalculator类中,属性使用protected访问修饰符进行定义。这意味着这些属性只能从StringCalculator类或StringCalculatorImplementation(这是StringCalculator类的派生类型)中访问。前面的代码将产生以下输出:

以下代码将不起作用,并将产生编译时错误:

class StringCalculatorImplementation : StringCalculator
{
    readonly StringCalculator _stringCalculator = new
    StringCalculator();
    public int Sum()
    {
        var num=_stringCalculator.Num1; //will not work
        var number=_stringCalculator.Num2; //will not work

        //other stuff
    }
}

在前面的代码中,我们尝试通过创建StringCalculator类的实例来从StringCalculatorImplementation类中访问Num1Num2。这是不可能的,也不会起作用。请参考以下截图:

内部

内部修饰符帮助我们定义成员在同一程序集中的范围。使用内部访问修饰符定义的成员不能在定义它们的程序集之外访问。

使用内部访问修饰符声明的类型或成员的可访问性级别对于程序集外部是受限制的。这意味着这些成员不允许从外部程序集访问。

为了理解内部的可访问性级别,让我们考虑以下代码示例:

namespace ExternalLib
{
    internal class StringCalculatorExternal
    {
        public string Num1 { get; set; }
        public string Num2 { get; set; }
    }
}

该代码属于包含StringCalculatorExternal类的ExternalLib程序集,该类具有内部访问修饰符和两个属性Num1Num2,使用了public访问修饰符。如果我们从其他程序集调用此代码,它将无法工作。让我们考虑以下代码片段:

namespace Day04
{
    internal class StringCalculator
    {
        public int Sum()
        {
            //This will not work
            StringCalculatorExternal externalLib = new StringCalculatorExternal();
            return Convert.ToInt32(externalLib.Num1) + Convert.ToInt32(externalLib.Num2);
        }
    }
}

前面的代码是一个单独的第四天的程序集,并且我们试图调用ExternalLib程序集的StringCalculatorExternal类,这是不可能的,因为我们已将此类定义为internal。这段代码将抛出以下错误:

复合

当我们联合使用受保护和内部访问修饰符,即protected internal,这些修饰符的组合称为复合修饰符。

protected internal表示受保护或内部,而不是受保护和内部。这意味着成员可以从同一程序集中的任何类中访问。

为了理解受保护的内部可访问性级别,让我们考虑以下代码示例:

namespace Day04
{
    internal class StringCalculator
    {
        protected internal string Num1 { get; set; }
        protected internal string Num2 { get; set; }
    }

    internal class StringCalculatorImplement :
    StringCalculator
    {
        public int Sum() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
    }
}

前面的代码是第四天的程序集,其中有一个StringCalculatorImplement类,即继承了StringCalculator类(这个类有两个属性,使用了protected internal访问修饰符)。让我们考虑一下来自同一程序集的代码:

namespace Day04
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var calculator = new
            StringCalculatorImplement();
            Write("Enter number1:");
            calculator.Num1 = ReadLine();
            Write("Enter number2:");
            calculator.Num2 = ReadLine();

            Write($"Sum of {calculator.Num1} and
            {calculator.Num2} is {calculator.Sum()}");
            ReadLine();
        }
    }
}

前面的代码将产生以下输出:

private

private修饰符是成员的最低范围。这意味着只有在定义了private修饰符的类内部才能访问该成员。

private表示受限制的访问,成员只能从类内部或其嵌套类型中访问,如果定义了的话。

为了理解私有的可访问性级别,让我们考虑以下代码示例:

internal class StringCalculator
{
    private string Num1 { get; set; }
    private string Num2 { get; set; }

    public int Sum() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
}

在前面的代码中,属性Num1Num2对于StringCalculator类外部是不可访问的。下面的代码将无法工作:

internal class StringCalculatorAnother
{
    private readonly StringCalculator _calculator;

    public StringCalculatorAnother(StringCalculator
    calculator)
    {
        _calculator = calculator;
    }

    public int Sum() => Convert.ToInt32(_calculator.Num1) +        Convert.ToInt32(_calculator.Num2);

}

前面的代码将会抛出编译时错误,如下截图所示:

访问修饰符的规则

我们已经讨论了访问修饰符和使用这些访问修饰符的可访问性。现在,我们应该遵循在使用这些访问修饰符时应该遵循的一些规则,这些规则在这里讨论:

  • 组合限制:在使用访问修饰符时有一个限制。除非使用了访问修饰符protected internal,否则不应该组合使用这些修饰符。考虑前一节中讨论的代码示例。

  • 命名空间限制:这些访问修饰符不应该与命名空间一起使用。

  • 默认可访问性限制:当成员声明时,如果没有使用访问修饰符,则使用默认可访问性。所有类都是隐式内部的,其成员是私有的。

  • 顶层类型限制:顶层类型是具有直接父类型对象的父类型。父类型或顶层类型不能使用除了internalpublic可访问性之外的任何可访问性。如果没有应用访问修饰符,则默认可访问性将是内部的。

  • 嵌套类型限制:嵌套类型是其他类型的成员,或者具有除了通用类型(即对象)以外的直接父类型。这些类型的可访问性可以根据以下表格中讨论的方式进行声明(docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/accessibility-levels):

嵌套类型 成员的默认可访问性 允许声明的可访问性 描述
枚举 public enum具有公共可访问性,其成员只有public可访问性。这些是为其他类型使用的,因此它们不允许显式设置任何可访问性。
private publicinternalprotectedprivateprotected internal 类默认是内部的,成员是private。有关访问修饰符的规则的更多细节,请参考前一节。
接口 public 接口默认是内部的,其成员是public。接口的成员是为了被继承类型使用的,因此接口不允许显式设置可访问性。
结构体 private publicinternalprivate class相同,结构体默认是内部的,其成员是private。我们可以显式应用publicinternalprivate的可访问性。

抽象

简而言之,我们可以说抽象修饰符表示事物尚未完成。当使用抽象修饰符创建一个class时,该class只能作为其他类的基类。在这个class中标记为抽象的成员应该在派生类中实现。

抽象修饰符表示不完整的事物,可以用于类、方法、属性、索引器和/或事件。标记为抽象的成员不允许定义除publicprotectedinternalprotected internal之外的可访问性。

抽象类是半定义的。这意味着这些类提供了一种方式来覆盖子类的成员。我们应该在项目中使用基类,其中我们需要所有子类都具有相同的成员,并且具有自己的实现或需要覆盖。例如,让我们考虑一个抽象类 car,其中有一个抽象方法 color,并且有子类 Honda car,Ford car,Maruti car 等。在这种情况下,所有子类都会有颜色成员,但具有不同的实现,因为颜色方法将在子类中被覆盖,具有自己的实现。这里需要注意的一点是,抽象类代表 is-a 关系。

为了理解这个修饰符的能力,让我们考虑以下例子:

namespace Day04
{
    internal abstract class StringCalculator
    {
        public abstract string Num1 { get; set; }
        protected abstract string Num2 { get; set; }
        internal abstract string Num3 { get; set; }
        protected internal abstract string Num4 { get;
        set; }

        public int Sum() => Convert.ToInt32(Num1) +            Convert.ToInt32(Num2);
    }
}

前面的代码片段是一个包含抽象属性和非抽象方法的抽象类。其他类只能实现这个类。请参考以下代码片段:

internal class StringCalculatorImplement : StringCalculator
{
    public override string Num1 { get; set; }
    protected override string Num2 { get; set; }
    internal override string Num3 { get; set; }
    protected internal override string Num4 { get; set; }

    //other stuffs here
}
StringCalculatorImplement is implementing the abstract class StringCalculator, and all members are marked as abstract.

抽象修饰符的规则

在使用抽象修饰符时,我们需要遵循一些规则,这些规则如下所述:

  • 实例化:如果一个类被标记为抽象,我们不能创建它的实例。换句话说,抽象类不允许对象初始化。如果我们尝试显式地这样做,将会得到一个编译时错误。请参考以下截图,我们在尝试实例化一个抽象类:

  • 非抽象:一个类可能包含或不包含被标记为抽象的抽象方法或成员。这意味着当我们必须为抽象类创建所有抽象成员和方法时,没有限制。以下代码遵守了这个规则:
internal abstract class StringCalculator
{
    public abstract string Num1 { get; set; }
    public abstract string Num2 { get; set; }
    public abstract int SumToBeImplement();

    //non-abstract
    public int Sum() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
}

internal class StringCalculatorImplement : StringCalculator
{
    public override string Num1 { get; set; }
    public override string Num2 { get; set; }
    public override int SumToBeImplement() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
}
  • 限制-继承性质:正如我们讨论的,抽象类是用来被其他类继承的。如果我们不想将抽象类从其他类继承,我们应该使用 sealed 修饰符。我们将在接下来的章节中详细讨论这一点。

有关更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members.

  • 实现性质:抽象类的所有成员应该在继承抽象类的子类中实现,只有当子类是非抽象的时候。为了理解这一点,让我们考虑以下例子:
internal abstract class StringCalculator
{
    public abstract string Num1 { get; set; }
    public abstract string Num2 { get; set; }
    public abstract int SumToBeImplement();

    //non-abstract
    public int Sum() => Convert.ToInt32(Num1) + Convert.ToInt32(Num2);
}
internal abstract class AnotherAbstractStringCalculator: StringCalculator
{
    //no need to implement members of StringCalculator class  
}
AnotherAbstractString, is inheriting another abstract class, StringCalculator. As both the classes are abstract, there's no need to implement members of the inherited abstract class that is StringCalculator.

现在,考虑另一个例子,其中继承的类是非抽象的。在这种情况下,子类应该实现抽象类的所有抽象成员;否则,它将抛出编译时错误。请参阅以下截图:

  • 虚拟性质:对于abstract类标记为抽象的方法和属性,默认情况下是虚拟的。这些方法和属性将在继承类中被重写。

以下是抽象类实现的完整示例:

internal abstract class StringCalculator
{
    public  string Num1 { get; set; }
    public  string Num2 { get; set; }
    public abstract int Sum();

}

internal class StringCalculatorImplement : StringCalculator
{
    public override int Sum() => Convert.ToInt32(Num1) +     Convert.ToInt32(Num2);
}

async

async修饰符提供了一种将匿名类型或 lambda 表达式的方法作为异步方法的方式。当它与一个方法一起使用时,该方法被称为async方法。

async 将在第六天详细讨论。

考虑以下代码示例:

internal class StringCalculator
{
    public string Num1 { get; set; }
    public string Num2 { get; set; }
    public async Task<int> Sum() => await Task.Run(()=>Convert.ToInt32(Num1) +
        Convert.ToInt32(Num2));
}

前面的代码将提供与前几节中讨论的代码示例相同的结果;唯一的区别是这个方法调用是异步的。

const

const修饰符允许定义一个常量字段或常量局部变量。当我们使用const定义字段或变量时,这些字段不再被称为变量,因为const不允许改变,而变量允许。常量字段是类级常量,在类内外都可以访问(取决于它们的修饰符),而常量局部变量是在方法内定义的。

作为const定义的字段和变量不是变量,不能被修改。这些常量可以是数字、bool、字符串或空引用。在声明常量时不允许使用静态修饰符。

以下是显示有效常量声明的代码片段:

internal class StringCalculator
{
    private const int Num1 = 70;
    public const double Pi = 3.14;
    public const string Book = "Learn C# in 7-days";

    public int Sum()
    {
        const int num2 = Num1 + 85;
        return  Convert.ToInt32(Num1) + Convert.ToInt32(num2);
    }
}

event

event修饰符帮助声明publisher类的事件。我们将在第五天详细讨论这一修饰符。有关此修饰符的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/event.

extern

extern修饰符帮助声明一个使用外部库或 dll 的方法。当你想要使用外部不受管控的库时,这一点非常重要。

使用extern关键字实现外部不受管控库的方法必须声明为静态的。有关更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extern.

new

new操作符可以是一个修饰符、一个操作符或修饰符。让我们详细讨论一下:

  • 操作符new 作为一个操作符,帮助我们创建一个class的对象实例,并调用它们的构造函数。例如,以下行展示了new作为一个操作符的使用:
StringCalculator calculator = new StringCalculator();
  • 修饰符new修饰符帮助隐藏从基类继承的成员:
internal class StringCalculator
{
    private const int Num1 = 70;
    private const int Num2 = 89;

    public int Sum() => Num1 + Num2;
}

internal class StingCalculatorImplement : StringCalculator
{
    public int Num1 { get; set; }
    public int Num2 { get; set; }

    public new int Sum() => Num1 + Num2;
}

这在 C#中也被称为隐藏。

  • 约束new操作符作为约束确保在每个泛型类的声明中,必须有一个公共的无参数构造函数。这将在第五天详细讨论。

override

override修饰符帮助扩展继承成员(即方法、属性、索引器或事件)的抽象或虚拟实现。这将在第七天详细讨论。

partial

通过partial修饰符,我们可以将一个类、一个接口或一个结构分割成多个文件。看下面的代码示例:

namespace Day04
{
    public partial class Calculator
    {
        public int Add(int num1, int num2) => num1 + num2;
    }
}
namespace Day04
{
    public partial class Calculator
    {
        public int Sub(int num1, int num2) => num1 - num2;
    }
}

这里有两个文件,Calculator.csCalculator1.cs。这两个文件都将Calculator作为它们的部分类。

readonly

readonly修饰符帮助我们创建一个字段声明为readonlyreadonly字段只能在声明时或作为声明的一部分被赋值。为了更好地理解这一点,请考虑以下代码片段:

internal class StringCalculator
{
    private readonly int _num2;
    public readonly int Num1 = 179;

    public StringCalculator(int num2)
    {
        _num2 = num2;
    }

    public int Sum() => Num1 + _num2; 
}
Num1 and _num2 are readonly .Here is the code snippet that tells us how to use these fields:
namespace Day04
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            WriteLine("Example of readOnly modifier");
            Write("Enter number of your choice:");
            var num = ReadLine();
            StringCalculator calculator =
            newStringCalculator(Convert.ToInt32(num));
            Write($"Sum of {calculator.Num1} and {num} is
            {calculator.Sum()}");
            ReadLine();
        }
    }
}
_num2 is initialized from the constructor and Num1 is initialized at the time of its declaration. When you run the preceding code, it generates output as shown in following screenshot:

如果我们明确尝试给Num1 readonly字段赋值,将会抛出编译时错误。请参阅以下截图:

Num1 to the readonly field. This is not allowed, so it throws an error.

sealed

sealed修饰符是当应用于一个class时,表示“我不会再被其他类继承。现在不要继承我。” 简单来说,这个修饰符限制了类被其他类继承。

当应用于派生或继承类的抽象方法(默认情况下是虚拟的)时,sealed修饰符与override一起使用。

为了更好地理解这一点,让我们考虑以下代码示例:

internal abstract class StringCalculator
{
    public int Num1 { get; set; }
    public int Num2 { get; set; }

    public abstract int Sum();
    public virtual int Sub() => Num1 -Num2;
}
internal class Calc : StringCalculator
{
    public int Num3 { get; set; }
    public int Num4 { get; set; }
    public override int Sub() => Num3 - Num4;
    //This will not be inherited from within derive classes
    //any more
    public sealed override int Sum() => Num3 + Num4;
}
calc, both the methods Sum() and Sub() are overridden. From here, method Sub() is available for further overriding, but Sum() is a sealed method, so we can't override this method anymore in derived classes. If we explicitly try to do this, it throws a compile-time error as shown in the following screenshot:

你不能在抽象类上应用sealed修饰符。如果我们明确尝试这样做,最终会抛出编译时错误。请参阅以下截图:

静态的

static修饰符帮助我们声明静态成员。这些成员实际上也被称为类级成员,而不是对象级成员。这意味着不需要创建对象实例来使用这些成员。

静态修饰符的规则

在使用static修饰符时需要遵循一些规则:

  • 限制:你只能在类、字段、方法、属性、操作符、事件和构造函数中使用static修饰符。这个修饰符不能用于索引器和除class之外的类型。

  • 静态的性质:当我们声明一个常量时,它本质上是静态的。考虑以下代码片段:

internal class StringCalculator
{
   public const int Num1 = 10;
   public const int Num2 = 20;
}

前面的StringCalculator类有两个常量,Num1Num2。这些可以被class访问,不需要创建class的实例。请参考以下代码片段:

internal class Program
{
    private static void Main(string[] args)
    {
        WriteLine("Example of static modifier");
        Write($"Sum of {StringCalculator.Num1} and
        {StringCalculator.Num2} is{StringCalculator.Num1 +
        StringCalculator.Num2}");
        ReadLine();
    }
}
  • 完全静态:如果类使用static修饰符定义,那么这个static类的所有成员都应该是static的。如果一个static类明确定义为创建非静态成员,将会有一个编译时错误。请参考以下截图:

  • 可用性:不需要创建类的实例来访问static成员。

关键字this不能应用于static方法或属性。我们已经在第二天讨论过这一点,以及base关键字。

不安全

这个修饰符帮助我们使用不安全的代码块。我们将在第六天详细讨论这个问题。

虚拟

这个修饰符帮助我们定义虚拟方法,这些方法是为了在继承类中被重写。请看以下代码:

internal class StringCalculator
{
    private const int Num1 = 70;
    private const int Num2 = 89;

    public virtual int Sum() => Num1 + Num2;
}

internal class StingCalculatorImplement : StringCalculator
{
    public int Num1 { get; set; }
    public int Num2 { get; set; }

    public override int Sum() => Num1 + Num2;
}

有关更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/virtual.

方法

具有访问修饰符、名称、返回类型和参数(可能有也可能没有)的一组语句仅仅是一个方法。方法的目的是执行一些任务。

方法的目的是被另一个方法或另一个程序调用。

如何使用方法?

如前所述,方法的目的是执行一些操作。因此,任何需要利用这些操作的方法或程序都可以调用/使用定义的方法。

方法有各种元素,如下所述:

  • 访问修饰符:方法应该有一个访问修饰符(有关修饰符的更多细节,请参考前一节)。这个修饰符帮助我们定义方法的范围或方法的可用性,例如。使用private修饰符定义的方法只能对其自己的类可见。

  • 返回类型:执行操作后,方法可能会返回或不返回任何东西。方法的返回类型基于数据类型(有关数据类型的信息,请参考第二天)。例如,如果方法返回一个数字,它的数据类型将是 int,如果方法不返回任何东西,它的返回类型是void

  • 名称:名称在class内是唯一的。名称区分大小写。在StringCalculator类中,我们不能定义两个名称为Sum()的方法。

  • 参数:对于任何方法来说都是可选的。这意味着一个方法可能有也可能没有参数。参数是基于数据类型定义的。

  • 功能体:方法要执行的一部分指令就是方法的功能。

以下截图显示了一个典型的方法:

在继续之前,让我们回顾一下我们在第四天开始时讨论的要求,我们在那里创建了一个方法来计算字符串参数列表的总和。以下是满足这些要求的程序:

namespace Day04
{
    class StringCalculatorUpdated
    {
        public int Add(string numbers)
        {
            int result=0;
            if (string.IsNullOrEmpty(numbers))
                return result;
            foreach (var n in numbers.Split(','))
            {
                result +=
                Convert.ToInt32(string.IsNullOrEmpty(n) ? "0" : n);
            }
            return result;
        }
    }
}
namespace Day04
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            WriteLine("Example of method");
            StringCalculatorUpdated calculator = new
            StringCalculatorUpdated();
            Write("Enter numbers comma separated:");
            var num = ReadLine();
            Write($"Sum of {num} is
            {calculator.Add(num)}");
            ReadLine();
        }
    }
}

前面的代码产生了预期的输出。请参考以下截图:

前面的代码完全正常工作,但需要重构,所以让我们将代码拆分成小方法:

namespace Day04
{
    internal class StringCalculatorUpdated
   {
        public int Add(string numbers) =>
        IsNullOrEmpty(numbers) ? 0 :
        AddStringNumbers(numbers);

        private bool IsNullOrEmpty(string numbers) =>
        string.IsNullOrEmpty(numbers);

        private int AddStringNumbers(string numbers) =>
        GetSplittedStrings(numbers).Sum(StringToInt32);

        private IEnumerable<string>
        GetSplittedStrings(string numbers) =>
        numbers.Split(',');
        private int StringToInt32(string n) =>
        Convert.ToInt32(string.IsNullOrEmpty(n) ? "0" : n);
    }
}

代码重构超出了本书的范围。有关代码重构的更多详细信息,请参阅www.packtpub.com/application-development/refactoring-microsoft-visual-studio-2010.

现在,我们的代码看起来更好,更易读。这将产生相同的输出。

属性

属性是类、结构或接口的成员,通常称为命名成员。属性的预期行为类似于字段,不同之处在于可以使用访问器来实现属性。

属性是字段的扩展。访问器 get 和 set 帮助检索和分配属性的值。

以下是类的典型属性(也称为具有自动属性语法的属性):

public int Number { get; set; }

对于自动属性,编译器会生成后备字段,这只是一个存储字段。因此,前面的属性将显示如下,带有后备字段:

private int _number;

public int Number
{
    get { return _number; }
    set { _number = value; }
}

具有表达式主体的前面属性如下:

private int _number;
public int Number
{
    get => _number;
    set => _number = value;
}

有关表达式主体属性的更多详细信息,请参阅visualstudiomagazine.com/articles/2015/06/03/c-sharp-6-expression-bodied-properties-dictionary-initializer.aspx.

属性类型

我们可以声明或使用多种属性。我们刚刚讨论了自动属性,并讨论了编译器如何将其转换为备份存储字段。在本节中,我们将讨论其他可用的属性类型。

读写属性

允许我们存储和检索值的属性就是读写属性。具有后备存储字段的典型读写属性将具有setget访问器。set访问器存储属性的数据类型的数据。请注意,对于 set 访问器,始终有一个参数,即 value,并且这与属性的存储数据或数据类型匹配。

自动属性会被编译器自动转换为带有后备存储字段的属性。

请查看以下代码片段以了解详情:

internal class ProeprtyExample
{
    private int _num1;
    //with backing field
    public int Num1
    {
        get => _num1;
        set => _num1 = value;
    }
    //auto property
    public int Num2 { get; set; }
}

以前,我们有两个属性:一个是使用后备字段定义的,另一个是使用自动属性定义的。访问器set负责使用参数值存储数据,并且与数据类型 int 匹配,get负责检索数据,数据类型为 int。

只读属性

只有get访问器或私有set访问器的属性称为只读属性。

只读和const之间有细微差别。有关更多详细信息,请参阅stackoverflow.com/questions/55984/what-is-the-difference-between-const-and-readonly

顾名思义,只读属性只能检索值。您不能在只读属性中存储数据。有关更多详细信息,请参阅以下代码片段:

internal class PropertyExample
{
    public PropertyExample(int num1)
    {
        Num1 = num1;
    }
    //constructor restricted property
    public int Num1 { get; }
    //read-only auto proeprty
    public int Num2 { get; private set; }
    //read-only collection initialized property
    public IEnumerable<string> Numbers { get; } = new List<string>();
}

在上面的代码中,我们有三个属性;所有都是只读的。Num1是一个只读属性,并且受构造函数限制。这意味着您只能在构造函数中设置属性。Num2是一个纯只读属性;这意味着它用于检索数据。Numbers 是自动初始化的只读属性;它对集合属性进行了默认初始化。

计算属性

返回表达式结果的属性称为计算属性。该表达式可以基于同一类的其他属性或基于任何有效的 CLR 兼容数据类型的表达式(有关数据类型,请参考第二天),其应与属性数据类型相同。

计算属性返回表达式的结果,并且不允许设置数据,因此这些是某种只读属性。

要详细了解,请考虑以下内容:

块主体成员

在块主体计算属性中,计算结果在 get 访问器中返回。请参考以下示例:

internal class ProeprtyExample
{
    //other stuff removed
    public int Num3 { get; set; }
    public int Num4 { get; set; }
    public int Sum {
        get
        {
            return Num3 + Num4;
        }
    }
}

在上面的代码中,我们有三个属性:Num3Num4Sum。属性Sum是一个计算属性,它从 get 访问器中返回表达式结果。

表达式主体成员

在表达式主体中,使用 lambda 表达式返回计算属性计算结果,这是由表达式主体成员使用的。请参考以下示例:

internal class ProeprtyExample
{
    public int Num3 { get; set; }
    public int Num4 { get; set; }
    public int Add => Num3 + Num4;
}

在上面的代码中,我们的Add属性返回了另外两个属性的Sum表达式。

使用验证的属性

可能会有一些情况,我们想要验证某些属性的数据。然后,我们会在属性上使用一些验证。这些不是特殊类型的属性,而是带有验证的完整属性。

数据注释是一种验证各种属性并添加自定义验证的方法。有关更多信息,请参阅www.codeproject.com/Articles/826304/Basic-Introduction-to-Data-Annotation-in-NET-Frame.

在需要使用属性验证输入时,这些属性非常重要。考虑以下代码片段:

internal class ProeprtyExample
{
    private int _number;
    public int Number
    {
        get => _number;
        set
        {
            if (value < 0)
            {
                //log for records or take action
                //Log("Number is negative.");
                throw new ArgumentException("Number can't be -ve.");
            }
            _number = value;
        }
    }
}

在上述代码中,对于前面的属性,客户端代码不需要应用任何显式验证。每当调用存储数据时,属性Number会自我验证。在以前的代码中,每当客户端代码尝试输入任何负数时,它会隐式抛出一个异常,即数字不能为负数。在这种情况下,客户端代码只输入正数。在同一节点上,您可以应用尽可能多的验证。

索引器

索引器提供了一种通过索引访问对象的方式,就像数组一样。例如,如果我们为一个类定义了索引器,那么该类的工作方式类似于数组。这意味着可以通过索引访问该类的集合。

关键字this用于定义索引器。索引器的主要好处是我们可以在不显式指定类型的情况下设置或检索索引值。

考虑以下代码片段:

public class PersonCollection
{
    private readonly string[] _persons = Persons();
    public bool this[string name] => IsValidPerson(name);
    private bool IsValidPerson(string name) =>
    _persons.Any(person => person == name);

    private static string[] Persons() => new[]
    {"Shivprasad","Denim","Vikas","Merint","Gaurav"};
}

上述代码是一个简单的表示索引器强大的示例。我们有一个PersonCollection类,其中有一个索引器,使得该类可以通过索引器访问。请参考以下代码:

private static void IndexerExample()
{
    WriteLine("Indexer example.");
    Write("Enter person name to search from collection:");
    var name = ReadLine();
    var person = new PersonCollection();
    var result = person[name] ? "exists." : "does not
    exist.";
    WriteLine($"Person name {name} {result}");
}

执行上述代码后,我们可以看到以下输出:

有关索引器的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/indexers/.

文件 I/O

文件只是在系统目录中物理存储的数据集合。文件包含的数据可以是任何信息。在 C#中,每当文件可供程序检索信息(读取)或更新信息(写入)时,那就是一个流。

流就是一系列字节。

在 C#文件中,I/O 只是调用输入流或输出流的一种方式:

  • 输入流:这只是一个读取操作。每当我们以编程方式从文件读取数据时,它被称为输入流或读取操作。

  • 输出流:这只是一个更新操作。每当我们以编程方式向文件添加数据时,它被称为输出流或写操作。

文件 I/O 是System.IO命名空间的一部分,其中包含各种类。在本节中,我们将讨论我们将在代码示例中使用的 FileStream。

完整的 System.IO 类列表可在docs.microsoft.com/en-us/dotnet/api/system.io?view=netcore-2.0.找到。

文件流

如前所述,在System.IO命名空间下有几个有用的类可用。FileStream 是其中之一,它帮助我们读取/写入文件中的数据。在讨论这个class之前,让我们考虑一个简短的示例,我们将在其中创建一个文件:

private static void FileInputOutputOperation()
{
    const string textLine = "This file is created during
    practice of C#";
    Write("Enter file name (without extension):");
    var fileName = ReadLine();
    var fileNameWithPath = $"D:/{fileName}.txt";
    using (var fileStream = File.Create(fileNameWithPath))
    {
        var iBytes = new
        UTF8Encoding(true).GetBytes(textLine);
        fileStream.Write(iBytes, 0, iBytes.Length);
    }
    WriteLine("Write operation is completed.");
    ReadLine();
    using (var fileStream =
    File.OpenRead(fileNameWithPath))
    {
        var bytes = new byte[1024];
        var encoding = new UTF8Encoding(true);
        while (fileStream.Read(bytes, 0, bytes.Length) >
        0)
        WriteLine(encoding.GetString(bytes));
    }
}

上述代码首先创建一个具有特定文本/数据的文件,然后显示相同的内容。以下是上述代码的输出。请参考以下截图:

完整的 FileStream 参考可在docs.microsoft.com/en-us/dotnet/api/system.io.filestream?view=netcore-2.0.找到。

异常处理

异常是一种错误,当方法不按预期工作或无法处理预期的情况时出现。有时会出现未知情况导致异常;例如,一个方法可能在除法运算中出现除以零的问题,这种情况在编写方法时从未预料到,这是一种不可预测的情况错误。为了处理这种情况以及可能导致此类异常或错误的其他未知情况,C#提供了一种称为异常处理的方法。在本节中,我们将详细讨论使用 C#处理异常和异常处理。

可以使用try...catch...finally块来处理异常。try块应该有 catch 块或 finally 块来处理异常。

考虑以下代码:

class ExceptionhandlingExample
    {
        public int Div(int dividend,int divisor)
        {
            //thrown an exception if divisor is 0
            return dividend / divisor;
        }
    }

如果使用以下代码调用时除数为零,上述代码将抛出未处理的除零异常:

private static void ExceptionExample()
{
    WriteLine("Exaception handling example.");
    ExceptionhandlingExample example = new ExceptionhandlingExample();
    Write("Enter dividen:");
    var dividend = ReadLine();
    Write("Enter divisor:");
    var divisor = ReadLine();
    var quotient = example.Div(Convert.ToInt32(dividend), Convert.ToInt32(divisor));
    WriteLine($"Quotient of dividend:{dividend}, divisio:{divisor} is {quotient}");
}

请参考以下屏幕截图以查看异常:

为了处理类似于之前情况的情况,我们可以使用异常处理。在 C#中,异常处理具有共同的组件,这里进行讨论。

try 块

try块是异常源的代码块。try块可以有多个catch块和/或一个最终块。这意味着try块应该至少有一个 catch 块或一个 final 块。

catch block

catch块是一个代码块,用于处理特定或一般异常。catch有一个Exception参数,告诉我们发生了什么异常。

finally block

finally块是一个无论如何都会执行(如果提供)的块。通常,finally块用于在异常后执行一些清理任务。

throw关键字有助于抛出系统或自定义异常。

现在,让我们重新访问之前抛出异常的代码:

class ExceptionhandlingExample
{
    public int Div(int dividend,int divisor)
    {
        int quotient = 0;
        try
        {
            quotient = dividend / divisor;
        }
        catch (Exception exception)
        {
            Console.WriteLine($"Exception occuered
            '{exception.Message}'");
        }
        finally
        {
            Console.WriteLine("Exception occured and cleaned.");
        }
        return quotient;
    }
}

在这里,我们通过添加try...catch...finally块修改了代码。现在,每当发生异常时,首先进入catch块,然后进入finally块。在放置了finally块之后,每当我们除以零时都会发生异常,这将产生以下结果:

catch 块中的不同编译生成的异常

正如我们之前讨论的,try块内可能有多个catch块。这意味着我们可以捕获多个异常。不同的catch块可以编写来处理特定的异常类。例如,除零异常的exception类是System.DivideByZeroException。本书不涵盖所有这些类的完整讨论。有关这些异常类的进一步研究,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/exceptions/compiler-generated-exceptions.

用户定义的异常

根据要求创建的自定义异常是用户异常,当我们创建一个exception类来处理特定情况时,它被称为用户定义的异常。所有用户定义的exception类都派生自Exception类。

让我们创建一个用户定义的exception。回想一下StringCalculatorUpdated类(在方法部分讨论),它负责计算字符串数字的总和。在现有要求中添加一个场景,即如果任何数字大于 1,000,则抛出NumberIsExceded异常:

internal class NumberIsExcededException : Exception
{
    public NumberIsExcededException(string message) :
    base(message)
    {
    }
    public NumberIsExcededException(string message,
    Exception innerException):base(message,innerException)
    {
    }
    protected NumberIsExcededException(SerializationInfo
    serializationInfo, StreamingContext streamingContext)
    : base(serializationInfo, streamingContext) {}
}
NumberIsExcededException class. We have three constructors and all are self-explanatory. The third constructor is for serialization. If required here, we can do the serialization. So, when the exception goes to client from the remote server, it should be serialized.

以下是处理我们新创建的异常的代码片段:

internal class StringCalculatorUpdated
{
    public int Add(string numbers)
    {
        var result = 0;
        try
        {
            return IsNullOrEmpty(numbers) ? result :
            AddStringNumbers(numbers);
        }
        catch (NumberIsExcededException excededException)
        {
            Console.WriteLine($"Exception
            occurred:'{excededException.Message}'");
        }

        return result;
    }
    //other stuffs omitted

    private int StringToInt32(string n)
    {
        var number = 
        Convert.ToInt32(string.IsNullOrEmpty(n) ? "0" : n);
        if(number>1000)
            throw new NumberIsExcededException($"Number
            :{number} excedes the limit of 1000.");
        return number;
    }
}

现在,每当数字超过 1,000 时,它都会抛出异常。让我们编写一个客户端代码来抛出异常,考虑上述代码被调用:

private static void CallStringCalculatorUpdated()
{
    WriteLine("Rules for operation:");
    WriteLine("o This operation should only accept input
    in a string data type\n" +
              "o Add operation can take 0, 1, or 2 comma -
              separated numbers, and will return their sum
              for example \"1\" or \"1, 2\"\n" +
              "o Add operation should accept empty string
              but for an empty string it will return 0.\n"
              +
              "o Throw an exception if number > 1000\n");
              StringCalculatorUpdated calculator = new
              StringCalculatorUpdated();
    Write("Enter numbers comma separated:");
    var num = ReadLine();

    Write($"Sum of {num} is {calculator.Add(num)}"); 
}

上述代码将生成以下输出:

讨论正则表达式及其重要性

正则表达式或模式匹配只是一种我们可以检查输入字符串是否正确的方式。这是通过System.Text.RegularExpressions命名空间的Regex类实现的。

正则表达式的重要性

在验证文本输入时,模式匹配非常重要。在这里,正则表达式发挥着重要作用。

灵活

模式非常灵活,为我们提供了一种制定自己的模式来验证输入的方法。

构造

有各种构造帮助我们定义正则表达式。因此,在需要验证输入的编程中,我们需要使它们变得重要。这些构造包括字符类、字符转义、量词等。

特殊字符

在我们日常编程中,正则表达式的使用非常广泛,这就是为什么正则表达式很重要。根据它们的使用情况,特殊字符的正则表达式在验证输入时帮助我们。

句号符(.)

这是一个通配符,匹配除换行符之外的任何字符。

单词符号(w)

反斜杠和小写w是一个字符类,将匹配任何单词字符。

空格符(s)

可以使用s(反斜杠和s)来匹配空格。

数字符号(d)

可以使用d(反斜杠和小写d)来匹配零到九的数字。

连字符(-)

可以使用连字符(-)来匹配字符范围。

指定匹配的次数

可以使用大括号({n})指定字符、组或字符类所需的最小匹配次数。

以下是显示先前特殊字符的代码片段:

private static void ValidateInputText(string inputText, string regExp,bool isCllection=false,RegexOptions option=RegexOptions.IgnoreCase)
{
    var regularExp = new Regex(regExp,option);

    if (isCllection)
    {
        var matches = regularExp.Matches(inputText);
        foreach (var match in matches)
        {
            WriteLine($"Text '{inputText}' matches
            '{match}' with pattern'{regExp}'");
        }
    }
    var singleMatch = Regex.Match(inputText, regExp,
    option);
    WriteLine($"Text '{inputText}' matches '{singleMatch}'
    with pattern '{regExp}'");
    ReadLine();

}

前面的代码允许对inputTextRegexpression进行操作。以下是调用代码:

private static void RegularExpressionExample()
{
    WriteLine("Regular expression example.\n");
    Write("Enter input text to match:");
    var inpuText = ReadLine();
    if (string.IsNullOrEmpty(inpuText))
        inpuText = @"The quick brown fox jumps over the lazy dog.";
    WriteLine("Following is the match based on different pattern:\n");
    const string theDot = @"\.";
    WriteLine("The Period sign [.]");
    ValidateInputText(inpuText,theDot,true);
    const string theword = @"\w";
    WriteLine("The Word sign [w]");
    ValidateInputText(inpuText, theword, true);
    const string theSpace = @"\s";
    WriteLine("The Space sign [s]");
    ValidateInputText(inpuText, theSpace, true);
    const string theSquareBracket = @"\[The]";
    WriteLine("The Square-Brackets sign [( )]");
    ValidateInputText(inpuText, theSquareBracket, true);
    const string theHyphen = @"\[a-z0-9]ww";
    WriteLine("The Hyphen sign [-]");
    ValidateInputText(inpuText, theHyphen, true);
    const string theStar = @"\[a*]";
    WriteLine("The Star sign [*] ");
    ValidateInputText(inpuText, theStar, true);
    const string thePlus = @"\[a+]";
    WriteLine("The Plus sign [+] ");
    ValidateInputText(inpuText, thePlus, true);
}

前面的代码生成以下输出:

正则表达式是一个广泛的主题。有关更多详细信息,请参阅docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions?view=netcore-2.0.

动手练习

以下是直到第四天学到的未解决问题:

  1. 访问修饰符及其可访问性是什么?

  2. 编写一个程序使用protected internal

  3. 什么是抽象类?通过一个程序详细阐述。

  4. 抽象类有构造函数吗?如果有,为什么我们不能实例化抽象类?(参考stackoverflow.com/questions/2700256/why-cant-an-object-of-abstract-class-be-created

  5. 通过一个小程序,解释如何阻止抽象类被继承。

  6. 区分syncasync方法。

  7. 通过一个小程序区分constreadOnly修饰符。

  8. 编写一个程序,计算字符串数字,以及我们的StringCalcuatorUpdated示例的以下规则:

  • 如果数字大于 1,000,则抛出异常。

  • 通过用零替换负数来忽略负数。

  • 如果输入的字符串不是数字,则抛出异常。

  1. 编写一个小程序详细说明属性类型。

  2. 创建一个属性,使用验证来满足问题 8中讨论的所有规则。

  3. 什么是异常?

  4. 我们如何在 C#中处理异常?通过一个小程序详细阐述。

  5. 如果一个字符串包含除了定界符之外的特殊字符,根据我们的StringCalculatorUpdated类的要求,编写一个用户定义的异常。

  6. 编写一个程序,使用System.IO命名空间的各种类动态创建文件(参考docs.microsoft.com/en-us/dotnet/api/system.io.filestream?view=netcore-2.0)。

  7. 什么是索引器?编写一个简短的程序来创建一个分页列表的集合。

  8. 正则表达式是什么,它们如何在字符串操作中有帮助。使用一个小程序进行详细说明。

回顾第 04 天

我们结束了第四天的学习。今天,我们讨论了所有可用的修饰符,并通过这些修饰符的代码示例进行了讨论;我们还讨论了访问修饰符,即publicprivateinternalprotected等等。

然后,我们来到了方法和属性,我们讨论了各种场景并处理了程序。我们还讨论了索引器和文件 I/O,并通过学习正则表达式结束了我们的一天。我们讨论了常量,并讨论了常量字段和常量局部。

明天,也就是第五天,我们将讨论一些高级概念,涵盖反射,并了解如何动态创建和执行代码。

第五章:第 05 天 - 反射和集合概述

今天是我们七天学习系列的第五天。到目前为止,我们已经深入了解了 C#语言,并了解了如何处理语句、循环、方法等。今天,我们将学习在编写代码时动态工作的最佳方法。

我们有很多方法可以动态实现代码更改并生成整个编程类。今天,我们将涵盖以下主题:

  • 什么是反射?

  • 委托和事件概述

  • 集合和非泛型

什么是反射?

简而言之,反射是一种进入程序内部的方法,收集程序/代码的对象信息和/或在运行时调用这些信息。因此,借助反射,我们可以通过在 C#中编写代码来分析和评估我们的代码。要详细了解反射,让我们以class OddEven的例子来说明。这是这个类的部分代码:

public class OddEven
{
   public string PrintOddEven(int startNumber, int
   lastNumber)
   {
     return GetOddEvenWithinRange(startNumber,
     lastNumber);
   }
   public string PrintSingleOddEven(int number) => CheckSingleNumberOddEvenPrimeResult(number);
   private string CheckSingleNumberOddEvenPrimeResult(int
   number)
   {
      var result = string.Empty;
      result = CheckSingleNumberOddEvenPrimeResult(result,
      number);
      return result;
   }
   //Rest code is omitted
}

通过查看代码,我们可以说这段代码有一些公共方法和私有方法。公共方法利用私有方法来满足各种功能需求,并执行任务以解决我们需要识别奇数或偶数的实际问题。

当我们需要利用前面的类时,我们必须实例化这个类,然后调用它们的方法来获取结果。以下是我们如何利用这个简单类来获取结果:

class Program
{
   static void Main(string[] args)
   {
      int userInput;
      do
      {
         userInput = DisplayMenu();
         switch (userInput)
         {
            case 1:
            Console.Clear();
            Console.Write("Enter number: ");
            var number = Console.ReadLine();
            var objectOddEven = new OddEven();
            var result =           
            objectOddEven.PrintSingleOddEven
            (Convert.ToInt32(number));
            Console.WriteLine
            ($"Number:{number} is {result}");
            PressAnyKey();
            break;
            //Rest code is omitted
         } while (userInput != 3);
       }
    //Rest code is ommitted
}
PrintSingleOddEven to check whether an entered number is odd or even. The following screenshot shows the output of our implementation:

前面的代码显示了我们可以实现代码的一种方式。同样,我们可以使用相同的解决方案来分析代码。我们已经说过反射是分析我们的代码的一种方法。在接下来的部分,我们将实现和讨论类似实现的代码,但使用反射。

您需要添加以下 NuGet 包来使用反射,使用包管理器控制台:install-Package System.Reflection

Reflection to solve the same problem and achieve the same results:
class Program
{
   private static void Main(string[] args)
   {
      int userInput;
      do
      {
         userInput = DisplayMenu();
         switch (userInput)
         {
            //Code omitted
            case 2:
            Console.Clear();
            Console.Write("Enter number: ");
            var num = Console.ReadLine();
            Object objInstance = 
            Activator.CreateInstance(typeof(OddEven));
            MethodInfo method = 
            typeof(OddEven).GetMethod
            ("PrintSingleOddEven");
            object res = method.Invoke
            (objInstance, new object[] 
            { Convert.ToInt32(num) });
            Console.WriteLine($"Number:{num} is {res}");
            PressAnyKey();
            break;
         }
      } while (userInput != 3);
    }
   //code omitted
}
MethodInfo with the use of System.Reflection and thereafter invoking the method by passing the required parameters. The preceding example is the simplest one to showcase the power of Reflection; we can do more things with the use of Reflection.

在前面的代码中,我们可以使用Assembly.CreateInstance("OddEven")来代替Activator.CreateInstance(typeof(OddEven))Assembly.CreateInstance查看程序集的类型,并使用Activator.CreateInstance创建实例。有关AssemblyCreateInstance的更多信息,请参阅:docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.createinstance?view=netstandard-2.0#System_Reflection_Assembly_CreateInstance_System_String_

以下是前面代码的输出:

反射的应用

在前一节中,我们了解了反射以及如何利用Reflection的能力来分析代码。在本节中,我们将看到更复杂的场景,我们可以在其中使用Reflection并更详细地讨论System.TypeSystem.Reflection

获取类型信息

有一个System.Type类可用,它为我们提供了关于我们对象类型的完整信息:我们可以使用typeof来获取关于我们类的所有信息。让我们看下面的代码片段:

class Program
{
   private static void Main(string[] args)
   {
      int userInput;
      do
      {
         userInput = DisplayMenu();
         switch (userInput)
         {
            // code omitted
            case 3:
            Console.Clear();
            Console.WriteLine
            ("Getting information using 'typeof' operator
            for class 'Day05.Program");
            var typeInfo = typeof(Program);
            Console.WriteLine();
            Console.WriteLine("Analysis result(s):");
            Console.WriteLine
            ("=========================");
            Console.WriteLine($"Assembly:
            {typeInfo.AssemblyQualifiedName}");
            Console.WriteLine($"Name:{typeInfo.Name}");
            Console.WriteLine($"Full Name:
            {typeInfo.FullName}");
            Console.WriteLine($"Namespace:
            {typeInfo.Namespace}");
            Console.WriteLine
            ("=========================");
            PressAnyKey();
            break;
            code omitted
          }
       } while (userInput != 5);
   }
      //code omitted
}
typeof to gather the information on our class Program. The typeof operator represents a type declaration here; in our case, it is a type declaration of class Program. Here is the result of the preceding code:

在同一个节点上,我们可以使用System.Type类的GetType()方法,该方法获取类型并提供信息。让我们分析和讨论以下代码片段:

internal class Program
{
   private static void Main(string[] args)
   {
      int userInput;
      do
      {
         userInput = DisplayMenu();
         switch (userInput)
         {
            //code omitted
            case 4:
            Console.Clear();
            Console.WriteLine("Getting information using 
            'GetType()' method for class
            'Day05.Program'");
            var info = Type.GetType("Day05.Program");
            Console.WriteLine();
            Console.WriteLine("Analysis result(s):");
            Console.WriteLine
            ("=========================");
            Console.WriteLine($"Assembly:
            {info.AssemblyQualifiedName}");
            Console.WriteLine($"Name:{info.Name}");
            Console.WriteLine($"Full Name:
            {info.FullName}");
            Console.WriteLine($"Namespace: 
            {info.Namespace}");
            Console.WriteLine
            ("=========================");
            PressAnyKey();
            break;
         }
      } while (userInput != 5);
   }
 //code omitted
}
class Program with the use of GetMethod(), and it results in the following:

在前面的部分讨论的代码片段中,有一个代表System.Type类的类型,然后我们使用属性收集信息。这些属性在下表中解释:

属性名称 描述
名称 返回类型的名称,例如,Program
完整名称 返回类型的完全限定名称,不包括程序集名称,例如,Day05.Program
命名空间 返回类型的命名空间,例如,Day05。如果没有命名空间,则此属性返回 null

这些属性是只读的(属于抽象类System.Type);这意味着我们只能读取或获取结果,但不能设置值。

System.Reflection.TypeExtensions类具有我们分析和动态编写代码所需的一切。完整的源代码可在github.com/dotnet/corefx/blob/master/src/System.Reflection.TypeExtensions/src/System/Reflection/TypeExtensions.cs上找到。

本书不涵盖所有扩展方法的实现,因此我们添加了以下表格,其中包含所有重要扩展方法的详细信息:

方法名 描述 来源 ( github.com/dotnet/corefx/blob/master/src )
GetConstructor(Type type, Type[] types) 在提供的类型上执行,并返回System.Reflection.ConstructorInfo类型的输出 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
ConstructorInfo[] GetConstructors(Type type) 返回提供的类型的所有构造函数信息和System.Reflection.ConstructorInfo数组输出 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
ConstructorInfo[] GetConstructors(Type type, BindingFlags bindingAttr) 返回提供的类型和属性的所有构造函数信息 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
MemberInfo[] GetDefaultMembers(Type type) 获取提供的属性的访问权限,对于成员,对于给定类型,并输出System.Reflection.MemberInfo数组 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
EventInfo GetEvent(Type type, string name) 提供对System.Reflection.MemberInfoEventMetadata输出的访问 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
FieldInfo GetField(Type type, string name) 获取指定类型的字段信息,并返回提供的字段名称的System.Reflection.FieldInfo输出 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
MemberInfo[] GetMember(Type type, string name) 通过成员名称获取指定类型的成员信息,此方法输出System.Reflection.MemberInfo数组 /System.Reflection.Emit/ref/System.Reflection.Emit.cs
PropertyInfo[] GetProperties(Type type) 为指定类型提供所有属性,并输出为System.Reflection.PropertyInfo数组 /System.Reflection.Emit/ref/System.Reflection.Emit.cs

尝试使用一个简单的程序来实现所有扩展方法。

在之前的章节中,我们学习了如何使用Reflection来分析我们的已编译代码/应用程序。当我们有现有的代码时,Reflection可以很好地工作。想象一种情况,我们需要一些动态代码生成逻辑。假设我们需要生成一个简单的类,如下面的代码片段中所述:

public class MathClass
{
   private readonly int _num1;
   private readonly int _num2;
   public MathClass(int num1, int num2)
   {
      _num1 = num1;
      _num2 = num2;
   }
     public int Sum() => _num1 + _num2;
     public int Substract() => _num1 - _num2;
     public int Division() => _num1 / _num2;
     public int Mod() => _num1 % _num2;
}

仅使用Reflection无法创建或编写纯动态代码或即时代码。借助Reflection,我们可以分析我们的MathClass,但是我们可以使用Reflection.Emit来即时创建这个类。

动态代码生成超出了本书的范围。您可以参考以下主题获取更多信息:stackoverflow.com/questions/41784393/how-to-emit-a-type-in-net-core

委托和事件概述

在本节中,我们将讨论委托和事件的基础知识。委托和事件都是 C#语言最先进的特性。我们将在接下来的章节中详细了解这些内容。

委托

在 C#中,委托是类似于 C 和 C++中的函数指针的概念。委托只是一个引用类型的变量,它保存了一个方法的引用,并触发该方法。

我们可以使用委托实现后期绑定。在第七章,使用 C#理解面向对象编程中,我们将详细讨论后期绑定。

System.Delegate是所有委托派生的类。我们使用委托来实现事件。

声明委托类型

声明委托类型类似于方法签名类。我们只需要声明一个类型 public delegate string: PrintFizzBuzz(int number);。在前面的代码中,我们声明了一个委托类型。这个声明类似于一个抽象方法,不同之处在于委托声明有一个委托类型。我们只声明了一个委托类型PrintFizzBuzz,它接受一个 int 类型的参数并返回字符串的结果。我们只能声明 public 或 internal 可访问的委托。

默认情况下,委托的可访问性是 internal。

在前面的图中,我们可以分析委托声明的语法。如果我们看到这个图,我们会注意到它以 public 开头,然后是关键字 delegate,这告诉我们这是一个委托类型,字符串,它是一个返回类型,我们的语法以名称和传递参数结束。以下表定义了声明的主要部分:

语法部分 描述
修饰符 修饰符是委托类型的定义可访问性。这些修饰符只能是 public 或 internal,默认情况下委托类型的修饰符是 internal。
返回类型 委托可以返回或不返回结果;可以是任何类型或 void。
名称 声明的委托的名称。委托类型的名称遵循与典型类相同的规则,如第二天所讨论的。
参数列表 典型的参数列表;参数可以是任何类型。

委托的实例

在前一节中,我们创建了一个名为PrintFizzBuzz的委托类型。现在我们需要声明这种类型的一个实例,这样我们就可以在我们的代码中使用它。这类似于我们声明变量的方式-请参考第二天了解更多关于变量声明的内容。以下代码片段告诉我们如何声明我们委托类型的一个实例:

PrintFizzBuzz printFizzBuzz;

委托的使用

我们可以直接通过调用匹配方法来使用委托类型,这意味着委托类型调用相关方法。在下面的代码片段中,我们只是调用一个方法:

internal class Program
{
   private static PrintFizzBuzz _printFizzBuzz;
   private static void Main(string[] args)
   {
      int userInput;
      do
      {
         userInput = DisplayMenu();
         switch (userInput)
         {
            //code omitted
            case 6:
            Clear();
            Write("Enter number: ");
            var inputNum = ReadLine();
            _printFizzBuzz = FizzBuzz.PrintFizzBuzz;
            WriteLine($"Entered number:{inputNum} is
            {_printFizzBuzz(Convert.ToInt32(inputNum))}");
            PressAnyKey();
            break;
         }
      } while (userInput != 7);
   }

在前一节中编写的代码片段中,我们从用户那里获取输入,然后借助委托获得预期的结果。以下屏幕截图显示了前面代码的完整输出:

更高级的委托,即多播和强类型的委托将在第六天讨论。

事件

一般来说,每当事件出现时,我们可以考虑用户的行为或用户行为。我们日常生活中有一些例子;比如我们检查邮件,发送邮件等。像点击邮件客户端中的发送按钮或接收按钮这样的操作只是事件。

事件是类型的成员,而这个类型是委托类型。这些成员在触发时通知其他类型。

事件使用发布者-订阅者模型。发布者只是一个具有事件和委托定义的对象。另一方面,订阅者是接受事件并提供事件处理程序的对象(事件处理程序只是由发布者类中的委托调用的方法)。

声明事件

在声明事件之前,我们应该有一个委托类型,所以我们应该首先声明一个委托。以下代码片段显示了委托类型:

public delegate string FizzBuzzDelegate(int num);
The following code snippet shows event declaration:
public event FizzBuzzDelegate FizzBuzzEvent;
The following code snippet shows a complete implementation of an event to find FizzBuzz numbers:
public delegate string FizzBuzzDelegate(int num);
public class FizzBuzzImpl
{
   public FizzBuzzImpl()
   {
      FizzBuzzEvent += PrintFizzBuzz;
   }
      public event FizzBuzzDelegate FizzBuzzEvent;
      private string PrintFizzBuzz(int num) => FizzBuzz.PrintFizzBuzz(num);
      public string EventImplementation(int num)
   {
      var fizzBuzImpl = new FizzBuzzImpl();
      return fizzBuzImpl.FizzBuzzEvent(num);
   }
}
FizzBuzzEvent that is attached to a delegate type named FizzBuzzDelegate, which called a method PrintFizzBuzz on instantiation of our class named FizzBuzzImpl. Hence, whenever we call our event FizzBuzzEvent, it automatically calls a method PrintFizzBuzz and returns the expected results:

集合和非泛型

在第二天,我们学习了数组,它们是固定大小的,并且您可以使用它们来进行强类型列表对象。但是,如果我们想要将这些对象使用或组织到其他数据结构中,例如队列、列表、堆栈等,怎么办?所有这些都可以通过使用集合(System.Collections)来实现。

有多种方法可以使用集合来玩耍数据(存储和检索)。以下是我们可以使用的主要集合类。

System.Collections.NonGeneric (www.nuget.org/packages/System.Collections.NonGeneric/ )是一个 NuGet 包,它提供了所有非泛型类型,如ArrayListHashTableStackSortedListQueue等。

ArrayList

由于它是一个数组,它包含一个有序的对象集合,并且可以单独进行索引。由于这是一个非泛型类,因此它在System.Collections.NonGeneric的单独 NuGet 包中可用。要使用示例代码,您首先应安装此 NuGet 包。

声明 ArrayList

ArrayList:
ArrayList arrayList = new ArrayList();
ArrayList arrayList1 = new ArrayList(capacity);
ArrayList arrayList2 = new ArrayList(collection);
arrayList is initialized using the default constructor. arrayList1 is initialized for a specific initial capacity. arrayList2 is initialized using an element of another collection.

ArrayList的属性和方法对于向集合中添加、存储或移除数据项非常重要。ArrayList类有许多属性和方法可用。在接下来的部分中,我们将讨论常用的方法和属性。

属性

ArrayList的属性在分析现有的ArrayList时起着至关重要的作用;以下是常用的属性:

属性 描述

| Capacity | 一个 getter setter 属性;通过使用它,我们可以设置或获取ArrayList的元素数量。例如:

ArrayList arrayList = new ArrayList {Capacity = 50};

|

| Count | ArrayList包含的实际元素总数。请注意,此计数可能与容量不同。例如:

ArrayList arrayList = new ArrayList {Capacity = 50};
var numRandom = new Random(50);
for (var countIndex = 0; countIndex < 50; countIndex++)
arrayList.Add(numRandom.Next(50));

|

| IsFixedSize | 一个 getter 属性,根据ArrayList是否为固定大小返回 true/false。例如:

ArrayList arrayList = new ArrayList();
var arrayListIsFixedSize = arrayList.IsFixedSize;

|

方法

正如我们在前一节中讨论的,属性在我们使用ArrayList时起着重要作用。在同一节点上,方法为我们提供了一种在使用非泛型集合时添加、删除或执行其他操作的方式:

方法 描述

| Add (object value) | 将对象添加到ArrayList的末尾。例如:

ArrayList arrayList = new ArrayList {Capacity = 50};
var numRandom = new Random(50);
for (var countIndex = 0; countIndex < 50; countIndex++)
arrayList.Add(numRandom.Next(50));

|

| Void Clear() | 从ArrayList中移除所有元素。例如:

arrayList.Clear();

|

| Void Remove(object obj) | 从集合中移除第一次出现的元素。例如:

arrayList.Remove(15);

|

Void Sort() ArrayList中的所有元素进行排序。
ArrayList:
public void ArrayListExample(int count)
{
var arrayList = new ArrayList();
var numRandom = new Random(count);
WriteLine($"Creating an ArrayList with capacity: {count}");
for (var countIndex = 0; countIndex < count; countIndex++)
arrayList.Add(numRandom.Next(count));
WriteLine($"Capacity: {arrayList.Capacity}");
WriteLine($"Count: {arrayList.Count}");
Write("ArrayList original contents: ");
PrintArrayListContents(arrayList);
WriteLine();
arrayList.Reverse();
Write("ArrayList reversed contents: ");
PrintArrayListContents(arrayList);
WriteLine();
Write("ArrayList sorted Content: ");
arrayList.Sort();
PrintArrayListContents(arrayList);
WriteLine();
ReadKey();
}

以下是前面程序的输出:

您将在第六天学习所有集合和泛型的高级概念。

HashTable

hashTable是一种非泛型类型,它只是键/值对集合的表示,并且是根据键(即哈希码)组织的。当我们需要根据键访问数据时,建议使用hashTable

声明 HashTable

Hashtable可以通过初始化Hashtable类来声明;以下代码片段显示了相同的内容:

Hashtable hashtable = new Hashtable();

接下来我们将讨论HashTable的常用方法和属性。

属性

hashTable的属性在分析现有的HashTable时起着至关重要的作用;以下是常用的属性:

属性 描述

| Count | 一个 getter 属性;返回HashTable中键/值对的数量。例如:

var hashtable = new Hashtable
{
{1, "Gaurav Aroraa"},
{2, "Vikas Tiwari"},
{3, "Denim Pinto"},
{4, "Diwakar"},
{5, "Merint"}
};
var count = hashtable.Count;

|

| IsFixedSize | 一个 getter 属性;根据HashTable是否为固定大小返回 true/false。例如:

var hashtable = new Hashtable
{
{1, "Gaurav Aroraa"},
{2, "Vikas Tiwari"},
{3, "Denim Pinto"},
{4, "Diwakar"},
{5, "Merint"}
};
var fixedSize = hashtable.IsFixedSize ? " fixed size." : " not fixed size.";
WriteLine($"HashTable is {fixedSize} .");

|

| IsReadOnly | 一个 getter 属性;告诉我们HashTable是否是只读的。例如:

WriteLine($"HashTable is ReadOnly : {hashtable.IsReadOnly} ");

|

方法

HashTable的方法通过提供更多操作的方式来添加、删除和分析集合,如下表所述:

方法 描述

| Add (object key, object value) | 向HashTable添加特定键和值的元素。例如:

var hashtable = new Hashtable
hashtable.Add(11,"Rama");

|

| Void Clear() | 从HashTable中移除所有元素。例如:

hashtable.Clear();

|

| Void Remove (object key) | 从 HashTable 中移除指定键的元素。例如:

hashtable.Remove(15);

|

HashTable collection, and will try to reiterate its keys:
public void HashTableExample()
{
   WriteLine("Creating HashTable");
   var hashtable = new Hashtable
   {
      {1, "Gaurav Aroraa"},
      {2, "Vikas Tiwari"},
      {3, "Denim Pinto"},
      {4, "Diwakar"},
      {5, "Merint"}
   };
      WriteLine("Reading HashTable Keys");
      foreach (var hashtableKey in hashtable.Keys)
   {
      WriteLine($"Key :{hashtableKey} - value :
      {hashtable[hashtableKey]}");
   }
}

以下是前面代码的输出:

SortedList

SortedList类是一个非泛型类型,它只是一个基于键的键/值对集合的表示,按键排序。SortedListArrayListHashTable的组合。因此,我们可以通过键或索引访问元素。

SortedList 的声明

SortedList可以通过初始化SortedList类来声明;以下代码片段显示了相同的方式:

SortedList sortedList = new SortedList();

接下来我们将讨论SortedList的常用方法和属性。

属性

SortedList的属性在分析现有的SortedList时起着至关重要的作用;以下是常用的属性:

属性 描述

| Capacity | 一个 getter setter 属性;通过使用这个属性,我们可以设置或获取SortedList的容量。例如:

var sortedList = new SortedList
{
{1, "Gaurav Aroraa"},
{2, "Vikas Tiwari"},
{3, "Denim Pinto"},
{4, "Diwakar"},
{5, "Merint"},
{11, "Rama"}
};
WriteLine($"Capacity: {sortedList.Capacity}");

|

| Count | 一个 getter 属性;返回HashTable中键/值对的数量。例如:

var sortedList = new SortedList
{
{1, "Gaurav Aroraa"},
{2, "Vikas Tiwari"},
{3, "Denim Pinto"},
{4, "Diwakar"},
{5, "Merint"},
{11, "Rama"}
};
WriteLine($"Capacity: {sortedList.Count}");

|

| IsFixedSize | 一个 getter 属性;根据SortedList是否是固定大小返回 true/false。例如:

var sortedList = new SortedList
{
{1, "Gaurav Aroraa"},
{2, "Vikas Tiwari"},
{3, "Denim Pinto"},
{4, "Diwakar"},
{5, "Merint"},
{11, "Rama"}
};
ar fixedSize = sortedList.IsFixedSize ? " fixed size." : " not fixed size.";
WriteLine($"SortedList is {fixedSize} .");

|

| IsReadOnly | 一个 getter 属性;告诉我们SortedList是否是只读的。例如:

WriteLine($"SortedList is ReadOnly : {sortedList.IsReadOnly} ");

|

方法

以下是常用的方法:

方法 描述

| Add (object key, object value) | 向SortedList添加特定键和值的元素。例如:

var sortedList = new SortedList
sortedList.Add(11,"Rama");

|

| Void Clear() | 从SortedList中移除所有元素。例如:

sortedList.Clear();

|

| Void Remove (object key) | 从SortedList中移除指定键的元素。例如:

sortedList.Remove(15);

|

在接下来的部分中,我们将使用前面部分提到的属性和方法来实现代码。让我们使用SortedList收集《7 天学会 C#》一书的所有利益相关者列表:

public void SortedListExample()
{
   WriteLine("Creating SortedList");
   var sortedList = new SortedList
   {
      {1, "Gaurav Aroraa"},
      {2, "Vikas Tiwari"},
      {3, "Denim Pinto"},
      {4, "Diwakar"},
      {5, "Merint"},
      {11, "Rama"}
   };
   WriteLine("Reading SortedList Keys");
   WriteLine($"Capacity: {sortedList.Capacity}");
   WriteLine($"Count: {sortedList.Count}");
   var fixedSize = sortedList.IsFixedSize ? " fixed
   size." :" not fixed size.";
   WriteLine($"SortedList is {fixedSize} .");
   WriteLine($"SortedList is ReadOnly :
   {sortedList.IsReadOnly} ");
   foreach (var key in sortedList.Keys)
   {
      WriteLine($"Key :{key} - value :
      {sortedList[key]}");
   }
}

以下是前面代码的输出:

一个非泛型类型,表示对象的后进先出(LIFO)集合。它包含两个主要的操作:PushPop。当我们向列表中插入一个项目时,称为推入,当我们从列表中提取/移除一个项目时,称为弹出。当我们在不移除列表中的项目的情况下获取一个对象时,称为查看。

栈的声明

Stack的声明与我们声明其他非泛型类型的方式非常相似。以下代码片段显示了相同的方式:

Stack stackList = new Stack();

我们将讨论Stack的常用方法和属性。

属性

Stack类只有一个属性,用于告诉计数:

属性 描述

| Count | 一个 getter 属性;返回栈包含的元素数量。例如:

var stackList = new Stack();
stackList.Push("Gaurav Aroraa");
stackList.Push("Vikas Tiwari");
stackList.Push("Denim Pinto");
stackList.Push("Diwakar");
stackList.Push("Merint");
WriteLine($"Count: {stackList.Count}");

|

方法

以下是常用的方法:

方法 描述

| Object Peek() | 返回栈顶的对象,但不移除它。例如:

WriteLine($"Next value without removing:{stackList.Peek()}");

|

| Object Pop() | 移除并返回栈顶的对象。例如:

WriteLine($"Remove item: {stackList.Pop()}");

|

| Void Push(object obj) | 在栈顶插入一个对象。例如:

WriteLine("Adding more items.");
stackList.Push("Rama");
stackList.Push("Shama");

|

| Void Clear() | 从栈中移除所有元素。例如:

var stackList = new Stack();
stackList.Push("Gaurav Aroraa");
stackList.Push("Vikas Tiwari");
stackList.Push("Denim Pinto");
stackList.Push("Diwakar");
stackList.Push("Merint");
stackList.Clear();

|

以下是栈的完整示例:

public void StackExample()
{
   WriteLine("Creating Stack");
   var stackList = new Stack();
   stackList.Push("Gaurav Aroraa");
   stackList.Push("Vikas Tiwari");
   stackList.Push("Denim Pinto");
   stackList.Push("Diwakar");
   stackList.Push("Merint");
   WriteLine("Reading stack items");
   ReadingStack(stackList);
   WriteLine();
   WriteLine($"Count: {stackList.Count}");
   WriteLine("Adding more items.");
   stackList.Push("Rama");
   stackList.Push("Shama");
   WriteLine();
   WriteLine($"Count: {stackList.Count}");
   WriteLine($"Next value without removing:
   {stackList.Peek()}");
   WriteLine();
   WriteLine("Reading stack items.");
   ReadingStack(stackList);
   WriteLine();
   WriteLine("Remove value");
   stackList.Pop();
   WriteLine();
   WriteLine("Reading stack items after removing an
   item.");
   ReadingStack(stackList);
   ReadLine();
}

前面的代码使用Stack捕获了《7 天学会 C#》一书的利益相关者列表,并展示了前几节讨论的属性和方法的用法。这段代码产生了以下截图中显示的输出:

Queue

队列只是一个表示对象的 FIFO 集合的非泛型类型。queue有两个主要操作:添加项目时称为 enqueuer,移除项目时称为dequeue

队列的声明

Queue的声明与我们声明其他非泛型类型的方式非常相似。以下代码片段显示了相同的方式:

Queue queue = new Queue();

我们将讨论Queue的常用方法和属性。

属性

Queue类只有一个属性,用于告诉计数:

属性 描述

| Count | 一个 getter 属性;返回queue包含的元素数量。例如:

Queue queue = new Queue();
queue.Enqueue("Gaurav Aroraa");
queue.Enqueue("Vikas Tiwari");
queue.Enqueue("Denim Pinto");
queue.Enqueue("Diwakar");
queue.Enqueue("Merint");
WriteLine($"Count: {queue.Count}");

|

方法

以下是常用的方法:

方法 描述

| Object Peek() | 返回queue顶部的对象,但不移除它。例如:

WriteLine($"Next value without removing:{stackList.Peek()}");

|

| Object Dequeue() | 移除并返回queue开头的对象。例如:

WriteLine($"Remove item: {queue.Dequeue()}");

|

| Void Enqueue (object obj) | 在queue的末尾插入一个对象。例如:

WriteLine("Adding more items.");
queue.Enqueue("Rama");

|

| Void Clear() | 从Queue中移除所有元素。例如:

Queue queue = new Queue();
queue.Enqueue("Gaurav Aroraa");
queue.Enqueue("Vikas Tiwari");
queue.Enqueue("Denim Pinto");
queue.Enqueue("Diwakar");
queue.Enqueue("Merint");
queue.Clear();

|

Enqueue and Dequeue methods to add and remove the items from the collections stored using queue:
public void QueueExample()
{
   WriteLine("Creating Queue");
   var queue = new Queue();
   queue.Enqueue("Gaurav Aroraa");
   queue.Enqueue("Vikas Tiwari");
   queue.Enqueue("Denim Pinto");
   queue.Enqueue("Diwakar");
   queue.Enqueue("Merint");
   WriteLine("Reading Queue items");
   ReadingQueue(queue);
   WriteLine();
   WriteLine($"Count: {queue.Count}");
   WriteLine("Adding more items.");
   queue.Enqueue("Rama");
   queue.Enqueue("Shama");
   WriteLine();
   WriteLine($"Count: {queue.Count}");
   WriteLine($"Next value without removing:
   {queue.Peek()}");
   WriteLine();
   WriteLine("Reading queue items.");
   ReadingQueue(queue);
   WriteLine();
   WriteLine($"Remove item: {queue.Dequeue()}");
   WriteLine();
   WriteLine("Reading queue items after removing an
   item.");
   ReadingQueue(queue);
}

以下是前述代码的输出:

BitArray

BitArray 实际上是一个管理位值数组的数组。这些值被表示为布尔值。True 表示位是ON(1),false 表示位是OFF(0)。当我们需要存储位时,这个非泛型集合类是很重要的。

BitArray 的实现没有涵盖。请参考本章末尾的练习来实现 BitArray。

我们在本章讨论了非泛型集合。泛型集合超出了本章的范围;我们将在第六天讨论它们。要比较不同的集合,请参考www.codeproject.com/Articles/832189/List-vs-IEnumerable-vs-IQueryable-vs-ICollection-v

动手练习

解决以下问题,涵盖了今天学习的概念:

  1. 什么是反射?编写一个使用System.Type的简短程序。

  2. 创建一个包含至少三个属性、两个构造函数、两个公共方法和三个私有方法的类,并实现至少一个接口。

  3. 编写一个程序,使用System.Reflection.Extensins来评估问题二中创建的类。

  4. 学习 NuGet 包System.Reflection.TypeExtensions,并编写一个程序来实现它的所有功能。

  5. 学习 NuGet 包System.Reflection. Primitives,并编写一个程序来实现它的所有功能。

  6. 委托类型是什么,如何定义多播委托?

  7. 什么是事件?事件是基于发布者-订阅者模型的吗?用一个现实世界的例子来展示这一点。

  8. 编写一个使用委托和事件的程序,以获得类似于github.com/garora/TDD-Katas#string-sum-kata的输出。

  9. 定义集合并实现非泛型类型。

参考我们从第一天开始的问题,元音计数问题,并使用所有非泛型集合类型来实现它。

重温第 05 天

今天,我们讨论了 C#的非常重要的概念,涵盖了反射、集合、委托和事件。

我们在代码分析方法中讨论了反射的重要性。在讨论过程中,我们实现了展示反射的强大之处的代码,分析了完整的代码。

然后我们讨论了委托和事件,以及委托和事件在 C#中的工作原理。我们还实现了委托和事件。

我们详细讨论了 C#语言的一个重要特性,即非泛型类型,即ArrayListHashTableSortedListQueueStack等。我们使用 C# 7.0 代码实现了所有这些。

第六章:第 06 天-深入探讨高级概念

今天是我们七天学习系列的第六天。在第五天,我们讨论了 C#语言的重要概念,并通过反射、集合、委托和事件进行了探讨。我们使用了代码片段来探讨非泛型集合。今天,我们将讨论使用泛型类型的集合的主要功能,然后我们将涵盖预处理指令和属性。

在本章中,我们将涵盖以下主题:

  • 玩转集合和泛型

  • 使用属性美化代码

  • 利用预处理指令

  • 开始使用 LINQ

  • 编写不安全的代码

  • 编写异步代码

  • 重温第六天

  • 实际练习

玩转集合和泛型

对于我们来说,集合并不新鲜,因为我们在第五天已经讨论了非泛型集合。因此,我们也有泛型集合。在本节中,我们将讨论使用代码示例的集合和泛型的所有内容。

理解集合类及其用法

如第五天讨论的那样,集合类是专门的类,用于数据交互(存储和检索)。我们已经讨论了各种集合类,包括

栈、队列、列表和哈希表,并且我们已经使用了System.Collections.NonGeneric命名空间编写了代码。以下表格为我们提供了非泛型集合类的用法和含义的概述:

属性 描述 用法
ArrayList 名称本身描述了它包含一个可以使用索引访问的有序集合。我们可以这样声明ArrayListArrayList arrayList = new ArrayList(); 在第二天,我们讨论了数组,并学习了如何访问数组的各个元素。在ArrayList的情况下,我们可以获得各种方法来添加或移除集合元素的好处,就像在第五天讨论的那样。

| HashTable | HashTable只是键值对集合的表示,并且根据键进行组织,键实际上就是哈希码。当我们需要根据键访问数据时,建议使用HashTable。我们可以这样声明HashTable

Hashtable hashtable = new Hashtable(); | 当我们需要使用键访问元素时,HashTable非常有用。在这种情况下,我们有一个键,需要根据键在集合中找到值。 |

SortedList SortedList类只是键值对集合的表示,并且根据键进行组织并按键排序。SortedList类是ArrayListHashTable的组合。因此,我们可以使用键或索引访问元素。我们可以这样声明SortedListSortedList sortedList = new SortedList(); 如所述,排序列表是数组和哈希表的组合。可以使用键或索引访问项目。当使用索引访问项目时,它是ArrayList;另一方面,当使用哈希键访问项目时,它是HashTableSortedList的主要特点是项目的集合始终按键值排序。
Stack 栈表示对象的集合;对象按照后进先出LIFO)的顺序可访问。它包含两个主要操作:push 和 pop。每当我们向列表中插入一个项目时,称为 push,当我们从列表中提取/移除一个项目时,称为 pop。当我们从列表中获取一个对象而不移除该项目时,称为 peeking。我们可以这样声明它:Stack stackList = new Stack(); 当需要首先检索插入的项目时,这是很重要的。
Queue 队列代表一个先进先出(FIFO)的对象集合。队列中有两个主要的操作--添加一个项目称为入队,移除一个项目称为出队。我们可以声明一个队列如下:Queue queue = new Queue(); 当需要首先检索插入的项目时,这一点很重要。
BitArray BitArray只是一个管理位值数组的数组。这些值被表示为布尔值。True 表示ON(1),False 表示OFF(0)。我们可以这样声明BitArrayBitArray bitArray = new BitArray(8); 当我们需要存储位时,这个非泛型的集合类很重要。

前面的表只显示了非泛型的集合类。借助泛型,我们还可以通过使用System.Collections命名空间来实现泛型集合类。在接下来的部分,我们将讨论泛型集合类。

性能 - BitArray 与 boolArray

在前面的表中,我们讨论了BitArray只是一个管理 true 或 false 值(01)的数组。但在内部,BitArray对每个元素执行了大约 8 次的 Byte 操作,并进行了各种逻辑操作,需要更多的 CPU 周期。另一方面,boolArraybool[])将每个元素存储为 1 字节,因此它占用更多的内存,但需要更少的 CPU 周期。BitArray优于bool[]是内存优化器。

让我们考虑以下性能测试,并看看BitArray的表现如何:

private static long BitArrayTest(int max) 
{ 
    Stopwatch stopwatch = Stopwatch.StartNew(); 
    var bitarray = new BitArray(max); 
    for (int index = 0; index < bitarray.Length; index++) 
    { 
        bitarray[index] = !bitarray[index]; 
        WriteLine($"'bitarray[{index}]' = {bitarray[index]}"); 
    } 
    stopwatch.Stop(); 
    return stopwatch.ElapsedMilliseconds; 
} 
BitArray performance by applying a very simple test, where we run a for loop up to the maximum count of int MaxValue.
bool[] to make this test simpler; we just initiated a for loop up to the maximum value of int.MaxValue:
private static long BoolArrayTest(int max) 
{ 
    Stopwatch stopwatch = Stopwatch.StartNew(); 
    var boolArray = new bool[max]; 
    for (int index = 0; index < boolArray.Length; index++) 
    { 
        boolArray[index] = !boolArray[index]; 
        WriteLine($"'boolArray[{index}]' = {boolArray[index]}"); 
    } 
    stopwatch.Stop(); 
    return stopwatch.ElapsedMilliseconds; 
} 
BitArrayTest and BoolArrayTest methods:
private static void BitArrayBoolArrayPerformance() 
{ 
    //This is a simple test 
    //Not testing bitwiseshift  etc. 
    WriteLine("BitArray vs. Bool Array performance test.\n"); 
    WriteLine($"Total elements of bit array: {int.MaxValue}"); 
    PressAnyKey(); 
    WriteLine("Starting BitArray Test:"); 
    var bitArrayTestResult = BitArrayTest(int.MaxValue); 
    WriteLine("Ending BitArray Test:"); 
    WriteLine($"Total timeElapsed: {bitArrayTestResult}"); 

    WriteLine("\nStarting BoolArray Test:"); 
    WriteLine($"Total elements of bit array: {int.MaxValue}"); 
    PressAnyKey(); 
    var boolArrayTestResult = BoolArrayTest(int.MaxValue); 
    WriteLine("Ending BitArray Test:"); 
    WriteLine($"Total timeElapsed: {boolArrayTestResult}"); 
} 

在我的机器上,BitArrayTest花费了 6 秒,而BoolArrayTest花费了 15 秒。

从前面的测试中,我们可以得出结论,布尔数组占用了可以表示这些值的 8 倍大小/空间。简单来说,布尔数组需要每个元素 1 字节的空间。

理解泛型及其用法

用简单的话来说,借助泛型,我们可以创建或编写一个类的代码,该类旨在接受为其编写的不同数据类型。比如说,如果一个泛型类被编写成接受一个结构,那么它将接受 int、string 或自定义结构。这个类也被称为泛型类。当我们声明这个泛型类的实例时,它更加神奇地允许我们定义数据类型。让我们来学习下面的代码片段,我们在其中定义了一个泛型类,并在创建其实例时提供了数据类型:

    IList<Person> persons = new List<Person>()

persons variable of a generic type, List. Here, we have Person as a strong type. The following is the complete code snippet that populates this strongly typed list:
private static IEnumerable<Person> CreatePersonList() 
        { 
            IList<Person> persons = new List<Person> 
            { 
                new Person 
                { 
                    FirstName = "Denim", 
                    LastName = "Pinto", 
                    Age = 31 
                }, 
                new Person 
                { 
                    FirstName = "Vikas", 
                    LastName = "Tiwari", 
                    Age = 25 
                }, 
                new Person 
                { 
                    FirstName = "Shivprasad", 
                    LastName = "Koirala", 
                    Age = 40 
                }, 
                new Person 
                { 
                    FirstName = "Gaurav", 
                    LastName = "Aroraa", 
                    Age = 43 
                } 
            }; 

            return persons; 
        } 
Person type and its collection items. These items can be iterated as mentioned in the following code snippet:
private static void Main(string[] args) 
        { 
            WriteLine("Person list:"); 
            foreach (var person in Person.GetPersonList()) 
            { 
                WriteLine($"Name:{person.FirstName} {person.LastName}"); 
                WriteLine($"Age:{person.Age}"); 
            } 
            ReadLine(); 
        } 

在运行前面的代码片段后,我们将得到以下输出:

我们可以创建一个泛型列表到一个强类型的列表,它可以接受Person以外的类型。为此,我们只需要创建一个这样的列表:

private IEnumerable<T> CreateGenericList<T>() 
{ 
    IList<T> persons = new List<T>(); 

    //other stuffs 

    return persons; 
} 
T could be Person or any related type.

集合和泛型

第二天,你学习了固定大小的数组。你可以使用固定大小的数组来创建强类型的列表对象。但是,如果我们想要将这些对象用或组织到其他数据结构中,比如队列、列表、栈等,该怎么办?我们可以通过使用集合(System.Collections)来实现所有这些。

System.Collections (www.nuget.org/packages/System.Collections/ )是一个 NuGet 包,提供了所有泛型类型,以下是经常使用的类型:

泛型集合类型 描述
System.Collections.Generic.List<T> 一个强类型的泛型列表
System.Collections.Generic.Dictionary<TKey, TValue> 一个带有键值对的强类型泛型字典
System.Collections.Generic.Queue<T> 一个泛型Queue
System.Collections.Generic.Stack<T> 一个泛型Stack
System.Collections.Generic.HashSet<T> 一个泛型HashSet
System.Collections.Generic.LinkedList<T> 一个泛型LinkedList
System.Collections.Generic.SortedDictionary<TKey, TValue> 一个带有键值对集合并按键排序的泛型SortedDictionary

上述表格只是System.Collections.Generics命名空间的泛型类的概述。在接下来的部分中,我们将通过代码示例详细讨论泛型集合。

有关System.Collections.Generics命名空间的完整类、结构和接口列表,请访问官方文档链接docs.microsoft.com/en-us/dotnet/api/system.collections.generic?view=netcore-2.0

我们为什么要使用泛型?

对于非泛型列表,我们使用来自对象类型的通用基类的集合[docs.microsoft.com/en-us/dotnet/api/system.object],这在编译时不是类型安全的。假设我们正在使用一个ArrayList的非泛型集合;请参考以下代码片段以了解更多详情:

ArrayList authorArrayList = new ArrayList {"Gaurav Aroraa", "43"}; 
foreach (string author in authorArrayList) 
{ 
    WriteLine($"Name:{author}"); 
} 

在这里,我们有一个包含字符串值的ArrayList。在这里,我们将年龄作为字符串,实际上应该是 int。让我们再拿一个 ArrayList,其中年龄是 int:

ArrayList editorArrayList = new ArrayList { "Vikas Tiwari", 25 }; 
foreach (int editor in editorArrayList) 
{ 
    WriteLine($"Name:{editor}"); 
} 

在这种情况下,我们的代码可以编译,但它会在运行时抛出类型转换异常。因此,我们的ArrayList没有编译时类型检查:

通过查看上述代码,我们可以很容易地理解为什么在编译时没有错误;这是因为ArrayList接受任何类型(值和引用),然后将其转换为.NET 的通用基本类型,即对象。但是当我们运行代码时,它需要实际类型,例如,如果它被定义为字符串,那么在运行时它应该是字符串类型而不是对象类型。因此,我们会得到运行时异常。

ArrayList中对象的转换、装箱和拆箱活动会影响性能,这取决于ArrayList的大小以及您正在迭代的数据有多大。

通过上述代码示例,我们知道了非泛型ArrayList的两个缺点:

  1. 它不是编译时类型安全的。

  2. 在处理大数据时会影响性能。

  3. ArrayList将所有内容转换为对象,因此无法在编译时阻止添加任何类型的项目。例如,在上述代码片段中,我们可以输入 int 和/或字符串类型的项目。

为了克服这些问题/缺点,我们有通用集合,它们阻止我们提供除了预期类型之外的任何内容。考虑以下代码片段:

List<string> authorName = new List<string> {"Gaurav Aroraa"}; 

我们有一个List,它被定义为只获取字符串类型的项目。因此,我们只能在这里添加字符串类型的值。现在考虑以下内容:

List<string> authorName = new List<string>(); 
authorName.Add("Gaurav Aroraa"); 
authorName.Add(43); 

在这里,我们试图提供一个 int 类型的项目(记住我们在ArrayList的情况下也做了同样的事情)。现在,我们得到了一个与转换相关的编译时错误,因此,一个定义为只接受字符串类型项目的泛型列表具有阻止客户端输入除字符串以外的任何类型项目的能力。如果我们将鼠标悬停在43上,它会显示完整的错误;请参考以下图片:

在上述代码片段中,我们通过声明一个字符串列表解决了一个问题,它只允许我们输入字符串值,因此在作者的情况下,我们只能输入作者的姓名而不是作者的年龄。您可能会认为,如果我们需要另一个类型为 int 的列表,它可以让我们输入作者的年龄,那么为什么我们要使用泛型集合?目前,我们只需要两个项目--姓名和年龄--因此我们在此节点上创建了两个不同类型的列表,一个是字符串类型,一个是 int 类型。如果我们需要另一种类型的项目,那么我们会再创建一个新的列表。这是当我们有多种类型的事物时,例如字符串、int、decimal 等。我们可以创建我们自己的类型。考虑以下泛型列表的声明:

List<Person> persons = new List<Person>(); 

我们有一个Person类型的List。这个泛型列表将允许所有在这个类型中定义的项目。以下是我们的Person类:

internal class Person 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public int Age { get; set; } 
} 

我们的Person类包含三个属性,两个是字符串类型,一个是整数类型。在这里,我们有了解决前一节中讨论的问题的完整解决方案。借助于这个Person类型的List,我们可以输入字符串和/或整数类型的项目。以下代码片段展示了这一点:

private static void PersonList() 
{ 
    List<Person> persons = new List<Person> 
    { 
        new Person 
        { 
            FirstName = "Gaurav", 
            LastName = "Aroraa", 
            Age = 43 
        } 
    }; 
    WriteLine("Person list:"); 
    foreach (var person in persons) 
    { 
        WriteLine($"Name:{person.FirstName} {person.LastName}"); 
        WriteLine($"Age:{person.Age}"); 
    } 
} 

运行此代码后,我们的输出将如下所示:

我们的Person类型的List将比ArrayList更高效,因为在我们的泛型类中,没有隐式类型转换为对象;项目实际上是它们期望的类型。

讨论约束

在前一节中,我们讨论了Person类型的List如何接受其定义类型的所有项目。在我们的示例代码中,我们只使用了字符串和整数数据类型,但在泛型中,您可以使用任何数据类型,包括整数、浮点数、双精度等。另一方面,可能存在一些情况,我们希望在泛型中将我们的使用限制在少数数据类型或特定数据类型。为了实现这一点,有泛型约束。考虑以下代码片段:

public class GenericConstraint<T> where T:class 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 
} 

在这里,我们的类是一个泛型类。GenericConstraint,类型为T,实际上是一个引用类型;因此,我们创建了这个类来仅接受引用类型。这个类有一个ImplementIt方法,它接受一个T类型的参数,并返回一个T类型的值。

查看docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-type-parameters以了解有关泛型类型参数指南的更多信息。

以下声明是有效的,因为这些是引用类型:

GenericConstraint<string> genericConstraint = new GenericConstraint<string>(); 
Person person = genericPersonConstraint.ImplementIt(new Person()); 

以下是一个无效声明,因为这是值类型,不适用于当前的泛型类:

GenericConstraint<int> genericConstraint = new GenericConstraint<int>(); 

第二天,我们学到 int 是一个值类型,而不是引用类型。前面的声明会导致编译时错误。在 Visual Studio 中,您将看到以下错误:

因此,借助泛型约束,我们限制了我们的类不接受除引用类型之外的任何类型。

约束基本上是一种行为,通过它您可以保护您的泛型类,防止客户端在实例化类时使用任何其他类型。如果客户端代码尝试提供不允许的类型,这将导致编译时错误。上下文关键字where帮助我们定义约束。

在现实世界中,您可以定义各种类型的约束,这些约束将限制客户端代码创建任何不需要的情况。让我们通过示例讨论这些类型:

值类型

此约束是使用上下文关键字where T: struct定义的。有了这个约束,客户端的代码应该包含一个值类型的参数;在这里,除了 Nullable 之外的任何值都可以指定。

示例

以下是声明带有值类型约束的泛型类的代码片段:

public class ValueTypeConstraint<T> where T : struct 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 
} 

用法

以下是描述带有值类型约束的泛型类的客户端代码的代码片段:

private static void ImplementValueTypeGenericClass() 
{ 
    const int age = 43; 
    ValueTypeConstraint<int> valueTypeConstraint = new
    ValueTypeConstraint<int>(); 
    WriteLine($"Age:{valueTypeConstraint.ImplementIt(age)}"); 

} 

引用类型

此约束是使用上下文关键字where T:class定义的。使用这个约束,客户端代码被限制不能提供除引用类型之外的任何类型。有效类型包括类、接口、委托和数组。

示例

以下代码片段声明了一个带有引用类型约束的泛型类:

public class ReferenceTypeConstraint<T> where T:class 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 
} 

用法

以下代码片段描述了带有引用类型约束的泛型类的客户端代码:

private static void ImplementReferenceTypeGenericClass() 
{ 
    const string thisIsAuthorName = "Gaurav Aroraa"; 
    ReferenceTypeConstraint<string> referenceTypeConstraint = new ReferenceTypeConstraint<string>(); 
    WriteLine($"Name:{referenceTypeConstraint.ImplementIt(thisIsAuthorName)}"); 

    ReferenceTypeConstraint<Person> referenceTypePersonConstraint = new ReferenceTypeConstraint<Person>(); 

    Person person = referenceTypePersonConstraint.ImplementIt(new Person 
    { 
        FirstName = "Gaurav", 
        LastName = "Aroraa", 
        Age = 43 
    }); 
    WriteLine($"Name:{person.FirstName}{person.LastName}"); 
    WriteLine($"Age:{person.Age}"); 
} 

默认构造函数

这个约束是用上下文关键字where T: new()定义的,它限制了泛型类型参数不能定义默认构造函数。还有一个必须的条件是类型T的参数必须有一个公共的无参数构造函数。当与其他约束一起使用时,new()约束必须在最后指定。

示例

以下代码片段声明了一个带有默认构造函数约束的通用类:

public class DefaultConstructorConstraint<T> where T : new() 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 
} 

用法

以下代码片段描述了带有默认构造函数约束的通用类的客户端代码:

private static void ImplementDefaultConstructorGenericClass() 
{ 
    DefaultConstructorConstraint<ClassWithDefautConstructor>
    constructorConstraint = new
    DefaultConstructorConstraint<ClassWithDefautConstructor>(); 
    var result = constructorConstraint.ImplementIt(new
    ClassWithDefautConstructor { Name = "Gaurav Aroraa" }); 
    WriteLine($"Name:{result.Name}"); 
} 

基类约束

这个约束是用上下文关键字where T: <BaseClass>定义的。这个约束限制了所有客户端代码,其中提供的参数不是指定基类的或不是派生自指定基类的。

示例

以下代码片段声明了一个带有基类约束的通用类:

public class BaseClassConstraint<T> where T:Person 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 
} 

用法

以下是一个代码片段,描述了带有基类约束的通用类的客户端代码:

private static void ImplementBaseClassConstraint() 
{ 
    BaseClassConstraint<Author>baseClassConstraint = new BaseClassConstraint<Author>(); 
    var result = baseClassConstraint.ImplementIt(new Author 
    { 
        FirstName = "Shivprasad", 
        LastName = "Koirala", 
         Age = 40 
    }); 

    WriteLine($"Name:{result.FirstName} {result.LastName}"); 
    WriteLine($"Age:{result.Age}"); 
} 

接口约束

这个约束是用上下文关键字where T:<interface name>定义的。客户端代码必须提供一个实现指定参数的类型的参数。在这个约束中可能定义多个接口。

示例

以下代码片段声明了一个带有接口约束的通用类:

public class InterfaceConstraint<T>:IDisposable where T : IDisposable 
{ 
    public T ImplementIt(T value) 
    { 
        return value; 
    } 

    public void Dispose() 
    { 
        //dispose stuff goes here 
    } 
} 

用法

以下代码片段描述了带有接口约束的通用类的客户端代码:

private static void ImplementInterfaceConstraint() 
{ 
    InterfaceConstraint<EntityClass> entityConstraint = new InterfaceConstraint<EntityClass>(); 
    var result=entityConstraint.ImplementIt(new EntityClass {Name = "Gaurav Aroraa"}); 
    WriteLine($"Name:{result.Name}"); 
} 

在本节中,我们讨论了泛型和集合,包括各种类型的泛型,我们还提到了为什么应该使用泛型。

有关泛型的更多详细信息,请访问官方文档docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/

使用属性美化代码

属性提供了一种将信息与代码关联起来的方式。这些信息可以是简单的消息/警告,也可以包含复杂的操作或代码本身。这些只需用标签声明即可。这些还可以通过提供内置或自定义属性来美化我们的代码。考虑以下代码:

private void PeerOperation() 
{ 
    //other stuffs 
    WriteLine("Level1 is completed."); 
    //other stuffs 
} 

在这种方法中,我们显示一个信息消息来通知对等方。前面的方法将通过属性的帮助进行装饰。考虑以下代码:

[PeerInformation("Level1 is completed.")] 
private void PeerOperation() 
{ 
    //other stuffs 
} 

现在,我们可以看到我们只是用属性装饰了我们的方法。

根据官方文档[docs.microsoft.com/en-us/dotnet/csharp/tutorials/attributes],属性提供了一种以声明方式将信息与代码关联起来的方式。它们还可以提供一个可重用的元素,可以应用于各种目标。

属性可以用于以下目的:

  • 添加元数据信息

  • 添加注释、描述、编译器指令等

在接下来的部分中,我们将详细讨论属性,包括代码示例。

属性的类型

在前面的部分中,我们讨论了属性,这些属性帮助我们装饰和美化我们的代码。在本节中,我们将详细讨论各种类型的属性。

AttributeUsage

这是一个在框架中预定义的属性。它限制了属性的使用;换句话说,它告诉属性可以用于哪种类型的项目,也就是属性目标。这些可以是以下中的所有或一个:

  • 程序集

  • 构造函数

  • 委托

  • 枚举

  • 事件

  • 字段

  • GenericParameter

  • 接口

  • 方法

  • 模块

  • 参数

  • 属性

  • 返回值

  • 结构

默认情况下,属性可以是任何类型的目标,除非你明确指定。

示例

以下属性被创建用于仅用于类:

[AttributeUsage(AttributeTargets.Class)] 
public class PeerInformationAttribute : Attribute 
{ 
    public PeerInformationAttribute(string information) 
    { 
        WriteLine(information); 
    } 
} 

在上述代码中,我们为类的唯一使用定义了属性。如果您尝试将此属性用于类以外的其他内容,则会收到编译时错误。请参阅以下图像,显示了一个为方法上的属性显示错误的图像,实际上该属性仅用于类:

过时

在某些情况下,您可能希望为特定代码引发警告,以便在客户端传达。Obsolete属性是一个预定义属性,执行相同的操作并警告调用用户特定部分已经过时

示例

Obsolete. You can compile and run the code even after a warning message because we have not asked this attribute to throw any error message on usage:
[Obsolete("Do not use this class use 'Person' instead.")] 
public class Author:Person 
{ 
    //other stuff goes here 
} 

以下图像显示了一个警告消息,表示不要使用Author类,因为它是Obsolete。但是客户端仍然可以编译和运行代码(我们没有要求此属性在使用时抛出错误):

以下将在使用时显示错误消息以及警告消息:

[Obsolete("Do not use this class use 'Person' instead.",true)] 
public class Author:Person 
{ 
    //other stuff goes here 
} 

考虑以下图像,用户在使用属性后出现异常,该属性被写入以在使用时抛出错误:

条件

条件属性是一个预定义属性,根据应用于正在处理的代码的条件限制执行。

示例

考虑以下代码片段,它限制了在定义的调试预处理器下方法的条件执行(我们将在接下来的部分详细讨论预处理器):

#define Debug 
using System.Diagnostics; 
using static System.Console; 

namespace Day06 
{ 
    internal class Program 
    { 
        private static void Main(string[] args) 
        { 
            PersonList(); 
            ReadLine(); 
        } 

        [Conditional("Debug")] 
        private static void PersonList() 
        { 
            WriteLine("Person list:"); 
            foreach (var person in Person.GetPersonList()) 
            { 
                WriteLine($"Name:{person.FirstName} {person.LastName}"); 
                WriteLine($"Age:{person.Age}"); 
            } 
        } 
    } 
} 

在定义预处理器符号时,请记住一件事;您要在文件的第一行上定义它。

创建和实现自定义属性

在上一节中,我们讨论了可用的或预定义的属性,并注意到这些属性非常有限,在实际应用中,我们的需求将需要更复杂的属性。在这种情况下,我们可以创建自己的自定义属性;这些属性类似于预定义属性,但具有我们自定义的操作代码和目标类型。所有自定义属性都应继承自System.Attribute类。

在本节中,我们将根据以下要求创建一个简单的自定义属性:

  • 创建一个ErrorLogger属性

  • 此属性将处理所有可用的环境,即调试、开发、生产等

  • 此方法应仅限于方法

  • 它应该显示自定义或提供的异常/异常消息

  • 默认情况下,它应将环境视为DEBUG

  • 如果为开发和DEBUG环境装饰,则应显示并抛出异常

先决条件

要创建和运行自定义属性,我们应该具备以下先决条件:

  1. Visual Studio 2017 或更高版本

  2. .NET Core 1.1 或更高版本

以下是创建我们期望的属性的代码片段:

public class ErrorLogger : Attribute 
{ 
    public ErrorLogger(string exception) 
    { 
        switch (Env) 
        { 
            case Env.Debug: 
            case Env.Dev: 
                WriteLine($"{exception}"); 
                throw new Exception(exception); 
            case Env.Prod: 
                WriteLine($"{exception}"); 
                break; 
            default: 
                WriteLine($"{exception}"); 
                throw new Exception(exception); 
        } 
    } 

    public Env Env { get; set; } 
} 

在上述代码中,我们只是向控制台写入客户端代码提供的任何异常。在DEBUGDev环境的情况下,进一步抛出异常。

以下代码片段显示了此属性的简单用法:

public class MathClass 
{ 
    [ErrorLogger("Add Math opetaion in development", Env =
    Env.Debug)] 
    public string Add(int num1, int num2) 
    { 
        return $"Sum of {num1} and {num2} = {num1 + num2}"; 
    } 

    [ErrorLogger("Substract Math opetaion in development", Env =
    Env.Dev)] 
    public string Substract(int num1, int num2) 
    { 
        return $"Substracrion of {num1} and {num2} = {num1 -
        num2}"; 
    } 

    [ErrorLogger("Multiply Math opetaion in development", Env =
    Env.Prod)] 
    public string Multiply(int num1, int num2) 
    { 
        return $"Multiplication of {num1} and {num2} = {num1 -
        num2}"; 
    } 
} 

在上述代码中,我们有不同的方法,标记为不同的环境。我们的属性将触发并编写为各个方法提供的异常。

利用预处理器指令

从名称上可以清楚地看出,预处理器指令是在实际编译开始之前进行的处理过程。换句话说,这些预处理器向编译器发出指令,对信息进行预处理,这是在编译器编译代码之前进行的。

重要点

在您使用预处理器时,请注意以下几点:

  • 预处理器指令实际上是编译器的条件

  • 预处理器指令必须以#符号开头

  • 预处理器指令不应以分号(;)结尾,就像语句结束一样

  • 预处理器不用于创建宏

  • 预处理器应逐行声明

预处理器指令的作用

考虑以下预处理器指令:

#if ... #endif  

这个指令是一个条件指令,当这个指令应用到代码时,代码会执行,你也可以使用#elseif和/或#else指令。由于这是一个条件指令,C#中的#if条件是布尔值,这些运算符可以用来检查相等(==)和不相等(!=),以及多个符号之间的关系,以及(&&),或(||),和非(!)运算符也可以用来评估条件。

你应该在文件的第一行上定义一个符号,使用#define指令。

考虑以下代码片段,它让我们了解条件编译:

#define DEBUG 
#define DEV 
using static System.Console; 

namespace Day06 
{ 
    public class PreprocessorDirective 
    { 
        public void ConditionalProcessor() =>
        #if (DEBUG && !DEV) 
            WriteLine("Symbol is DEBUG."); 
            #elseif (!DEBUG && DEV) 
            WriteLine("Symbol is DEV"); 
            #else 
            WriteLine("Symbols are DEBUG & DEV"); 
            #endif 
    } 
} 
DEBUG and DEV, and now, on the basis of our condition the following will be the output of the preceding code.

#define 和#undef

#define指令基本上为我们定义了一个将在条件预处理器指令中使用的符号。

#define不能用于声明常量值。

在使用#define声明符号时应该记住以下几点:

  • 它不能用于声明常量

  • 它可以定义一个符号,但不能为这些符号赋值

  • 对符号的任何指令都应该在文件中定义符号之后,这意味着#define指令总是在使用之前出现

  • 使用#define指令定义或创建的符号的作用域在它被声明/定义的文件中

回想一下我们在#if指令中讨论的代码示例,我们在那里定义了两个符号。所以,定义一个符号很容易,比如:#define DEBUG

#undef指令让我们取消之前定义的符号。这个预处理器应该出现在任何非指令语句之前。考虑以下代码:

#define DEBUG 
#define DEV 
#undef DEBUG 
using static System.Console; 

namespace Day06 
{ 
    public class PreprocessorDirective 
    { 
        public void ConditionalProcessor() => 
#if (DEBUG && !DEV) 
            WriteLine("Symbol is DEBUG."); 
#elif (!DEBUG && DEV) 
            WriteLine("Symbol is DEV"); 
#else 
            WriteLine("Symbols are DEBUG & DEV"); 
#endif 
    } 
} 

在上面的代码中,我们取消了DEBUG符号,代码将产生以下输出:

#region 和#endregion 指令

在处理长代码文件时,这些指令非常有用。有时候,当我们在处理一个长代码库时,比如一个企业应用,这种应用会有 1000 行代码,并且这些行会是不同函数/方法或业务逻辑的一部分。因此,为了更好地可读性,我们可以在区域内管理这些部分。在一个区域中,我们可以为区域包含的代码命名并给出简短的描述。让我们看一下以下图像:

在上面的图像中,左侧部分显示了#region...#endregion指令的扩展视图,告诉我们如何将这些指令应用到我们的长代码文件中。图像的右侧显示了折叠视图,当你将鼠标悬停在折叠区域文本上时,你会看到在 Visual Studio 中出现了一个矩形块,它显示了这些区域包含的内容。因此,你无需展开区域来检查这个区域下写了什么代码。

#line 指令

#line指令提供了一种修改编译器实际行号的方式。你还可以为错误和警告提供输出FileName,这是可选的。这个指令在构建过程中的自动化中可能会有用。在原始源代码中删除了行号的情况下,你需要基于原始文件生成输出。

另外,#line默认指令将行号返回到默认值,并且它会计算之前重新编号的行。

#line隐藏指令不会影响错误报告中的文件名或行号。

#line文件名指令定义了一个在编译器输出中想要出现的文件名的方式。在这里,默认值是实际使用的文件名;你可以在双引号中提供一个新的名字,并且这个名字必须在行号之前。

考虑以下代码片段:

        public void LinePreprocessor() 
        { 
            #line 85 "LineprocessorIsTheFileName" 
            WriteLine("This statement is at line#85 and not at
            line# 25");
            #line default 
            WriteLine("This statement is at line#29 and not at
            line# 28");
            #line hidden 
            WriteLine("This statement is at line#30"); 
        } 
    } 
85 for the first statement, which was originally at line number 25.

#warning 指令

#warning指令提供了一种在代码的任何部分生成警告的方式,并通常在条件指令内工作。考虑以下代码片段:

        public void WarningPreProcessor() 
        { 
           #if DEBUG 
           #warning "This is a DEBUG compilation." 
           WriteLine("Environment is DEBUG."); 
           #endif 
        } 
    } 

上述代码将在编译时发出警告,并且警告消息将是您使用#warning指令提供的内容:

#error

#error指令提供了一种在代码的任何部分生成错误的方式。考虑以下代码片段:

        public void ErrorPreProcessor() 
        { 
           #if DEV 
           #error "This is a DEV compilation." 
           WriteLine("Environment is DEV."); 
           #endif 
        } 

这将引发错误,由于这个错误,您的代码将无法正确构建;它将以您使用#error指令提供的错误消息失败构建。让我们看一下以下图片:

在本节中,我们讨论了预处理指令及其在代码示例中的使用。

有关 C#预处理指令的完整参考,请参考官方文档:

docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/

开始使用 LINQ

LINQ 只是语言集成查询的缩写,是编程语言的一部分。LINQ 提供了一种使用指定语法编写或查询数据的简单方法,就像我们在尝试为某些特定条件查询数据时使用 where 子句一样。因此,我们可以说 LINQ 是一种用于查询数据的语法。

在本节中,我们将看到一个简单的示例来查询数据。我们有Person列表,以下代码片段为我们提供了各种查询数据的方式:

private static void TestLINQ() 
{ 
    var person = from p in Person.GetPersonList() 
        where p.Id == 1 
        select p; 
    foreach (var per in person) 
    { 
        WriteLine($"Person Id:{per.Id}"); 
        WriteLine($"Name:{per.FirstName} {per.LastName}"); 
        WriteLine($"Age:{per.Age}"); 
    } 

} 
List of persons for *personId* =1\. The LINQ query returns a result of IEnumerable<Person> type which can be easily accessed using foreach. This code produces the following output:

LINQ 的完整讨论超出了本书的范围。有关完整的 LINQ 功能,请参考:code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b

编写不安全代码

在本节中,我们将讨论如何使用 Visual Studio 编写不安全代码的介绍。语言 C#提供了一种编写代码的方式,该代码编译并创建对象,这些对象在根下由垃圾收集器管理有关垃圾收集器的更多详细信息,请参考[第 01 天]。简而言之,C#不像使用函数指针访问引用的 C、C++语言。但是在某些情况下,有必要在 C#语言中使用函数指针,类似于支持函数指针的语言如 C 或 C++,但 C#语言不支持它。为了克服这种情况,我们在 C#语言中有不安全代码。有一个修饰符不安全,告诉编译器这段代码不受垃圾收集器控制,在该块内我们可以使用函数指针和其他不安全的东西。要使用不安全代码,我们首先要求编译器从 Visual Studio 2017 或更高版本开始设置不安全编译,只需转到项目属性,在“生成”选项卡上,选择“允许不安全代码”选项,参考以下截图:

如果未选择此选项,您将无法继续使用不安全代码,请参考以下截图:

设置不安全编译后,让我们编写代码使用指针交换两个数字,考虑以下代码片段:

public unsafe void SwapNumbers(int*  num1, int* num2) 
{ 
    int tempNum = *num1; 
    *num1 = *num2; 
    *num2 = tempNum; 
} 

上面是一个非常简单的交换函数,它只是使用指针交换两个数字。让我们调用这个函数来看看实际结果:

private static unsafe void TestUnsafeSwap() 
{ 
    Write("Enter first number:"); 
    var num1 = Convert.ToInt32(ReadLine()); 
    Write("Enter second number:"); 
    var num2 = Convert.ToInt32(ReadLine()); 
    WriteLine("Before calling swap function:"); 
    WriteLine($"Number1:{num1}, Number2:{num2}"); 
    //call swap 
    new UnsafeSwap().SwapNumbers(&num1, &num2); 
    WriteLine("After calling swap function:"); 
    WriteLine($"Number1:{num1}, Number2:{num2}"); 
} 

在上面的代码片段中,我们输入了两个数字,然后显示交换前后的结果,这产生了以下输出:

在本节中,我们讨论了如何处理不安全代码。

有关不安全代码的更多详细信息,请参考语言规范的官方文档:docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/unsafe-code

编写异步代码

在我们讨论异步方式的代码之前,让我们先讨论一下我们的普通代码,即同步代码,让我们考虑以下代码片段:

public class FilePolling 
{ 
    public void PoleAFile(string fileName) 
    { 
        Console.Write($"This is polling file:
        {fileName}"); 
        //file polling stuff goes here 
    } 
} 

前面的代码片段简短而简洁。它告诉我们它正在轮询一个特定的文件。在这里,系统必须等待完成轮询文件的操作,然后才能开始下一个操作。这就是同步代码。现在,考虑一种情况,我们不需要等待完成这个函数的操作就开始另一个操作或函数。为了满足这样的情况,我们有异步编码,这是可能的关键字是 async。

考虑以下代码:

public async void PoleAFileAsync(string fileName) 
{ 
    Console.Write($"This is polling file: {fileName}"); 
    //file polling async stuff goes here 
} 

仅仅通过async关键字,我们的代码就能够进行异步调用。

从先前的代码来看,我们可以说异步编程是一种不让客户端代码等待执行另一个函数或操作的任何异步操作的编程。简单地说,我们可以说异步代码不能阻止需要调用的另一个操作。

在本章中,我们讨论了异步编码。关于这个主题的完整讨论超出了我们书的范围。有关完整详情,请参阅官方文档:docs.microsoft.com/en-us/dotnet/csharp/async

动手练习

  1. 通过创建StringCalculator的泛型代码来定义泛型类:github.com/garora/TDD-Katas/tree/develop/Src/cs/StringCalculator

  2. 创建一个泛型和非泛型集合,并测试哪一个在性能上更好。

  3. 我们在“为什么应该使用泛型?”一节中讨论了代码片段,其中讲述了运行时编译异常。在这方面,为什么我们不应该以以下方式使用相同的代码?

internal class Program 
{ 
      private static void Main(string[] args) 
{ 
    //No exception at compile-time or run-time 
    ArrayList authorEditorArrayList = new ArrayList {
    "Gaurav Arora", 43, "Vikas Tiwari", 25 }; 
    foreach (var authorEditor in authorEditorArrayList) 
    { 
        WriteLine($"{authorEditor}"); 
    } 
}     
} 
  1. 在泛型代码中,default关键字的用途是什么,通过一个现实世界的例子加以阐述。

  2. 使用所有 3 种预定义属性编写简单代码。

  3. 属性的默认限制类型是什么?编写一个程序来展示所有限制类型。

  4. 创建一个名为LogFailuresAttribute的自定义属性,用于记录所有异常到文本文件中。

  5. 为什么预处理器指令#define不能用于声明常量值?

  6. 编写一个程序来创建一个作者列表,并在其上应用 LINQ 功能。

  7. 编写一个程序来对数组进行排序

  8. 编写一个完整的程序来编写同步和异步方法来写一个文件。

重温第 6 天

今天,我们讨论了泛型、属性、预处理器、LINQ、不安全代码和异步编程等高级概念。

我们的一天从泛型开始,您通过代码片段了解了泛型类。然后,我们深入了解了属性,并学习了如何使用预定义属性装饰我们的 C#代码。我们创建了一个自定义属性,并在我们的代码示例中使用了它。我们讨论了预处理器指令,并学习了这些指令在我们编码中的用法。其他讨论的概念包括 LINQ、不安全代码和异步编程。

明天,也就是第七天将是我们七天学习系列的结束日。我们将介绍 OOP 概念及其在 C#语言中的实现。

第七章:第 7 天 - 用 C#理解面向对象编程

今天是我们七天学习系列的第七天。昨天(第六天),我们学习了一些高级主题,讨论了属性、泛型和 LINQ。今天,我们将开始学习使用 C#的面向对象编程(OOP)。

这将是一个实际的面向对象编程方法,同时涵盖所有方面。即使没有任何面向对象编程的基础知识,您也将受益,并且可以自信地在工作场所轻松地进行实践。

我们将涵盖以下主题:

  • 面向对象编程介绍

  • 讨论对象关系

  • 封装

  • 抽象

  • 继承

  • 多态

面向对象编程介绍

OOP 是纯粹基于对象的编程范式之一。这些对象包含数据(请参考第七天了解更多细节)。

当我们对编程语言进行分类时,称之为编程范式。有关更多信息,请参阅en.wikipedia.org/wiki/Programming_paradigm

OOP 已经被考虑来克服早期编程方法的局限性(考虑过程语言方法)。

一般来说,我将 OOP 定义如下:

一种现代编程语言,我们使用对象作为构建应用程序的基本组件。

我们周围有很多对象的例子,在现实世界中,我们有各种方面是对象的代表。让我们回到我们的编程世界,思考一个定义如下的程序:

程序是一系列指令,指示语言编译器要做什么。

为了更深入地理解 OOP,我们应该了解早期的编程方法,主要是过程式编程、结构化编程等。

  • 结构化编程:这是由艾兹格·W·迪科斯特拉在 1966 年创造的一个术语。结构化编程是一种编程范式,用于解决处理 1000 行代码并将其分成小部分的问题。这些小部分通常被称为子例程、块结构、forwhile循环等。使用结构化编程技术的已知语言包括 ALGOL、Pascal、PL/I 等。

  • 过程式编程:这是从结构化编程派生出来的一种范式,简单地基于我们如何进行调用(称为过程调用)。使用过程式编程技术的已知语言包括 COBOL、Pascal、C。Go 编程语言的一个最新例子于 2009 年发布。

这两种方法的主要问题是,一旦程序增长,程序就变得难以管理。具有更复杂和庞大代码库的程序使这两种方法变得紧张。简而言之,使用这两种方法会使代码的可维护性变得繁琐。为了克服这些问题,现在我们有了 OOP,它具有以下特点:

  • 继承

  • 封装

  • 多态

  • 抽象

讨论对象关系

在我们开始讨论 OOP 之前,首先我们应该了解关系。在现实世界中,对象之间有关系,也有层次结构。面向对象编程中有以下类型的关系:

  • 关联:关联表示对象之间的关系,所有对象都有自己的生命周期。在关联中,这些对象没有所有者。例如,会议中的人。在这里,人和会议是独立的;它们没有父级。一个人可以参加多个会议,一个会议可以组合多个人。会议和人都是独立初始化和销毁的。

聚合和组合都是关联的类型。

  • 聚合:聚合是一种特殊形式的关联。与关联类似,对象在聚合中有自己的生命周期,但它涉及所有权,这意味着子对象不能属于另一个父对象。聚合是一种单向关系,对象的生命周期彼此独立。例如,子对象和父对象的关系是一种聚合,因为每个子对象都有父对象,但并不是每个父对象都有子对象。

  • 组合:组合是一种死亡关系,表示两个对象之间的关系,一个对象(子对象)依赖于另一个对象(父对象)。如果父对象被删除,所有的子对象都会自动被删除。例如,一个房子和一个房间。一个房子有多个房间。但一个房间不能属于多个房子。如果我们拆毁了房子,房间会自动删除。

在接下来的部分,我们将详细讨论面向对象编程的所有特性。此外,我们将了解如何使用 C#实现这些特性。

继承

继承是面向对象编程中最重要的特性/概念之一。它在名称上是不言自明的;继承从一个类继承特性。简而言之,继承是一个在编译时执行的活动,通过语法的帮助进行指示。继承另一个类的类称为子类或派生类,被继承的类称为基类或父类。在这里,派生类继承基类的所有特性,无论是实现还是重写。

在接下来的部分,我们将使用 C#的代码示例详细讨论继承。

理解继承

继承作为面向对象编程的一个特性,帮助您定义一个子类。这个子类继承了父类或基类的行为。

继承一个类意味着重用这个类。在 C#中,继承是用冒号(:)符号来象征性地定义的。

修饰符(参考第二章,第 02 天-开始使用 C#)告诉我们基类对派生类的重用范围。例如,考虑类B继承类A。在这里,类B包含类*A 的所有特性,包括它自己的特性。请参考以下图表:

在上图中,派生类(即B)通过忽略修饰符继承了所有特性。特性的继承无论是公共的还是私有的。这些修饰符在这些特性要被实现时才会考虑。在实现时只有公共特性才会被考虑。所以,在这里,公共特性,即ABC将被实现,但私有特性,即B将不会被实现。

继承的类型

直到这一点,我们已经了解了继承的概念。现在,是时候讨论继承的类型了;继承有以下几种类型:

  • 单一继承:

这是一种广泛使用的继承类型。单一继承是指一个类继承另一个类。继承另一个类的类称为子类,被继承的类称为父类或基类。在子类中,类只从一个父类继承特性。

C#只支持单一继承。

在下一节中,您可以按层次继承类(正如我们将在下一节中看到的),但这是派生类的自然单一继承。请参考以下图表:

前面的图表是单一继承的表示,显示Class B(继承类)继承Class A(基类)。Class B可以重用所有特性,即ABC,包括它自己的特性,即 D。继承中成员的可见性或可重用性取决于保护级别(这将在接下来的部分“继承中的成员可见性”中讨论)。

  • 多重继承:

多重继承发生在派生类继承多个基类时。诸如 C++之类的语言支持多重继承。C#不支持多重继承,但我们可以借助接口实现多重继承。如果您想知道为什么 C#不支持多重继承,请参考官方链接blogs.msdn.microsoft.com/csharpfaq/2004/03/07/why-doesnt-c-support-multiple-inheritance/。参考以下图表:

前面的图表是多重继承的表示(在没有接口帮助的情况下在 C#中不可能),显示了Class C(派生类)从两个基类(AB)继承。在多重继承中,派生的Class C将拥有Class AClass B的所有特性。

  • 层次继承:

当超过一个类从一个类继承时,层次继承发生。参考以下图表:

在前面的图表中,Class B(派生类)和Class C(派生类)从Class A(基类)继承。借助层次继承,Class B可以使用Class A的所有特性。同样,Class C也可以使用Class A的所有特性。

  • 多级继承:

当一个类从已经是派生类的类派生时,称为多级继承。

在多级继承中,最近派生的类拥有所有先前派生类的特性。

在这种情况下,派生类可以有其父类和父类的父类。参考以下图表:

前面的图表表示多级继承,并显示Class C(最近派生的类)可以重用Class BClass A的所有特性。

  • 混合继承:

混合继承是多重继承的组合。

C#不支持混合继承。

多重和多级继承的组合是层次继承,其中一个父类是一个派生类,最近派生的类继承多个父类。还可以有更多的组合。参考以下图表:

前面的图像代表混合继承,显示了层次和多重继承的组合。您可以看到Class A是一个父类,所有其他类都直接或间接地从Class A派生而来。我们的派生Class E可以重用Class ABCD类的所有特性。

  • 隐式继承:

.NET 中的所有类型都隐式继承自system.object或其派生类。有关隐式继承的更多信息,请参考docs.microsoft.com/en-us/dotnet/csharp/tutorials/inheritance#implicit-inheritance

继承中的成员可见性

正如我们之前讨论的,在继承中,派生类可以重用父类的功能并使用或修改其父类的成员。但是这些成员可以根据其访问修饰符或可见性进行重用或修改(有关更多详细信息,请参阅第四章,第 04 天 - 讨论 C#类成员)。

在本节中,我们将简要讨论继承中的成员可见性。在 C#语言中可能的任何类型的继承中,以下成员不能被基类继承:

  • 静态构造函数:静态构造函数是初始化静态数据的构造函数(参考第四章,第 4 天:讨论 C#类成员中的修饰符部分)。静态构造函数的重要性在于,在创建类的第一个实例或在某些操作中调用或引用其他静态成员之前调用这些构造函数。作为静态数据初始化程序,静态构造函数不能被派生类继承。

  • 实例构造函数:这不是静态构造函数;每当创建类的新实例时,都会调用构造函数,即实例类。一个类可以有多个构造函数。由于实例构造函数用于创建类的实例,因此不能被派生类继承。有关构造函数的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/constructors

  • Finalizers:这些只是类的析构函数。这些在运行时由垃圾收集器调用或使用来销毁类的实例。由于析构函数只调用一次且每个类只有一个,因此不能被派生类继承。有关析构函数的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors

派生类可以重用或继承基类的所有成员,但其使用或可见性取决于其访问修饰符(参考第四章,第 4 天 - 讨论 C#类成员)。这些成员的不同可见性取决于以下可访问性修饰符:

  • 私有:如果成员是private,则private成员的可见性仅限于其派生类;如果派生类嵌套在其基类中,则派生类中将可用private成员。

考虑以下代码片段:

public class BaseClass
{
   private const string AuthorName = "Gaurav Aroraa";
   public class DeriveClass: BaseClass
   {
      public void Display()
      {
         Write("This is from inherited Private member:");
         WriteLine($"{nameof(AuthorName)}'{AuthorName}'");
         ReadLine();
      }
   }
}
BaseClass is to have one private member, AuthorName, and this will be available in DeriveClass, as DeriveClass is a nested class of BaseClass. You can also see this in compile time while moving the cursor over to the usage of the private AuthorName member. See the following screenshot:

前面的图像显示了派生类中私有方法的可见性。如果类嵌套在其基类中,则派生类中的私有方法是可见的。

如果类没有嵌套在其父类/基类中,则可以看到以下编译时异常:

在前面的屏幕截图中,我们有ChildClass,它继承自BaseClass。在这里,我们不能使用BaseClass的私有成员,因为ChildClass没有嵌套在BaseClass中。

  • 受保护:如果成员是受保护修饰符,则仅对派生类可见。在使用基类的实例时,这些成员将不可用或不可见,因为它们被定义为受保护。

以下屏幕截图显示了如何使用基类访问/可见受保护的成员:

在前面的屏幕截图中,受保护的成员EditorNameChildClass中可见,因为它继承了BaseClass

以下屏幕截图显示了使用BaseClass实例中不可访问受保护成员的情况。如果尝试这样做,将会收到编译时错误:

  • 内部:具有内部修饰符的成员仅在与基类相同程序集的派生类中可用。这些成员对于属于其他程序集的派生类将不可用或不可见。

考虑以下代码片段:

namespace Day07
{
   public class MemberVisibility
   {
      private void InternalMemberExample()
      {
         var childClass = new Lib.ChildClass();
         WriteLine("Calling from derived class that
         belongs to same assembly of BaseClass");
         childClass.Display();
      }
   }
}

前面的代码显示了内部成员的可见性。在这里,ChildClass属于Lib程序集,而BaseClass就在其中。

另一方面,如果BaseClass存在于Lib之外的程序集中,则内部成员将无法访问;请参阅以下屏幕截图:

上面的截图显示了一个编译时错误,告诉我们内部成员是不可访问的,因为它们在同一个程序集中不可用。

  • Public:公共成员在派生类中可用或可见,并且可以进一步使用。

考虑以下代码片段:

public class ChilClassYounger : ChildClass
{
   private string _copyEditor = "Diwakar Shukla";
   public new void Display()
   {
      WriteLine($"This is from ChildClassYounger: copy
      editor is '{_copyEditor}'");
      WriteLine("This is from ChildClass:");
      base.Display();
   }
}
ChildClassYoung has a Display() method that displays the console output. ChildClass also has a public Display() method that also displays the console output. In our derived class, we can reuse the Display() method of ChildClass because it is declared as public. After running the previous code, it will give the following output:

在前面的代码中,您应该注意到我们在ChildClassYounger类的Display()方法中添加了一个new关键字。这是因为我们在父类(即ChildClass)中有一个同名的方法。如果我们不添加new关键字,我们将看到一个编译时警告,如下面的截图所示:

通过应用new关键字,您隐藏了从ChildClass继承的ChildClass.Display()成员。在 C#中,这个概念被称为方法隐藏。

实现继承

在前一节中,您详细了解了继承,并了解了继承成员的可见性。在本节中,我们将实现继承。

继承是IS-A关系的表示,这表明AuthorIS-APersonPersonIS-AHuman,所以AuthorIS-AHuman。让我们在一个代码示例中理解这一点:

public class Person
{
   public string FirstName { get; set; } = "Gaurav";
   public string LastName { get; set; } = "Aroraa";
   public int Age { get; set; } = 43;
   public string Name => $"{FirstName} {LastName}";
   public virtual void Detail()
   {
      WriteLine("Person's Detail:");
      WriteLine($"Name: {Name}");
      WriteLine($"Age: {Age}");
      ReadLine();
   }
}
public class Author:Person
{
   public override void Detail()
   {
      WriteLine("Author's detail:");
      WriteLine($"Name: {Name}");
      WriteLine($"Age: {Age}");
      ReadLine();
   }
}
public class Editor : Person
{
   public override void Detail()
   {
    WriteLine("Editor's detail:");
    WriteLine($"Name: {Name}");
    WriteLine($"Age: {Age}");
    ReadLine();
   }
}
public class Reviewer : Person
{
   public override void Detail()
   {
      WriteLine("Reviewer's detail:");
      WriteLine($"Name: {Name}");
      WriteLine($"Age: {Age}");
      ReadLine();
   }
}

在上面的代码中,我们有一个基类Person和三个派生类,分别是AuthorEditorReviewer。这显示了单一继承。以下是先前代码的实现:

private static void InheritanceImplementationExample()
{
   WriteLine("Inheritance implementation");
   WriteLine();
   var person = new Person();
   WriteLine("Parent class Person:");
   person.Detail();
   var author = new Author();
   WriteLine("Derive class Author:");
   Write("First Name:");
   author.FirstName = ReadLine();
   Write("Last Name:");
   author.LastName = ReadLine();
   Write("Age:");
   author.Age = Convert.ToInt32(ReadLine());
   author.Detail();
   //code removed
}

在上面的代码中,我们实例化了一个单一类并调用了 details;每个类都继承了Person类,因此,它的所有成员。这产生了以下输出:

在 C#中实现多重继承

我们已经在前一节中讨论过 C#不支持多重继承。但是我们可以借助接口实现多重继承(请参阅第二章,第 02 天-开始使用 C#)。在本节中,我们将使用 C#实现多重继承。

让我们考虑前一节的代码片段,该片段实现了单一继承。让我们通过实现接口来重写代码。

接口代表具有/能够做的关系,这表明Publisher具有AuthorAuthor具有Book。在 C#中,您可以将类的实例分配给任何类型为接口或基类的变量。从面向对象的角度来看,这个概念被称为多态性(有关更多细节,请参阅多态性部分)。

首先,让我们创建一个接口:

public interface IBook
{
   string Title { get; set; }
   string Isbn { get; set; }
   bool Ispublished { get; set; }
   void Detail();
}
IBook interface, which is related to book details. This interface is intended to collect book details, such as Title, ISBN, and whether the book is published. It has a method that provides the complete book details.

现在,让我们实现IBook接口来派生Author类,该类继承了Person类:

public class Author:Person, IBook
{
   public string Title { get; set; }
   public string Isbn { get; set; }
   public bool Ispublished { get; set; }
   public override void Detail()
   {
      WriteLine("Author's detail:");
      WriteLine($"Name: {Name}");
      WriteLine($"Age: {Age}");
      ReadLine();
   }
   void IBook.Detail()
   {
      WriteLine("Book details:");
      WriteLine($"Author Name: {Name}");
      WriteLine($"Author Age: {Age}");
      WriteLine($"Title: {Title}");
      WriteLine($"Isbn: {Isbn}");
      WriteLine($"Published: {(Ispublished ? "Yes" :
      "No")}");
      ReadLine(); 
   } 
} 
IBook interface. Our derived class Author inherits the Person base class and implements the IBook interface. In the preceding code, a notable point is that both the class and interface have the Detail() method. Now, it depends on which method we want to modify or which method we want to reuse. If we try to modify the Detail() method of the Person class, then we need to override or hide it (using the new keyword). On the other hand, if we want to use the interface's method, we need to explicitly call the IBook.Detail() method. When you call interface methods explicitly, modifiers are not required; hence, there is no need to put a public modifier here. This method implicitly has public visibility:
//multiple Inheritance
WriteLine("Book details:");
Write("Title:");
author.Title = ReadLine();
Write("Isbn:");
author.Isbn = ReadLine();
Write("Published (Y/N):");
author.Ispublished = ReadLine() == "Y";((IBook)author).Detail(); //
we need to cast as both Person class and IBook has same named methods
Author class with IBook:

上面的图像显示了使用接口实现的代码的输出。接口的所有成员对子类都是可访问的;在实例化子类时,不需要特殊实现。子类的实例能够访问所有可见成员。在上述实现中的重要一点是在((IBook)author).Detail();语句中,我们显式地将子类的实例转换为接口,以获得接口成员的实现。默认情况下,它提供类成员的实现,因此我们需要明确告诉编译器我们需要一个接口方法。

抽象

抽象是通过隐藏不相关或不必要的信息来显示相关数据的过程。例如,如果您购买了一部手机,您可能对您的消息是如何传递的或者您的呼叫是如何连接到另一个号码不感兴趣,但您可能会对知道每当您在手机上按下呼叫按钮时,它应该连接您的呼叫感兴趣。在这个例子中,我们隐藏了那些用户不感兴趣的功能,并提供了用户感兴趣的功能。这个过程叫做抽象。

实现抽象

在 C#中,抽象类可以通过以下方式实现:

抽象类

抽象类是半定义的,这意味着它提供了一种方法来覆盖其子类的成员。我们应该在需要将相同成员提供给所有子类并具有自己实现或想要覆盖的项目中使用基类。例如,如果我们有一个抽象类 Car,其中有一个抽象方法 color,并且有子类 HondCar、FordCar、MarutiCar 等。在这种情况下,所有子类都将具有 color 成员,但具有不同的实现,因为 color 方法将在子类中被覆盖并具有自己的实现。这里需要注意的一点是 - 抽象类代表 IS-A 关系。

您还可以在 Day04 部分“抽象”中重新讨论我们对抽象类的讨论,并查看代码示例以了解实现。

抽象类的特点

在前一节中,我们了解了抽象类的一些特点:

  • 抽象类不能被初始化,这意味着您不能创建抽象类的对象。

  • 抽象类旨在充当基类,因此其他类可以继承它。

  • 如果声明了抽象类,则按设计必须由其他类继承。

  • 抽象类可以同时拥有具体方法和抽象方法。抽象方法应该在继承抽象类的子类中实现。

接口

接口不包含功能或具体成员。您可以称之为类或结构将实现以定义功能签名的合同。通过使用接口,您可以确保每当类或结构实现它时,该类或结构将使用接口的合同。例如,如果 ICalculator 接口具有 Add()方法,这意味着每当类或结构实现此接口时,它都提供了一个特定的合同功能,即加法。

有关接口的更多信息,请参阅:docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/index

接口只能拥有以下成员:

  • 方法

  • 属性

  • 索引器

  • 事件

接口的特点

以下是接口的主要特点

  • 接口默认为 internal

  • 接口的所有成员默认为 public,并且不需要显式应用 public 修饰符到成员上。

  • 类似地,接口也不能被实例化。它们只能实现,并且实现它的类或结构应该实现所有成员。

  • 接口不能包含任何具体方法

  • 接口可以被另一个接口、类或结构实现。

  • 类或结构可以实现多个接口。

类可以继承抽象类或普通类并实现接口。

在本节中,我们将使用抽象类来实现抽象。让我们考虑以下代码片段:

public class AbstractionImplementation
{
public void Display()
{
BookAuthor author = new BookAuthor();
author.GetDetail();
BookEditor editor = new BookEditor();
editor.GetDetail();
BookReviewer reviewer = new BookReviewer();
reviewer.GetDetail();
}
}

上面的代码片段只包含一个负责显示操作的公共方法。Display()方法是获取书籍作者、编辑和评论员详细信息的方法。乍一看,我们可以说上面的代码是不同实现的不同类。但实际上,我们正在使用抽象类来抽象我们的代码,然后派生类提供所需的详细信息。

考虑以下代码:

public abstract class Team
{
public abstract void GetDetail();
}

我们有一个抽象类 Team,其中有一个抽象方法 GetDetail(),这个方法负责获取团队的详细信息。现在,想一下这个团队包括什么,这个团队由作者、编辑和评论员组成。因此,我们有以下代码片段:

public class BookAuthor : Team
{
public override void GetDetail() => Display();
private void Display()
{
WriteLine("Author detail");
Write("Enter Author Name:");
var name = ReadLine();
WriteLine($"Book author is: {name}");
}
}

BookAuthor 类继承 Team 并重写 GetDetail()方法。该方法进一步调用一个私有方法 Display(),这是用户不会意识到的。用户只会调用 GetDetail()方法。

同样,我们还有 BookEditor 和 BookReviewer 类:

public class BookEditor : Team
{
public override void GetDetail() => Display();
private void Display()
{
WriteLine("Editor detail");
Write("Enter Editor Name:");
var name = ReadLine();
WriteLine($"Book editor is: {name}");
}
}
public class BookReviewer : Team
{
public override void GetDetail() => Display();
private void Display()
{
WriteLine("Reviewer detail");
Write("Enter Reviewer Name:");
var name = ReadLine();
WriteLine($"Book reviewer is: {name}");
}
}

在上述代码中,类将只公开一个方法,即GetDetail(),以提供所需的详细信息。

当客户端调用此代码时,将会得到以下输出:

封装

封装是一种数据不直接对用户可访问的过程。当你想要限制或隐藏客户端或用户对数据的直接访问时,这种活动或过程就被称为封装。

当我们说信息隐藏时,意味着隐藏用户不需要或对信息不感兴趣的信息,例如-当你买一辆自行车时,你可能不会对引擎如何工作,内部如何供应燃料等感兴趣,但你可能对自行车的里程等感兴趣。

信息隐藏不是数据隐藏,而是在 C#中的实现隐藏,欲了解更多信息,请参考:blog.ploeh.dk/2012/11/27/Encapsulationofproperties/

在 C#中,当函数和数据组合在一个单元中(称为类),并且你不能直接访问数据时,称为封装。在 C#类中,应用访问修饰符到成员、属性,以避免其他情况或用户直接访问数据。

在本节中,我们将详细讨论封装。

C#中的访问修饰符是什么?

如前一节所讨论的,封装是隐藏信息不让外部世界知道的概念。在 C#中,我们有访问修饰符或访问限定符,可以帮助我们隐藏信息。这些访问修饰符帮助你定义类成员的范围和可见性。

以下是访问修饰符:

  • 公共的

  • 私有的

  • 受保护的

  • 内部的

  • 受保护的内部

我们在第四天已经讨论了所有上述的访问修饰符。请参考访问修饰符部分以及它们的可访问性,以了解这些修饰符如何工作并帮助我们定义可见性。

实现封装

在本节中,我们将在 C# 7.0 中实现封装。想象一个场景,我们需要提供一个Author的信息,包括最近出版的书。考虑以下代码片段:

internal class Writer
{
   private string _title;
   private string _isbn;
   private string _name;
   public void SetName(string fname, string lName)
   {
      if (string.IsNullOrEmpty(fname) ||
      string.IsNullOrWhiteSpace(lName))
      throw new ArgumentException("Name can not be
      blank.");
      _name = $"{fname} {lName}";
   }
   public void SetTitle(string title)
   {
      if (string.IsNullOrWhiteSpace(title))
      throw new ArgumentException("Book title can not be
      blank.");
      _title = title;
   }
   public void SetIsbn(string isbn)
   {
      if (!string.IsNullOrEmpty(isbn))
      {
         if (isbn.Length == 10 | isbn.Length == 13)
         {
            if (!ulong.TryParse(isbn, out _))
            throw new ArgumentException("The ISBN can
            consist of numeric characters only.");
         }
         else
      throw new ArgumentException("ISBN should be 10 or 13
      characters numeric string only.");
      }
    _isbn = isbn;
   }
   public override string ToString() => $"Author '{_name}'
   has authored a book '{_title}' with ISBN '{_isbn}'";
  }

在上述显示封装实现的代码片段中,我们隐藏了用户不想知道的字段。因为主要目的是展示最近的出版物。

以下是客户端需要的信息的代码:

public class EncapsulationImplementation
{
   public void Display()
   {
      WriteLine("Encapsulation example");
      Writer writer = new Writer();
      Write("Enter First Name:");
      var fName = ReadLine();
      Write("Enter Last Name:");
      var lName = ReadLine();
      writer.SetName(fName,lName);
      Write("Book title:");
      writer.SetTitle(ReadLine());
      Write("Enter ISBN:");
      writer.SetIsbn(ReadLine());
      WriteLine("Complete details of book:");
      WriteLine(writer.ToString());
   }
}

上述代码片段只是为了获取所需的信息。用户不会知道信息是如何从类中获取/检索的。

上述图像显示了在执行前面的代码后,您将看到的确切输出。

多态性

简单来说,多态意味着有多种形式。在 C#中,我们可以将一个接口表达为多个函数,这就是多态。多态来自希腊词,意思是“多种形式”。

在 C#中,所有类型(包括用户定义的类型)都继承自对象,因此 C#中的每种类型都是多态的。

正如我们讨论的,多态意味着多种形式。这些形式可以是函数的形式,在派生类中以不同形式实现相同名称和相同参数的函数。此外,多态性具有提供相同名称的方法的不同实现的能力。

在接下来的部分中,我们将讨论各种类型的多态性,包括它们在 C# 7.0 中的实现。

多态性的类型

在 C#中,我们有两种多态性,这些类型是:

  • 编译时多态

编译时多态也被称为早期绑定或重载或静态绑定。它在编译时确定,并用于具有不同参数的相同函数名称。编译时或早期绑定进一步分为两种类型,这些类型是:

    • 函数重载

函数重载,如其名所示,函数被重载。当您声明具有相同名称但不同参数的函数时,它被称为函数重载。您可以声明尽可能多的重载函数。

考虑以下代码片段:

public class Math
{
    public int Add(int num1, int num2) =>   num1 + num2;
    public double Add(double num1, double num2) => num1 + num2;
}

上述代码是重载的一种表示,Math类有一个带有双重载参数的Add()方法。这些方法用于分离行为。考虑以下代码:

public class CompileTimePolymorphismImplementation
{
   public void Run()
   {
      Write("Enter first number:");
      var num1 = ReadLine();
      Write("Enter second number:");
      var num2 = ReadLine();
      Math math = new Math();
      var sum1 = math.Add(FloatToInt(num1),
      FloatToInt(num1));
      var sum2 = math.Add(ToFloat(num1), ToFloat(num2));
      WriteLine("Using Addd(int num1, int num2)");
      WriteLine($"{FloatToInt(num1)} + {FloatToInt(num2)}
      = {sum1}");
      WriteLine("Using Add(double num1, double num2)");
      WriteLine($"{ToFloat(num1)} + {ToFloat(num2)} =
      {sum2}");
   }
   private int FloatToInt(string num) =>
   (int)System.Math.Round(ToFloat(num), 0);
   private float ToFloat(string num) = 
   float.Parse(num);
}

上述代码片段同时使用了这两种方法。以下是上述实现的输出:

如果您分析之前的结果,您会发现接受双参数的重载方法提供了准确的结果,即 99,因为我们提供了小数值并且它添加了小数。另一方面,带有整数类型参数的Add方法,将双精度数四舍五入并将其转换为整数,因此显示了错误的结果。然而,之前的例子与正确的计算无关,但它告诉了关于使用函数重载进行编译时多态性的情况。

    • 运算符重载

运算符重载是重新定义特定运算符的实际功能的一种方式。

在处理用户定义的复杂类型时,这一点非常重要,直接使用内置运算符是不可能的。

我们已经在第二章中详细讨论了运算符重载,第 02 天 - 开始使用 C#部分 - 运算符重载 - 如果您想复习运算符重载,请参考本节。

  • 运行时多态性

运行时多态性也被称为晚期绑定、覆盖或动态绑定。我们可以通过在 C#中覆盖方法来实现运行时多态性。虚拟或抽象方法可以在派生类中被覆盖。

在 C#中,抽象类提供了一种在派生类中覆盖抽象方法的方法。virtual关键字也是在派生类中覆盖方法的一种方式。我们在第二章中讨论了virtual关键字(如果您想复习,请参考)。

考虑以下示例:

internal abstract class Team
{
   public abstract string Detail();
}
internal class Author : Team
{
   private readonly string _name;
   public Author(string name) => _name = name;
   public override string Detail()
   {
      WriteLine("Author Team:");
      return $"Member name: {_name}";
   }
}
Team is having an abstract method Detail() that is overridden.
public class RunTimePolymorphismImplementation
{
   public void Run()
   {
      Write("Enter name:");
      var name = ReadLine();
      Author author = new Author(name);
      WriteLine(author.Detail());
   }
}
Author class and produces the following output:

上图显示了一个实现抽象类的程序示例的输出。

我们还可以使用抽象类和虚拟方法实现运行时多态性,考虑以下代码片段:

internal class Team
{
   protected string Name;
   public Team(string name)
   {
      Name = name;
   }
   public virtual string Detail() => Name;
}
internal class Author : Team
{
   public Author(string name) : base(name)
   {}
   public override string Detail() => Name;
}
internal class Editor : Team
{
   public Editor(string name) : base(name)
   {}
   public override string Detail() => Name;
}
internal class Client
{
   public void ShowDetail(Team team) =>
   WriteLine($"Member: {team.Detail()}");
}
Team and perform the operation by knowing the type of a class at runtime.

我们的ShowDetail()方法显示了特定类型的成员名称。

实现多态性

让我们在一个完整的程序中实现多态性,考虑以下代码片段:

public class PolymorphismImplementation
{
   public void Build()
   {
      List<Team> teams = new List<Team> {new Author(), new
      Editor(), new Reviewer()};
      foreach (Team team in teams)
      team.BuildTeam();
   }
}
public class Team
{
   public string Name { get; private set; }
   public string Title { get; private set; }
   public virtual void BuildTeam()
   {
      Write("Name:");
      Name = ReadLine();
      Write("Title:");
      Title = ReadLine();
      WriteLine();
      WriteLine($"Name:{Name}\nTitle:{Title}");
      WriteLine();
   }
}
internal class Author : Team
{
   public override void BuildTeam()
   {
      WriteLine("Building Author Team");
      base.BuildTeam();
   }
}
internal class Editor : Team
{
   public override void BuildTeam()
   {
      WriteLine("Building Editor Team");
      base.BuildTeam();
   }
}
internal class Reviewer : Team
{
   public override void BuildTeam()
   {
      WriteLine("Building Reviewer Team");
      base.BuildTeam();
   }
}

上述代码片段是多态性的一种表示,即构建不同的团队。它产生以下输出:

上图显示了一个程序的结果,该程序表示了多态性的实现。

动手练习

以下是今天学习中未解决的问题:

  1. 什么是面向对象编程?

  2. 为什么我们应该使用面向对象的语言而不是过程式语言?

  3. 定义继承?

  4. 通常有多少种继承类型?

  5. 为什么我们不能在 C#中实现多重继承?

  6. 我们如何在 C#中实现多重继承。

  7. 用一个简短的程序定义继承成员的可见性。

  8. 定义隐藏并用一个简短的程序详细说明。

  9. 什么是覆盖?

  10. 何时使用隐藏,何时使用覆盖,用一个简短的程序详细说明(提示:参考 - docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords

  11. 什么是隐式继承?

  12. 抽象类和接口之间有什么区别?

  13. 什么是封装,用一个简短的程序来详细说明。

  14. 定义有助于封装的访问修饰符或访问说明符。

  15. 什么是抽象?用一个现实世界的例子详细说明。

  16. 用一个现实世界的例子说明封装和抽象的区别。(提示:stackoverflow.com/questions/16014290/simple-way-to-understand-encapsulation-and-abstraction)

  17. 何时使用抽象类和接口,用简短的程序详细说明。(提示:dzone.com/articles/when-to-use-abstract-class-and-intreface)

  18. 抽象函数和虚函数有什么区别?(提示:stackoverflow.com/questions/391483/what-is-the-difference-between-an-abstract-function-and-a-virtual-function)

  19. 在 C#中定义多态?

  20. 有多少种多态性,使用 C# 7.0 编写一个简短的程序来实现?

  21. 用实际例子定义晚期绑定和早期绑定。

  22. 用程序证明 - 在 C#中,每种类型都是多态的。

  23. 重载和重写有什么区别?

重温第 7 天

最后,我们到达了我们 7 天学习系列的最后一天,也就是第七天。今天,我们已经学习了面向对象编程范式的概念,从对象关系开始,概述了关联、聚合和组合,然后讨论了结构化和过程化语言。我们讨论了面向对象编程的封装、抽象、继承和多态这四个特性。我们还使用 C# 7.0 实现了面向对象编程的概念。

明天,第八天,我们将开始一个真实世界的应用程序,这将帮助我们复习到目前为止学到的所有概念。如果你现在想复习,请继续查看之前几天的学习。

接下来做什么?

今天我们结束了我们的 7 天学习系列。在这段旅程中,我们从非常基础的开始,然后逐渐适应了高级术语,但这只是一个开始,还有更多要学习。我尽量将几乎所有的东西都结合在这里,为下一步,我建议你应该学习这些:

  1. 多线程

  2. 构造函数链

  3. 索引器

  4. 扩展方法

  5. 高级正则表达式

  6. 高级不安全代码实现

  7. 垃圾回收的高级概念

有关更高级的主题,请参考以下内容:

  1. C# 7.0 和 .NET Core Cookbook (www.packtpub.com/application-development/c-7-and-net-core-cookbook)

  2. questpond.over-blog.com/

  3. Functional C# (www.packtpub.com/application-development/functional-c)

  4. 使用 C# 7.0 进行多线程的 Cookbook - 第二版 (www.packtpub.com/application-development/multithreading-c-cookbook-second-edition)

第八章:第 08 天-测试你的技能-构建一个真实世界的应用程序

在第七天,我们学习了 C# 7.0 中的面向对象编程概念。通过对面向对象编程概念的理解,我们这个学习系列的旅程需要一个实际的、实践的、真实世界的应用程序,这就是我们在这里的原因。今天是我们七天学习系列的复习日。在过去的七天里,我们学到了很多东西,包括以下内容:

  • .NET Framework 和.NET Core

  • 基本的 C#概念,包括语句,循环,类,结构等

  • 包括委托,泛型,集合,文件处理,属性等在内的高级 C#概念

  • C# 7.0 和 C# 7.1 的新功能

在过去的七天里,我们通过代码片段详细讨论了上述主题,并详细讨论了代码。我们从第一天开始涵盖了非常基本的概念,第二天和第三天涵盖了中级内容,然后逐渐通过代码解释讨论了高级主题。

今天,我们将重新审视一切,并在 C# 7.0 中构建一个真实世界的应用程序。以下是我们将遵循的步骤来完成应用程序:

  1. 讨论我们应用程序的要求。

  2. 为什么我们要开发这个应用程序?

  3. 开始应用程序开发:

  • 先决条件

  • 数据库设计

  • 讨论基本架构

为什么我们要开发这个应用程序?

我们的应用程序将基于印度的 GST 税收系统(www.gstn.org/)。在印度,这个系统最近宣布,行业内急需尽快采用。现在是创建一个实际应用程序,让我们获得实际经验的正确时机。

讨论我们应用程序的要求:

在本节中,我们将讨论我们的应用程序并制定它。首先,让我们为我们的应用程序决定一个名称;让我们称之为FlixOneInvoicing—一个生成发票的系统。如前所述,今天的行业需要一个系统,可以满足其需求,以满足我们通过我们的基于 GST 的应用程序示例所展示的 GST 的所有部分。以下是系统的主要要求:

  • 系统应该是公司特定的,并且公司应该是可配置的

  • 公司可以有多个地址(注册和送货地址可能不同)

  • 公司可以是个人或注册实体

  • 系统应该具有客户功能

  • 系统应该支持服务和商品行业

  • 系统应该遵循印度的 GST 规定

  • 系统应该具有报告功能

  • 系统应该有基本操作,如添加,更新,删除等

上述高级要求让我们了解了我们将要开发的系统的类型。在接下来的章节中,我们将根据这些要求开发一个应用程序。

开始应用程序开发

在之前的章节中,我们讨论了为什么我们要开发这个应用程序以及为什么它是必需的,根据行业需求。我们还讨论了基本的系统要求,并在理论上制定了系统,以便在我们开始实际编码时,我们可以遵循所有这些规则/要求。在本节中,我们将开始实际开发。

先决条件

要开始开发这个应用程序,我们需要以下先决条件:

  • Visual Studio 2017 更新 3 或更高版本

  • SQL Server 2008 R2 或更高版本

  • C# 7.0 或更高版本

  • ASP.NET Core

  • Entity Framework Core

数据库设计

要执行数据库设计,您应该对 SQL Server 和数据库的核心概念有基本的了解。如果您想学习数据库概念,以下资源可能有所帮助:

根据我们在上一节讨论的基本业务需求,让我们设计一个完整的数据库,以便保存重要的应用程序数据。

概述

我们需要以单一系统,多个公司的方式开发我们的数据库。单一系统,多个公司功能将使我们的系统能够在公司结构内运行,其中公司有多个分支机构,一个总部和单独的用户来维护其他分支机构的系统。

在本节中,我们将讨论以下数据库图:

根据我们的要求,我们的系统适用于多个公司,这意味着每个公司都将有自己的配置、用户、客户和发票。例如,如果两个不同的公司(abcxyz)使用相同的系统,那么abc的用户只能访问abc的信息。

当前系统不遵循 B2B 或 B2C 类别。

让我们分析以前的数据库图以了解关系层次结构的运作方式。公司表由用户表引用,以便用户仅适用于特定公司。地址表突出显示公司客户表之外,并且被两个表引用。使地址表引用公司客户表使我们能够为它们每个人拥有多个地址。

国家和州的主数据分别存储在国家表中。州只能属于特定国家,因此相应地参考国家表。

我们以这种方式安排我们的表以实现规范化。请参考searchsqlserver.techtarget.com/definition/normalization以了解数据库中规范化概念。

讨论模式和表:

在上一节中,我们概述了我们的数据库设计。让我们讨论系统中重要的表及其用途:

  • 用户:此表包含跨公司的用户相关的所有数据。这些用户可以在系统上操作。此表保存用户信息;companyid是与公司表的外键,并且它在用户公司表之间提供关系,指示系统特定用户适用于特定公司:

  • 公司:此表包含与公司相关的所有信息,并存储名称GSTN字段。如果公司未注册 GSTN,则GSTN字段为空。与地址表存在外键关系,因为一个公司可能有多个地址。因此,公司地址表展示一对多的关系:

  • 客户:此表包含与客户相关的所有信息,包括名称GSTNGSTN字段为空,因为个人不会注册GSTN。此表还与地址表有关:

  • 地址:此表包含与公司或客户地址相关的所有信息,可能有多个:

  • 发票和发票明细:这些表是事务性表。发票表包含创建发票所需的所有细节,而发票明细表包含特定发票的项目/交易的完整细节:

  • 国家和州: 这些表存储主记录数据。这些数据不会改变,也不会受到系统事务的影响。目前,这两个表包含了特定于印度的主数据:

根据我们最初的要求,前述表格是可以的;我们可以根据需要添加/更新表格。该系统是用于更新的。

您可以参考Database_FlixOneInvoice.sql,其中包含了完整的数据库架构和主数据,可在 GitHub 存储库[]的 Day-08 中找到。

在下一节中,我们将讨论系统架构和我们将要编写的实际代码。

讨论基本架构

在本节中,我们将讨论我们应用程序的基本架构;我们不会讨论设计模式和其他与架构相关的内容,这超出了本书的范围。

要了解设计模式,请参考www.questpond.com/demo.html#designpattern

如先决条件中所述,我们的应用程序将基于 ASP.NET Core,它消耗 RESTful API。这只是一个基本版本,所以我们不会展示太多设计模式的实现。以下图片给出了我们的 Visual Studio 解决方案的概要。我们有一个表示和领域,您可以将这些层拆分为更多层来定义业务工作流程。

我使用 C# 7.0 编写了实际的代码;该应用程序涵盖了我们在第 7 天讨论的内容。

完整的应用程序已随本章一起在 GitHub 上发布:<>

在本节中,我们将涵盖我们在第 7 天学到的所有主要代码片段。下载完整的应用程序,在 Visual Studio 中打开解决方案,然后查看代码。将代码与您在这七天旅程中学到的所有内容联系起来。例如,看看我们在哪里使用了继承、封装等。尝试将我们在本书中讨论的概念可视化。您将能够连接我们为应用程序编写的每一条代码声明。

重温第 08 天

这是我们书的复习日。当然,这是书的最后一章,但这只是您开始探索更多与 C#相关内容的开始。在这一天,我们开发了一个基于印度 GST 系统的应用程序。借助这个应用程序,我们重新访问了您在这七天学习系列中学到的所有内容,包括属性、反射、C# 7.0 特性等。

posted @   绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
历史上的今天:
2020-05-17 HowToDoInJava Spring 教程·翻译完成
点击右上角即可分享
微信分享提示