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

99 阅读27分钟

C# 七天学习手册(四)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:第 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 特性等。