C# 基础、核心概念和模式交互式指南(一)

原文:Interactive C# Fundamentals, Core Concepts and Patterns

协议:CC BY-NC-SA 4.0

一、面向对象的编程概念

欢迎学习面向对象编程(OOP)。你可能已经熟悉“需要是发明之母”这句谚语同样的概念也适用于此。如果我们对为什么引入这种类型的编程有一个基本的想法,或者这些概念将如何使真实世界的编程变得容易,我们的学习路径将是令人愉快的,我们将能够在各个方向上扩展我们的学习。因此,我将尝试解决一些常见的问题,然后提供一个面向对象编程的概述。

我只有两条警告信息给你。

  • 如果你在第一遍后没有理解所有的东西,不要灰心丧气。有时这看起来很复杂,但渐渐地对你来说会变得容易。
  • 有很多对 OOP 的批评。不要忘记,每个人都有批判新事物的倾向。所以,即使你想批判这些概念,我建议你先试着去理解和运用这些概念,然后自己决定是欣赏还是批判。

现在让我们开始旅程吧…

我们开始用二进制代码进行计算机编程,需要机械开关来加载这些程序。你可以猜到在那个年代,程序员的工作是非常具有挑战性的。后来,一些高级编程语言被开发出来,使他们的生活变得更容易。他们开始编写简单的类似英语的说明来服务我们的目的。反过来,编译器用来将这些指令翻译成二进制,因为计算机只能理解二进制语言的指令。所以,我们变得乐于开发那些高级语言。

但是经过一段时间,计算机的容量和功能增加了很多。一个明显的结果是,我们需要扩展我们的视野,我们开始尝试在计算机编程中实现更复杂的概念。不幸的是,当时可用的编程语言都不够成熟,无法实现这些概念。这些是我们主要关心的问题:

  • 我们如何重用现有的代码来避免重复工作?
  • 如何在共享环境中控制全局变量的使用?
  • 当应用中出现过多跳转时(使用类似goto的关键字),我们如何调试代码?
  • 假设一个新的程序员加入了一个团队。他发现很难理解这个程序的整体结构。我们怎样才能让他/她的生活更轻松?
  • 如何才能有效地维护一个庞大的代码库?

为了克服这些问题,专业程序员想出了将大问题分解成小块的主意。这背后的想法非常简单:如果我们能解决每个小问题/小块,最终我们会解决大问题。因此,他们开始将大问题分解成小部分,函数(或过程或子例程)的概念出现了。这些功能中的每一个都致力于解决一个小问题领域。在高层次上,管理功能及其交互成为关注的关键领域。在这种背景下,结构化编程的概念应运而生。结构化编程开始流行起来,因为小函数易于管理和调试。除此之外,我们开始限制全局变量的使用,在函数中用局部变量代替(在大多数情况下)。

结构化编程流行了近二十年。在此期间,硬件容量显著增加,一个明显的影响是,人们希望完成更复杂的任务。渐渐地,结构化编程的缺点和局限性引起了我们的注意;例如

  • 假设,我们在应用的多个函数中使用了特定的数据类型。现在,如果我们需要更改数据类型,我们必须跨产品的所有功能实现更改。
  • 很难用结构化编程的关键组件(即数据+函数)对所有真实场景进行建模。在现实世界中,无论何时我们创造一个产品,我们都需要关注两个方面。
    • 目的。我们为什么需要这种产品?
    • 行为。该产品如何让我们的生活变得更轻松?

然后对象的概念就产生了。

Points to Remember

结构化编程和面向对象编程之间的根本区别可以概括为:我们关注的是数据本身,而不是对数据的操作。

面向对象编程的核心原则很少。你可以很容易地猜到,我们将在本书的其余部分详细讨论它们。首先,我将逐一介绍他们。

类和对象

这些都是 OOP 的核心。类是其对象的蓝图或模板。对象是类的实例。用简单的语言来说,我们可以说,在结构化编程中,我们将问题隔离或划分为函数,而在 OOP 中,我们将问题划分为对象。在计算机编程中,我们已经熟悉了 int、double、float 等数据类型。这些被称为内置数据类型或原始数据类型,因为它们已经在相应的计算机语言中进行了定义。但是当我们需要创建自己的数据类型(例如,学生)时,我们需要创建一个学生类。正如当我们需要创建一个整数变量时,我们需要首先引用 int,同样,当我们需要创建一个学生对象(例如,John)时,我们需要首先引用我们的学生类。同样,我们可以说罗纳尔多是足球运动员类的对象,哈里是雇员类的对象,你最喜欢的汽车是车辆类的对象,等等。

包装

封装的目的至少是以下之一:

  • 设置限制,使对象的组件不能被直接访问
  • 将数据与作用于该数据的方法绑定在一起(即形成一个胶囊)

在一些 OOP 语言中,信息的隐藏在默认情况下是不实现的。因此,他们提出了一个额外的术语,叫做信息隐藏。

稍后我们会看到数据封装是一个类的关键特性之一。在理想情况下,这些数据对外界是不可见的。只有通过类内部定义的方法,我们才能访问这些数据。因此,我们可以将这些方法视为对象数据和外部世界(即我们的程序)之间的接口。

在 C# 中,我们可以通过正确使用访问说明符(或修饰符)和属性来实现封装。

抽象

抽象的主要目的是只显示必要的细节,隐藏实现的背景细节。抽象与封装也有很大的关系,但是通过一个简单的日常场景就可以很容易理解这种区别。

当我们按下遥控器上的按钮打开电视时,我们并不关心电视的内部电路,也不关心遥控器如何启动电视。我们简单的知道遥控器上不同的按钮有不同的功能,只要正常工作,我们就很开心。因此,用户与封装在遥控器(或电视)中的复杂实现细节隔离开来。同时,可以通过遥控器执行的常见操作可以被认为是遥控器中的抽象。

遗产

每当我们谈论可重用性时,我们通常指的是继承,继承是一个类对象获得另一个类对象的属性的过程。考虑这个例子。公共汽车是一种交通工具,因为它符合交通工具的基本标准。同样,火车是另一种交通工具。同样,尽管货物列车和旅客列车是不同的,但我们可以说它们都继承了列车类别,因为最终它们都满足了列车的基本标准,而列车又是车辆。因此,我们可以简单地说,继承的概念支持层次分类。

在编程世界中,继承从现有类(在 C# 中称为基类或父类)创建一个新的子类,它位于层次链中的上一级。然后我们可以添加新的功能(方法)或者修改基类功能(覆盖)到子类中。我们必须记住,由于这些修改,核心架构不应受到影响。换句话说,如果您从 Vehicle 类派生 Bus 类,并在 Bus 类中添加/修改功能,这些修改不应该影响为 Vehicle 类描述的原始功能。

因此,关键的优势是我们可以通过这种机制避免大量的重复代码。

多态

多态通常与一个具有多种形式的名称相关联。考虑你的宠物狗的行为。当它看到一个不认识的人时,它很生气,开始不停地叫。但是当它看到你的时候,它会发出不同的声音,表现出不同的行为。在编码界,你可以想到一个非常流行的方法,加法。对于两个整数的加法,我们期望得到两个整数的和。但是对于两个字符串,我们期望得到一个连接的字符串。

多态有两种类型。

  • 编译时多态:一旦程序被编译,编译器可以很早就决定在什么情况下调用哪个方法。也称为静态绑定或早期绑定。
  • 运行时多态:实际的方法调用在运行时被解析。在编译时,我们无法预测程序运行时将调用哪个方法(例如,程序可能会因不同的输入而表现不同)。举一个非常简单的用例:假设,当我们执行一个程序时,我们想在第一行生成一个随机数。而如果生成的数字是偶数,我们会调用一个方法 Method1(),打印“Hello”;否则,我们将调用一个名称相同但输出“Hi”的方法。现在,你会同意,如果我们执行程序,那么只有我们能看到哪个方法被调用(即,编译器不能在编译时解析调用)。在程序执行之前,我们不知道会看到“Hello”还是“Hi”。这就是为什么有时它也被称为动态绑定或延迟绑定。

摘要

本章讨论了以下主题。

  • 面向对象编程导论
  • 为什么会进化?
  • 和结构化编程有什么不同?
  • 面向对象编程的核心特征是什么?

二、积木:类和对象

班级

一个类就是一个蓝图或者一个模板。它可以描述其对象的行为。它是如何构建或实例化对象的基础。

目标

对象是一个类的实例。

面向对象编程(OOP)技术主要依赖于这两个概念——类和对象。通过一个类,我们创建了一个新的数据类型,对象被用来保存数据(字段)和方法。对象行为可以通过这些方法公开。

如果你熟悉足球(或英式足球,在美国众所周知),我们知道参加比赛的球员是根据他们在不同位置的技能挑选出来的。除了这些技能,他们还需要具备最低水平的比赛体能和一般运动能力。所以,如果我们说 c 罗是足球运动员(又名英式足球运动员),我们可以预测 c 罗拥有这些基本能力以及一些足球特有的技能(尽管 c 罗对我们来说是未知的)。所以,我们可以简单地说,罗纳尔多是一个足球运动员阶层的对象。

Note

尽管如此,你可能会觉得这是一个先有鸡还是先有蛋的困境。你可以争辩说,如果我们说,“X 先生踢得像 c 罗”,那么在这种情况下,c 罗的表现就像一个阶级。然而,在面向对象的设计中,我们通过决定谁先来使事情变得简单,我们将那个人标记为应用中的类。

现在考虑另一个足球运动员,贝克汉姆。我们可以再次预测,如果贝克汉姆是足球运动员,那么贝克汉姆一定在足球的很多方面都很优秀。此外,他必须具备参加比赛的最低健康水平。

现在假设罗纳尔多和贝克汉姆都参加了同一场比赛。不难预测,虽然 c 罗和贝克汉姆都是足球运动员,但在那场比赛中,他们的踢球风格和表现会有所不同。同样,在面向对象编程的世界中,对象的性能可以彼此不同,即使它们属于同一个类。

我们可以考虑任何不同的领域。现在你可以预测你的宠物狗或宠物猫可以被认为是动物类的对象。你最喜欢的车可以被认为是一个车辆类的对象。你喜欢的小说可以考虑作为一个书类的对象,等等。

简而言之,在现实世界的场景中,每个对象都必须有两个基本特征:状态和行为。如果我们考虑足球运动员类的对象—罗纳尔多或贝克汉姆,我们会注意到他们有“比赛状态”或“非比赛状态”这样的状态在玩耍状态下,他们可以表现出不同的行为——他们可以跑,他们可以踢,等等。

在非玩耍状态下,它们的行为也会发生变化。在这种状态下,他们可以小睡一会儿,或者吃饭,或者只是通过看书、看电影等活动放松一下。

同样,我们可以说,在任何特定的时刻,我们家里的电视可以处于“开”或“关”的状态。当且仅当它处于开启模式时,它可以显示不同的频道。如果处于关闭模式,它将不会显示任何内容。

因此,从面向对象编程开始,我们总是建议您问自己这样的问题:

  • 我的对象可能有哪些状态?
  • 在这些状态下,它们可以执行哪些不同的功能(行为)?

一旦你得到了这些问题的答案,你就可以开始下一步了。这是因为任何面向对象程序中的软件对象都遵循相同的模式:它们的状态存储在字段/变量中,它们的能力/行为通过不同的方法/函数来描述。

现在让我们开始编程吧。要创建对象,我们需要首先决定它们将属于哪个类;也就是说,一般来说,如果我们要创建对象,首先需要创建一个类。

Note

有一些例外情况(如系统中的ExpandoObject类。动态名称空间)。它可以表示一个对象,其成员可以在运行时添加或删除。但是你才刚刚开始类和对象的旅程。让我们把事情变得非常简单。此刻我们可以忽略那些边角案例。

Points to Remember

一般来说,如果我们想使用对象,我们需要先有一个类。

假设我们已经创建了一个类,并将这个类的名称指定为 A。现在我们可以使用下面的语句创建一个类 A 的对象obA:

A obA=new A();

前一行可以分解为以下两行:

A obA;//Line-1

obA=new A();//Line-2

在第 1 行的末尾,obA是一个引用。在此之前,没有分配任何内存。但是一旦新的出现,内存就被分配了。

如果您仔细观察,您会发现在第二行中,类名后面跟了一个括号。我们用它来构造物体。这些是用于运行初始化代码的构造函数。构造函数可以有不同的实参(也就是说,它们可以随不同数量的形参或不同类型的形参而变化)。

在下面的例子中,类 A 有四个不同的构造函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是如果我们没有为我们的类提供任何构造函数,C# 将提供一个默认的。

Points to Remember

如果我们没有为我们的类提供任何构造函数,C# 将为你提供一个默认的无参数公共构造函数。但是如果你提供了任何构造函数,那么编译器不会为你生成默认的构造函数。

因此,当我们看到如下内容时,我们确信使用了无参数的构造函数。

A obA=new A();

但是要知道它是用户自定义的构造函数还是 C# 提供的(换句话说,默认的构造函数),我们需要考察类体;例如,如果在一个类定义中,我们编写了如下代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以得出结论,这里我们使用了用户定义的无参数构造函数。因此,在这种情况下,C# 编译器不会为我们生成任何默认的构造函数。

课堂演示

如果您已经达到了这一点,这意味着您可以猜到类只是我们程序的积木。我们将变量(称为字段)和方法封装在一个类中,形成一个单元。这些变量被称为实例变量(静态变量将在本书的后面部分讨论),因为该类的每个对象(即该类的每个实例)都包含这些变量的副本。(稍后,您将了解到字段可以是任何隐式数据类型、不同的类对象、枚举、结构、委托等。).另一方面,方法包含一组代码。这些只是一系列执行特定操作的语句。实例变量通常通过方法来访问。这些变量和方法统称为类成员。

Points to Remember

  • 根据 C# 语言规范,除了字段和方法之外,一个类还可以包含许多其他东西——常量、事件、运算符、构造函数、析构函数、索引器、属性和嵌套类型。但是为了简单起见,我们从最常见的方法和字段开始。我将在本书后面的章节中讨论其他主题。
  • 字段和方法可以与不同种类的修饰符相关联。
    • 字段修饰符可以是它们中的任何一种—静态、公共、私有、受保护、内部、新、不安全、只读和易变。
    • 方法修饰符可以是以下任意一种:静态、公共、私有、受保护、内部、新、虚拟抽象重写或异步。

其中大部分将在接下来的章节中介绍。

考虑一个简单的例子。现在我们已经创建了一个名为 ClassEx1 的类,并且只封装了一个整型字段 MyInt。我们还在该字段中初始化了值 25。因此,我们可以预测,每当我们创建这个类的对象时,该对象中都会有一个名为 myInt 的整数,对应的值将是 25。

为了便于参考,我们已经创建了两个对象— obAobB from our classes 1 class。我们已经测试了对象中变量 MyInt 的值。你可以看到在这两种情况下,我们得到的值都是 25。

演示 1

using System;

namespace ClassEx1
{
    class ClassEx1
    {
        //Field initialization is optional.
        public int MyInt = 25;
        //public int MyInt;
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** A class demo with 2 objects ***");
            ClassEx1 obA = new ClassEx1();
            ClassEx1 obB = new ClassEx1();
            Console.WriteLine("obA.i ={0}", obA.MyInt);
            Console.WriteLine("obB.i ={0}", obB.MyInt);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

附加注释

  • 没有必要以这种方式初始化 MyInt。我们才刚刚开始。我们从一个非常简单的例子开始。换句话说,字段初始化是可选的。

  • 如果您没有为您的字段提供任何初始化,它将采用一些默认值。我们将很快介绍这些默认值。

  • Suppose that in the preceding example, you did not initialize the field. Then your class will look like this:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    Still, you can instantiate your object and then supply your intended value like this:

    ClassEx1 obA = new ClassEx1();
    obA.MyInt = 25;//setting 25 into MyInt of obA
    
    

如果你熟悉 Java,要在控制台打印,你可能会喜欢这种格式。C# 也允许这样做。

Console.WriteLine("obA.i =" + obA.MyInt);
Console.WriteLine("obB.i =" + obB.MyInt);

学生问:

先生,请告诉我们更多关于构造函数的信息。

老师说:我们必须记住这些要点:

  • 构造函数用于初始化对象。
  • 类名和相应的构造函数名必须相同。
  • 它们没有任何返回类型。
  • 我们可以说有两种类型的构造函数:无参数构造函数(有时称为无参数构造函数或默认构造函数)和有参数构造函数(称为参数化构造函数)。按照 C# 的说法,我们是在创建自己的无参数构造函数,还是由 C# 编译器创建,这都无关紧要。在这两种情况下,我们通常称之为默认构造函数。或者我们也可以根据构造函数是静态构造函数还是非静态构造函数(或者实例构造函数)来区分构造函数。你将熟悉本章中的实例构造函数。实例构造函数用于初始化类的实例(对象),而静态构造函数用于在类第一次出现时初始化类本身。我在另一章讨论了“静态”。
  • 一般来说,常见的任务,如类中所有变量的初始化,都是通过构造函数来实现的。

学生问:

先生,构造函数没有任何返回类型。这个语句的意思是他们的返回类型是 void 吗?

老师说:不。隐式地,构造函数的返回类型和它的类类型是一样的。我们不应该忘记,即使是 void 也被认为是一个返回类型。

学生问:

先生,我们对使用用户定义的无参数构造函数和 C# 提供的默认构造函数有点困惑。两者看起来是一样的。两者有什么关键区别吗?

老师说:我已经提到过,用 C# 的说法,我们是否创建了自己的无参数构造函数或者它是否是由 C# 编译器创建的并不重要。在这两种情况下,我们通常称之为默认构造函数。有时两者可能看起来一样。但是请记住,使用用户定义的构造函数,我们可以有一定的灵活性。我们可以把自己的逻辑和一些额外的控制对象的创建。

考虑下面的例子并分析输出。

演示 2

using System;

namespace DefaultConstructorCaseStudy
{

    class DefConsDemo
    {
        public int myInt;
        public float myFloat;
        public double myDouble;
        public DefConsDemo()
        {
            Console.WriteLine("I am initializing with my own choice");
            myInt = 10;
            myFloat = 0.123456F;
            myDouble = 9.8765432;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Comparison between user-defined and  C# provided default constructors***\n");
            DefConsDemo ObDef = new DefConsDemo();
            Console.WriteLine("myInt={0}", ObDef.myInt);
            Console.WriteLine("myFloat={0}", ObDef.myFloat.ToString("0.0####"));
            Console.WriteLine("myDouble={0}", ObDef.myDouble);
            Console.Read();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

您可以看到,在我们为变量设置值之前,我们已经打印了一行附加的内容,“我正在用我自己的选择进行初始化。”

但是,如果您只是不提供这个无参数的构造函数,而想使用 C# 提供的默认构造函数,您将得到下一节中显示的输出。

附加注释

要查看下面的输出,您需要注释掉或移除前面示例中的构造函数定义。现在您可以看到,这些值中的每一个都用该类型的相应默认值进行了初始化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你必须记住另一个关键点。我们可以为用户定义的构造函数使用我们自己的访问修饰符。因此,如果您提供自己的无参数构造函数,您可以使它不是公共的。

让我们看看 C# 语言规范告诉了我们什么。

如果一个类不包含实例构造函数声明,则自动提供一个默认的实例构造函数。默认构造函数只是调用直接基类的无参数构造函数。如果类是抽象的,那么默认构造函数的声明的可访问性是受保护的;否则,默认构造函数声明的可访问性是公共的。因此,默认构造函数始终采用以下形式:

protected C(): base() {}

或者

public C(): base() {}

C是类的名称。如果重载决策无法确定基类构造函数初始值设定项的唯一最佳候选项,则会发生编译时错误。

很快,您将熟悉这些新术语:访问修饰符、重载和基。所以,不要惊慌。你可以学习这些概念,然后再来回答这一部分。

所以,简单来说,下面的声明

class A
    {
        int myInt;
    }

相当于这样:

class A
    {
        int myInt;
        public A():base()
        { }
    }

学生问:

先生,我们看到 C# 提供的默认构造函数正在用一些默认值初始化实例变量。其他类型的默认值是什么?

老师说:你可以参考下表供你参考。

| 类型 | 默认值 | | :-- | :-- | | sbyte,byte,short,ushort,int,uint,long,ulong | Zero | | 茶 | \x0000 ' | | 漂浮物 | 0.0f | | 两倍 | 0.0d | | 小数 | 0.0 米 | | 弯曲件 | 错误的 | | 结构体 | 将所有值类型设置为默认值,将所有引用类型设置为 null* | | 枚举 E | 0(转换为类型 E) |

*We will discuss value types and reference types in detail later in the book.

学生问:

先生,我们似乎可以调用一些方法来初始化这些变量。那我们为什么选择构造函数呢?

老师说:如果你从那个角度考虑,那么你必须同意,要做那个工作,你需要显式地调用方法;也就是说,用简单语言来说,呼叫不是自动的。但是对于构造函数,我们在每次创建对象时都执行自动初始化。

学生问:

先生,字段初始化和通过构造函数初始化哪个先发生?

老师说:字段初始化首先发生。这个初始化过程遵循声明顺序。

恶作剧

你能预测产量吗?

using System;
namespace ConsEx2
{
    class ConsEx2
    {
        int i;
        public ConsEx2(int i)
        {
            this.i = i;
        }
    }
   class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Experiment with constructor***");
            ConsEx2 ob2 = new ConsEx2();
        }
    }
}

输出

编译错误:“ConsEx2”不包含采用 0 个参数的构造函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

参见下面的问答。我们还将很快讨论关键字“this”。

学生问:

先生,在这种情况下,我们应该有一个来自 C# 的默认构造函数。那为什么编译器会抱怨一个 0 参数的构造函数呢?

老师说:我已经提到过,在 C# 中,当且仅当我们不提供任何构造函数时,我们可以得到一个默认的 0 参数构造函数。但是,在这个例子中,我们已经有了一个参数化的构造函数。所以,在这种情况下,编译器不会为我们提供默认的 0 参数构造函数。

因此,如果您想删除这个编译错误,您有以下选择:

  • 您可以像这样定义一个自定义构造函数:

    public ConsEx2() { }
    
    
  • 您可以从该程序中移除自定义构造函数声明(您已经定义但尚未使用)。

  • 您可以在您的Main()方法中提供必要的整数参数,如下所示:

    ConsEx2 ob2 = new ConsEx2(25);
    
    

学生问:

先生,我们能说那个类是自定义类型吗?

老师说:是的。

学生问:

先生,你能解释一下参考的概念吗?

老师说:是的。当我们写ClassA obA=new ClassA();ClassA 的一个实例将在内存中诞生,它创建一个对该实例的引用并将结果存储在obA变量中。所以,我们可以说内存中的对象被一个叫做“引用”的标识符引用。

稍后,当你进一步了解内存管理时,你会看到在 C# 中,我们主要使用两种类型的数据——值类型和引用类型。值类型存储在堆栈中,引用类型存储在堆中。因为对象是引用类型,所以它们存储在堆中。但是重要的是引用本身存储在一个堆栈中。所以,当我们写作时

ClassA obA=new Class A();

你可以想象如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们假设对象存储在堆地址 10001 中,obA 在堆栈中保存着这个线索。

学生问:

先生,为什么我们同时使用堆栈和堆?

老师说:这一级最简单的答案是,当引用变量超出范围时,它将从堆栈中删除,但实际数据仍然存在于堆中,直到程序终止或垃圾收集器清除该内存。因此,我们可以控制特定数据的生命周期。

学生问:

先生,那么引用基本上是用来指向一个地址的。这是正确的吗?

老师说:是的。

学生问:

先生,那么引用就类似于 C/C++中的指针。这是正确的吗?

老师说:引用似乎是一种特殊的指针。但是我们必须注意这两者之间的关键区别。通过指针,我们可以指向任何地址(基本上,它是内存中的一个数字槽)。因此,很有可能使用指针,我们可以指向一个无效的地址,然后我们可能会在运行时遇到不想要的结果。但是引用类型将总是指向有效的地址(在托管堆中),或者它们将指向 null。

很快,我们将学习 C# 中的一个关键概念。它被称为垃圾收集机制,用于回收内存。垃圾收集器不知道这些指针。因此,在 C# 中,指针不允许指向引用。稍后,您还将了解到,如果一个结构(在 C# 中称为 struct)包含一个引用,则指针类型不允许指向该结构。

为了简单起见,您可以记住,在 C# 中,指针类型只在“不安全”的上下文中出现。我将在本书的后面讨论这种“不安全”的环境。

学生问:

先生,我们如何检查引用变量是否指向 null?

老师说:下面的简单检查可以满足你的需要。为了便于参考,我可以在前面的程序中添加这几行代码。

     ......
ConsEx2 ob2 = new ConsEx2(25);
if (ob2 == null)
 {
  Console.WriteLine("ob2 is  null");
 }
else
{
 Console.WriteLine("ob2 is  NOT null");
}
    .....

学生问:

先生,多个变量可以引用内存中的同一个对象吗?

老师说:是的。以下类型的声明非常好:

ConsEx2 ob2 = new ConsEx2(25);
ConsEx2 ob1=ob2;

演示 3

在下面的例子中,我们创建了同一个类的两个对象,但是实例变量(i)用不同的值初始化。为了完成这项工作,我们使用了一个可以接受一个整数参数的参数化构造函数。

using System;

namespace ClassEx2
{
    class ClassA
    {
        public int i;
        public ClassA(int i)
        {
            this.i = i;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** A class demo with 2 objects ***");
            ClassA obA = new ClassA(10);
            ClassA obB = new ClassA(20);
            Console.WriteLine("obA.i =" + obA.i);
            Console.WriteLine("obB.i =" + obB.i);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

学生问:

先生,这是什么目的?

老师说:好问题。有时我们需要引用当前对象,为此,我们使用“this”关键字。在前面的例子中,不使用“this”关键字,我们也可以编写类似下面的代码来达到相同的结果。

class ClassA
{
        int i;//instance variable
        ClassA(int myInteger)//myInteger is a local variable here
        {
          i=myInteger;
        }
}

你熟悉像a=25这样的代码;这里我们给 a 赋值 25。但是你熟悉像25=a;这样的代码吗?不。编译器会引发一个问题。

在前面的例子中,myInteger是我们的局部变量(在方法、块或构造函数中可见),而i是我们的实例变量(在类中声明,但在方法、块或构造函数之外)。

所以,代替 myInteger,如果我们使用 I,我们需要告诉编译器我们的赋值方向。不应该混淆“哪个值被分配到哪里”这里我们将局部变量的值赋给实例变量,编译器应该清楚地理解我们的意图。有了this.i=i;,编译器会清楚地明白,应该用局部变量 I 的值来初始化实例变量 I。

我也可以从另一个角度来解释这个场景。假设,在前面的场景中,您错误地编写了类似 i=i 的内容。那么从编译器的角度来看就会出现混乱。因为在那种情况下,它看到你在处理两个相同的局部变量。(虽然你的本意不同,你的意思是左边的 I 是字段,另一个是方法参数)。现在,如果你为 ClassA 创建一个对象,obA,试着看看obA.i的值,代码如下:

ClassA obA = new ClassA(20);
Console.WriteLine("obA.i =" + obA.i);

您将得到 0(整数的默认值)。所以,你的实例变量不能得到你想要的值,20。在这种情况下,我们的 Visual Studio Community Edition IDE 也会发出警告:“对同一个变量进行了赋值,您是想给其他变量赋值吗?”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Points to Remember

与字段同名的方法参数会在方法体中隐藏整个字段。在这种场景中,关键字“this”帮助我们识别哪个是参数,哪个是字段。

演示 4

在下面的演示中,我们使用了两个不同的构造函数。用户定义的无参数构造函数总是用值 5 初始化实例变量 I,但是参数化构造函数可以用我们提供的任何整数值初始化实例变量。

using System;
class ClassA
{
    public int i;
    public ClassA()
    {
        this.i = 5;
    }
    public ClassA(int i)
    {
        this.i = i;
    }

}
class ClassEx4
{
    static void Main(string[] args)
    {
        Console.WriteLine("*** A Simple class with 2  different constructor ***");
        ClassA obA = new ClassA();
        ClassA obB = new ClassA(75);
        Console.WriteLine("obA.i =" + obA.i);
        Console.WriteLine("obB.i =" + obB.i);
        Console.ReadKey();
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

附加注释

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 前面,我们使用同一个构造函数来创建不同的对象,这些对象用不同的值进行初始化。在这个例子中,我们使用了不同的构造函数来创建用不同的值初始化的不同对象。
  • 在 Java 中,我们可以用 this (5)代替 this.i=5。但是在 C# 中,那种编码是不允许的。对于这种编码,我们会遇到如下编译错误:

演示 5

我提到过一个类可以同时有变量和方法。所以,现在我们要用一个返回整数的方法来创建一个类。该方法用于接受两个整数输入,然后返回这两个整数的和。

using System;

namespace InstanceMethodDemo
{
        class Ex5
        {
                public int Sum(int x, int y)
                {
                        return x + y;
                }
        }

        class Program
        {
                static void Main(string[] args)
                {
                        Console.WriteLine("*** A Simple class with a method returning an integer ***\n\n");
                        Ex5 ob = new Ex5();
                        int result = ob.Sum(57,63);
                        Console.WriteLine("Sum of 57 and 63 is : " + result);
                        Console.ReadKey();

                }
        }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对象初始化器

老师继续说:现在我们要学习创建物体的两种不同的技术。我们可以根据需要使用它们。考虑下面的程序,它后面是输出和分析。

演示 6

using System;

namespace ObjectInitializerEx1
{
    class Employee
    {
        public string Name;
        public int Id;
        public double Salary;
        //Parameterless constructor
        public Employee() { }
        //Constructor with one parameter
        public Employee(string name) { this.Name = name; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Object initializers Example-1***");

            //Part-1:Instantiating without Object Initializers

            //Using parameterless constructor
            Employee emp1 = new Employee();
            emp1.Name = "Amit";
            emp1.Id = 1;
            emp1.Salary = 10000.23;
            //Using the constructor with one parameter
            Employee emp2 = new Employee("Sumit");
            emp2.Id = 2;
            emp2.Salary = 20000.32;

            //Part-2:Instantiating with Object Initializers

            //Using parameterless constructor
            Employee emp3 = new Employee { Name = "Bob", Id = 3, Salary = 15000.53 };
            //Using the constructor with one parameter
            Employee emp4 = new Employee("Robin") { Id=4,Salary = 25000.35 };

            Console.WriteLine("Employee Details:");
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp1.Name,emp1.Id,emp1.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp2.Name,emp2.Id,emp2.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp3.Name, emp3.Id, emp3.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp4.Name, emp4.Id, emp4.Salary);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

请仔细注意以下部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个例子的第二部分,我们已经介绍了对象初始化器的概念。我们可以看到,与底部(第二部分)相比,在顶部(第一部分),我们需要编写更多的代码来完成对象(emp1 和 emp2)。在第二部分中,一行代码就足以实例化每个对象(emp3 和 emp4)。我们还试验了不同类型的构造函数。但是很明显,在所有情况下,对象初始化器都简化了实例化过程。这个概念是在 C# 3.0 中引入的。

可选参数

老师继续说:现在考虑下面的程序和输出。

演示 7

using System;

namespace OptionalParameterEx1
{
    class Employee
    {
        public string Name;
        public int Id;
        public double Salary;
        public Employee(string name = "Anonymous", int id = 0, double salary = 0.01)
        {
            this.Name = name;
            this.Id = id;
            this.Salary = salary;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Optional Parameter Example-1***");

            Employee emp1 = new Employee("Amit", 1, 10000.23);
            Employee emp2 = new Employee("Sumit", 2);
            Employee emp3 = new Employee("Bob");
            Employee emp4 = new Employee();

            Console.WriteLine("Employee Details:");
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp1.Name, emp1.Id, emp1.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp2.Name, emp2.Id, emp2.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp3.Name, emp3.Id, emp3.Salary);
            Console.WriteLine("Name ={0} Id={1} Salary={2}", emp4.Name, emp4.Id, emp4.Salary);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

这里我们使用了构造函数中可选参数的概念。这个构造函数需要三个参数:一个用于雇员的姓名,一个用于雇员的 ID,一个用于雇员的工资。但是如果我们传递的参数更少,编译器根本不会抱怨。另一方面,我们的应用选择了我们已经在可选参数列表中设置的默认值。从输出的最后一行,您可以看到 employee 对象的默认值是匿名的、0 和 0.01(对应于雇员的姓名、ID 和薪水)。

学生问:

先生,在 OOP 中我们看到代码总是被捆绑在对象中。这种类型的设计在现实场景中有什么好处?

老师说:其实有很多好处。从现实世界的场景思考;例如,考虑您的笔记本电脑或打印机。如果您的笔记本电脑中的任何部件出现故障,或者您的打印墨盒没有墨水了,您可以简单地更换这些部件。您不需要更换整个笔记本电脑或整个打印机。同样的概念也适用于其他真实世界的对象。

而且,您可以在类似型号的笔记本电脑或打印机中重复使用相同的部件。

除此之外,你必须同意我们不关心这些功能是如何在那些部分实际实现的。如果这些部分工作正常,满足我们的需求,我们就很高兴。

在面向对象编程中,对象扮演着同样的角色:它们可以被重用,也可以被插入。同时,他们隐藏了实现细节。例如,在演示 5 中,我们可以看到,当我们调用带有两个整数参数(57 和 63)的 Sum()方法时,我们知道我们将得到这些整数的和。外部用户完全不知道该方法的内部机制。因此,我们可以通过向外界隐藏这些信息来提供一定程度的安全性。

最后,从另一个编码的角度来看,假设下面的场景。假设您需要在程序中存储员工信息。如果你开始这样编码:

string empName= "emp1Name";
string deptName= "Comp.Sc.";
int empSalary= "10000";

然后对于第二个雇员,我们必须这样写:

string empName2= "emp2Name";
string deptName2= "Electrical";
int empSalary2= "20000";

等等。

真的可以这样继续下去吗?答案是否定的。简单地说,像这样创建一个雇员类和过程总是一个更好的主意:

Employee emp1, emp2;

它更干净、可读性更强,显然是一种更好的方法。

学生问:

先生,到目前为止,我们已经讨论了构造函数,但没有讨论析构函数。为什么呢?

老师说:我将在第十四章(内存清理)讨论带有垃圾收集的析构函数。

摘要

本章讨论了以下主题。

  • 类、对象和引用的概念
  • 对象和引用之间的区别
  • 指针和引用的区别
  • 局部变量和实例变量的区别
  • 不同类型的构造函数及其用法
  • 用户定义的无参数构造函数和 C# 提供的默认构造函数之间的区别
  • this 关键字
  • 对象初始化器的概念
  • 可选参数的概念
  • 面向对象方法在现实编程中的好处

三、继承的概念

教师开始讨论:继承的主要目的是促进可重用性和消除冗余(代码)。基本思想是,子类可以获得其父类的特征/特性。在编程术语中,我们说子类是从它的父类/基类派生出来的。因此,父类被放在类层次结构中的更高一级。

类型

一般来说,我们处理四种类型的继承。

  • 单一继承:子类从一个基类派生而来
  • 分层继承:一个基类可以派生出多个子类
  • 多级继承:父类有一个孙类
  • 多重继承:一个孩子可以来自多个父母

Points to Remember

  • C# 不支持多重继承(通过类);也就是说,子类不能从多个父类派生。为了处理这种情况,我们需要理解接口。
  • 还有另一种类型的继承被称为混合继承。它是两种或两种以上遗传类型的结合。
| 图表 | 带代码格式的类型 | | :-- | :-- | | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Finter-cs-fund%2Fimg%2FA460204_1_En_3_Figa_HTML.jpg&pos_id=img-Lh7VuQ2d-1723307204458) | 单继承:`#region Single Inheritance``class Parent``{``//Some code``}``class Child : Parent``{``//Some code``}``#endregion` | | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Finter-cs-fund%2Fimg%2FA460204_1_En_3_Figb_HTML.jpg&pos_id=img-1JwHHqmH-1723307204459) | 分层继承:`#region Hierarchical Inheritance``class Parent``{``//Some code``}``class Child1 : Parent``{``//Some code``}``class Child2 : Parent``{``//Some code``}``#endregion` | | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Finter-cs-fund%2Fimg%2FA460204_1_En_3_Figc_HTML.jpg&pos_id=img-C5uLHCSh-1723307204459) | 多级继承:`#region Multilevel Inheritance``class Parent``{``//Some code``}``class Child : Parent``{``//Some code``}``class GrandChild : Child``{``//Some code``}``#endregion` | | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Finter-cs-fund%2Fimg%2FA460204_1_En_3_Figd_HTML.jpg&pos_id=img-5QrWAl06-1723307204459) | 多重继承:在 C# 中不支持通过类继承。我们需要了解接口。下面举个例子:`#region Multiple Inheritance``interface``IInter1``{``//Some Code``}``interface``IInter2``{``//Some code``}``class MyClass : IInter1, IInter2``{``//Some code``}``#endregion` |

让我们从一个关于继承的简单程序开始。

演示 1

using System;

namespace InheritanceEx1
{
    class ParentClass
    {
        public void ShowParent()
        {
            Console.WriteLine("In Parent");
        }
    }
    class ChildClass :ParentClass
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing Inheritance***\n\n");
            ChildClass child1 = new ChildClass();
            //Invoking ShowParent()through ChildClass object
            child1.ShowParent();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

附加注释

我们已经通过一个子类对象调用了ShowParent()方法。

Points to Remember

  • 请记住,在 C# 中,Object 是。NET 框架。换句话说,System.Object是类型层次结构中的最终基类。
  • 除了构造函数(实例和静态)和析构函数,所有成员都是继承的(也就是说,这与访问说明符无关)。但是由于它们的可访问性限制,所有继承的成员在子类/派生类中可能都不可访问。
  • 子类可以添加新成员,但不能删除父成员的定义。(就像你可以为自己选择一个新名字,但不能改变父母的姓氏一样)。
  • 继承层次结构是可传递的;也就是说,如果类 C 继承了类 B,而类 B 又派生自类 A,那么类 C 就包含了类 B 和类 A 的所有成员。

学生问:

这意味着私有成员也继承了。这种理解正确吗?

老师说:是的。

学生问:

我们如何检查私有成员也被继承的事实?

老师说:你可以参考演示 2 中显示的程序和输出。

演示 2

using System;

namespace InheritanceWithPrivateMemberTest
{
    class A
    {
        private int a;
    }
    class B : A { }

    class Program
    {
        static void Main(string[] args)
        {
            B obB = new B();
            A obA = new A();
            //This is a proof that a is also inherited. See the error message.
            Console.WriteLine(obB.a);//A.a is inaccessible due to its
            //protection level
            Console.WriteLine(obB.b);//'B' does not contain a definition
            //for 'b' and no extension ......
            Console.WriteLine(obA.b);//'A' does not contain a definition
            //for 'b' and no extension ......
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

我们遇到了两种不同类型的错误:CS0122 和 CS1061。

  • CS0122: A.a 由于其保护级别而无法访问。它指示来自类 A 的私有成员 A 在子类 b 中被继承。
  • CS1061:我们用另一个字段测试了输出,该字段不在此类层次结构中(即,该字段不存在,既不在 A 中也不在 B 中)。当我们试图用 A 类或 B 类对象访问成员时,我们遇到了一个不同的错误。因此,如果 a 在 B 类中不存在,那么你应该得到一个类似的错误。

学生问:

为什么 C# 不支持通过类的多重继承?

老师说:主要原因是为了避免歧义。在典型的场景中,它会造成混乱;例如,假设我们的父类中有一个名为Show()的方法。父类有多个子类,比如 Child1 和 Child2,它们为了自己的目的正在重新定义(用编程术语来说,重写)方法。代码可能类似于演示 3 所示。

演示 3

class Parent
    {
        public void Show()
        {
        Console.WriteLine("I am in Parent");
        }
    }
    class Child1: Parent
    {
        public void Show()
        {
        Console.WriteLine("I am in Child-1");
        }
    }
    class Child2:Parent
    {
        public void Show()
        {
        Console.WriteLine("I am in Child-2");
        }
    }

现在,让我们假设另一个名为孙的类派生自 Child1 和 Child2,但是它没有覆盖Show()方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以,现在我们有了歧义:孙子将从哪个类继承/调用Show()——Child 1 还是 Child 2?为了避免这种类型的歧义,C# 不支持通过类的多重继承。这就是所谓的钻石问题。

所以,如果你这样编码:

    class GrandChild : Child1, Child2//Error: Diamond Effect
    {
        public void Show()
        {
            Console.WriteLine("I am in Child-2");
        }
    }

C# 编译器会报错:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

所以,编程语言不支持多重继承。这种理解正确吗?

老师说:不。这个决定是由编程语言的设计者做出的(例如,C++支持多重继承的概念)。

学生问:

为什么 C++设计者支持多重继承?似乎钻石问题也会影响到他们。

老师说:我试图从我的角度来解释。他们可能不想放弃多重继承(也就是说,他们希望包含这个特性来丰富语言)。他们为您提供支持,但将正确使用的控制权留给了您。

另一方面,由于这种支持,C# 设计者希望避免任何不想要的结果。他们只是想让语言更简单,更不容易出错。

老师问:

C# 中有混合继承吗?

老师解释:仔细想想。混合继承是两种或两种以上继承的结合。所以,如果你不想通过类来组合任何类型的多重继承,这个问题的答案是肯定的。但是,如果你试图用任何类型的多重继承(通过类)进行混合继承,C# 编译器将立即提出它的关注。

老师问:

假设我们有一个父类和一个子类。我们能猜到类的构造函数会以什么顺序被调用吗?

老师说:我们必须记住,构造函数的调用遵循从父类到子类的路径。让我们用一个简单的例子来测试一下:我们有一个父类 parent、一个子类 child 和一个孙类 grande。顾名思义,子类派生自父类,孙类派生自子类。我们已经创建了一个孙类的对象。请注意,构造函数是按照它们的派生顺序调用的。

演示 4

using System;

namespace ConstructorCallSequenceTest
{
    class Parent
    {
        public Parent()
        {
            Console.WriteLine("At present: I am in Parent Constructor");
        }
    }
    class Child : Parent
    {
        public Child()

        {
            Console.WriteLine("At present: I am in Child Constructor");
        }
    }
    class GrandChild : Child
    {
        public GrandChild()
        {
            Console.WriteLine("At present: I am in GrandChild Constructor");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing the call sequence of constructors***\n\n");
            GrandChild grandChild = new GrandChild();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

学生问:

先生,有时候我们不确定。在继承层次结构中,谁应该是父类,谁应该是子类?我们如何处理这种情况?

老师说:你可以试着记住一句简单的话:足球运动员就是运动员,但反过来就不一定了。或者,公共汽车是一种交通工具,但反过来就不一定了。这种“是-a”测试可以帮助你决定谁应该是父母;例如,“运动员”是父类,“足球运动员”是子类。

我们还通过这个“is-a”测试来预先确定我们是否可以将一个类放在同一个继承层次中。

一个特殊的关键词:基础

在 C# 中,有一个特殊的关键字叫做 base。它用于以有效的方式访问父类(也称为基类)的成员。每当子类想要引用它的直接父类时,它可以使用 base 关键字。

让我们通过两个简单的例子来研究 base 关键字的不同用法。

演示 5

using System;

namespace UseOfbaseKeywordEx1
{
    class Parent
    {
        private int a;
        private int b;
        public Parent(int a, int b)
        {
            Console.WriteLine("I am in Parent constructor");
            Console.WriteLine("Setting the value for instance variable  a and b");
            this.a = a;
            this.b = b;
            Console.WriteLine("a={0}", this.a);
            Console.WriteLine("b={0}", this.b);
        }
}
    class Child : Parent
    {
        private int c;
        public Child(int a, int b,int c):base(a,b)

        {
            Console.WriteLine("I am in Child constructor");
            Console.WriteLine("Setting the value for instance variable c");
            this.c = c;
            Console.WriteLine("c={0}", this.c);
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing the use of base keyword. Example-1***\n\n");
            Child obChild = new Child(1, 2, 3);
            //Console.WriteLine("a in ObB2={0}", obChild.a);// a is private,
 //so Child.a is inaccessible
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

我们需要了解为什么有必要使用关键字 base。如果我们在前面的示例中没有使用它,我们将需要编写类似如下的代码:

public Child(int a, int b, int c)
        {
            this.a = a;
            this.b = b;
            this.c = c;
        }

这种方法有两个主要问题。您试图编写重复的代码来初始化实例变量 a 和 b。在这种特殊情况下,您将收到编译错误,因为 a 和 b 由于其保护级别而不可访问(请注意它们是私有的)。通过使用“base”关键字,我们有效地处理了这两种情况。虽然我们在子类构造函数声明之后写了“base”这个词,但是父类构造函数在子类构造函数之前被调用。理想情况下,这是我们的真实意图。

Points to Remember

前面你已经看到,当我们初始化一个对象时,构造函数体在父类到子类的方向上执行。但是初始化的相反方向(即子到父)随着字段的初始化(以及父类构造函数调用的参数)而发生。*

在 C# 中,不能使用一个实例字段在方法体外部初始化另一个实例字段。

*我认为以下 MSDN 资源值得一读:

  • https://blogs.msdn.microsoft.com/ericlippert/2008/02/15/why-do-initializers-run-in-the-opposite-order-as-constructors-part-one/
  • https://blogs.msdn.microsoft.com/ericlippert/2008/02/18/why-do-initializers-run-in-the-opposite-order-as-constructors-part-two/

恶作剧

产量是多少?

using System;

namespace FieldInitializationOrderEx1
{
    class A
    {
        int x = 10;
        int y = x + 2;//Error
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Analyzing C#'s field initialization order ***");
            int x = 10;
            int y = x + 2;//ok
            Console.WriteLine("x={0}", x);
            Console.WriteLine("y={0}", y);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

这个限制是由 C# 的设计者实现的。

学生问:

为什么 C# 设计者设置了这个限制(与错误 CS0236 相关)?

老师说:这个话题有很多讨论。上例中的语句y=x+2;相当于y=this.x+2;this”表示当前对象,因此,如果我们要像this.x一样进行调用,需要先完成当前对象。但是在某些情况下(例如,如果x是一个还没有被创建的属性(而不是一个字段),或者它是另一个实例的一部分,等等),当前对象可能还没有完成。)我们将很快了解更多关于属性的内容。我们还应该记住,创建构造函数是为了处理这种初始化。因此,如果允许这些类型的构造,他们也可以质疑构造函数的用途。

老师继续说:现在我们来看看下面这个例子中 base 关键字的另一种用法。注意,在这个例子中,我们通过基类方法中的 base 关键字调用父类方法(ParentMethod())。

演示 6

using System;

namespace UseOfbaseKeywordEx2
{
    class Parent
    {
        public void ParentMethod()
        {
            Console.WriteLine("I am inside the Parent method");
        }
    }
    class Child : Parent
    {
        public void childMethod()
        {
               Console.WriteLine("I am inside the Child method");
               Console.WriteLine("I am calling the Parent method now");
               base.ParentMethod();
        }

}

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing the use of base keyword. Example-2***\n\n");
            Child obChild = new Child();
            obChild.childMethod();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Points to Remember

  • 根据语言规范,基类访问只允许在构造函数、实例方法或实例属性访问中进行。
  • 关键字“base”不应在静态方法上下文中使用。
  • 它类似于 Java 中的“super”关键字和 C++中的“base”关键字。它几乎与 C++的 base 关键字相同,它就是从这个关键字被采用的。然而,在 Java 中,有一个限制规定“super”应该是第一个语句。Oracle Java 文档说超类构造函数的调用必须是子类构造函数的第一行。

学生问:

先生,假设有一些方法在父类和子类中都有一个共同的名字。如果我们创建一个子类对象,尝试调用同一个命名方法,会调用哪个?

老师说:你试图在这里引入方法覆盖的概念。我们将在关于多态的章节中进一步讨论它。但是要回答您的问题,请考虑下面的程序和输出。

演示 7

using System;

namespace UseOfbaseKeywordEx3
{
    class Parent
    {
        public void ShowMe()
        {
            Console.WriteLine("I am inside the Parent method");
        }
    }
    class Child : Parent
    {

        public void ShowMe()
        {
            Console.WriteLine("I am inside the Child method");
            //base.ParentMethod();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing the use of base keyword. Example-3***\n\n");
            Child obChild = new Child();
            obChild.ShowMe();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

在这种情况下,您的程序被编译并运行,但是您应该注意到您会收到一条警告消息,提示您的派生类方法隐藏了继承的父类方法,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,如果您想调用父类方法,您可以简单地使用子类方法中的代码,就像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

base 关键字可用于以下任何一种情况:

  • 调用父类中定义的隐藏/重写方法。
  • 我们可以在创建派生类的实例时指定特定的基类构造函数版本(参见演示 5)。

学生问:

在我看来,子类可以使用它的超类方法。但是有什么方法可以让超类使用它的子类方法呢?

老师说:不。你必须记住,超类是在它的子类之前完成的,所以它不知道它的子类方法。它只声明一些(想想一些契约/方法)可以被它的子节点使用的东西。只有付出而不期望从孩子那里得到回报。

如果你仔细观察,你会发现“是-a”测试是单向的(例如,足球运动员总是运动员,但反过来就不一定了;所以没有向后继承的概念)。

学生问:

先生,所以每当我们想使用一个父类方法,并把额外的东西放入其中,我们可以使用关键字 base。这种理解正确吗?

老师说:是的。

学生问:

先生,在 OOP 中,继承帮助我们重用行为。还有其他方法可以达到同样的效果吗?

老师说:是的。尽管继承的概念在很多地方被使用,但它并不总是提供最佳的解决方案。为了更好地理解它,您需要理解设计模式的概念。一个非常常见的替代方法是使用组合的概念,这将在后面介绍。

学生问:

先生,如果一个用户已经为他的应用创建了一个方法,我们应该通过继承的概念来重用同一个方法,以避免重复工作。这种理解正确吗?

老师说:一点也不。我们不应该以这种方式概括继承。这取决于特定的应用。假设已经有人做了一个Show()方法来描述一个汽车类的细节。现在让我们假设你也创建了一个名为Animal的类,你也需要用一个方法描述一个动物的特征。假设您也认为名称“Show()”最适合您的方法。在这种情况下,因为我们已经有了一个名为Car and if you think that you need to reuse t的类中的Show()方法,而你的Animal类中的【何】方法,你可以写这样的代码:

Class Animal: Car{...} .

现在想一想。“这样的设计好吗?”你必须同意汽车和动物之间没有关系。因此,我们不应该在相同的继承层次结构中关联它们。

学生问:

我们如何继承构造函数或析构函数?

在本章的开始,我提到了构造函数(静态和非静态)和析构函数是不被继承的。

摘要

本章涵盖了以下主题。

  • 继承的概念
  • 继承的不同类型
  • 为什么 C# 不支持通过类的多重继承
  • C# 中允许的混合继承类型
  • “base”关键字的不同用法
  • C# 的 base 关键字和 Java 的 super 关键字的简单比较
  • 继承层次结构中的构造函数调用序列
  • 如果父类方法的子类也包含同名的方法,如何调用父类方法
  • 如何将类放入继承层次结构中
  • 继承概念的正确使用

还有更多。

四、熟悉多态

老师开始讨论:让我们回忆一下本书开始时我们讨论的关于多态的内容。多态通常与一个名称的多种形式相关联;例如,如果我们有两个整数操作数进行加法运算,我们期望得到整数的和,但是如果操作数是两个字符串,我们期望得到一个连接的字符串。我还提到多态有两种类型:编译时多态和运行时多态。

这里我们将从编译时多态开始讨论。

在编译时多态中,编译器可以在编译时将适当的方法绑定到相应的对象,因为它具有所有必要的信息(例如,方法参数)。因此,一旦程序被编译,它就能更早地决定调用哪个方法。这就是为什么它也被称为静态绑定或早期绑定。

在 C# 中,编译时多态可以通过方法重载和运算符重载来实现。

Points to Remember

在 C# 中,方法重载和运算符重载可以帮助我们实现编译时多态。

方法重载

老师继续:先说一个程序。考虑下面的程序和相应的输出。你注意到什么特别的模式了吗?

演示 1

using System;

namespace OverloadingEx1
{
    class OverloadEx1
    {
        public int Add(int x, int y)
        {
            return x + y;
        }
        public double Add(double x, double y)
        {
            return x + y;
        }
        public string Add(String s1, String s2)
        {
            return string.Concat(s1, s2);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Concept of method Overloading***\n\n");
            OverloadEx1 ob = new OverloadEx1();
            Console.WriteLine("2+3={0}", ob.Add(2, 3));
            Console.WriteLine("20.5+30.7={0}", ob.Add(20.5, 30.7));
            Console.WriteLine("Amit + Bose ={0}", ob.Add("Amit","Bose"));
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

学生回应:可以。我们看到所有的方法都有相同的名字“Add ”,但是从它们的方法体来看,似乎每个方法都在做不同的事情。

老师说:正确的观察。当我们做这种编码时,我们称之为方法重载。但是您还应该注意到,在这种情况下,方法名称是相同的,但是方法签名是不同的。

学生问:

什么是方法签名?

老师说:理想情况下,方法名和参数的数量和类型组成了它的签名。C# 编译器可以区分名称相同但参数列表不同的方法;例如,对于一个 C# 编译器来说,double Add(double x, double y)int Add(int x, int y)是不同的。

恶作剧

下面的代码段是方法重载的一个例子。这样对吗?

class OverloadEx1
  {
      public int Add(int x, int y)
      {
          return x + y;
      }
      public double Add(int x, int y, int z)
      {
          return x + y+ z;
      }
  }

回答

是的。

恶作剧

下面的代码段是方法重载的一个例子吗?

class OverloadEx1
 {
     public int Add(int x, int y)
     {
         return x + y;
     }
     public double Add(int x, int y)
     {
         return x + y;
     }
 }

回答

不会。编译器不会考虑“返回类型”来区分这些方法。我们必须记住,返回类型不被认为是方法签名的一部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,我们可以让构造函数重载吗?

老师说:当然。你可以为构造函数重载写一个类似的程序。

演示 2

using System;

namespace ConstructorOverloadingEx1
{
    class ConsOverloadEx
    {
        public ConsOverloadEx()
        {
            Console.WriteLine("Constructor with no argument");
        }
        public ConsOverloadEx(int a)
        {
            Console.WriteLine("Constructor with one integer argument {0}", a);
        }
        public ConsOverloadEx(int a, double b)
        {
            Console.WriteLine("You have passed one integer argument {0} and one double argument {1} in the constructor", a,b);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Constructor overloading Demo***\n\n");
            ConsOverloadEx ob1 = new ConsOverloadEx();
            ConsOverloadEx ob2 = new ConsOverloadEx(25);
            ConsOverloadEx ob3 = new ConsOverloadEx(10,25.5);
            //ConsOverloadEx ob4 = new ConsOverloadEx(37.5);//Error
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

学生问:

先生,这似乎也是方法重载。构造函数和方法有什么区别?

老师澄清:我们已经在关于类的讨论中谈到了构造函数。作为参考,构造函数与类同名,并且没有返回类型。因此,您可以将构造函数视为一种特殊的方法,它与类同名,并且没有返回类型。但是还有许多其他的区别:构造函数的主要焦点是初始化对象。我们不能直接打电话给他们。

学生问:

先生,我们能这样写代码吗?

演示 3

class ConsOverloadEx
    {
        public ConsOverloadEx()
        {
            Console.WriteLine("A Constructor with no argument");
        }
        public void ConsOverloadEx()
        {
            Console.WriteLine("a method");
        }
    }

老师说:Java 8 允许这样做,但是 C# 编译器会出错。

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,我们能重载 Main()方法吗?

老师说:是的。可以考虑以下方案。

演示 4

using System;

namespace OverloadingMainEx
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing Overloaded version of Main()***");
            Console.WriteLine("I am inside Main(string[] args) now");
            Console.WriteLine("Calling overloaded version\n");
            Main(5);
            //Console.WriteLine("***Concept of method Overloading***\n\n");
            Console.ReadKey();
        }
        static void Main(int a)
        {
            Console.WriteLine("I am inside Main(int a) now");
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

虽然您可以编译并运行前面的程序,但编译器会显示以下警告消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,那么如果我们增加一个方法体,为什么我们会得到一个编译错误,就像下面这样?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师说:根据规范,你的程序可以有一个带有 Main(string[] args)或 Main()方法的入口点。这里出现了 Main 方法的两个版本。这就是为什么编译器不知道使用哪一个作为入口点。因此,您需要按照编译器的建议来决定入口点。如果您简单地删除或注释掉 Main(string[] args)版本,您的程序可以成功编译,然后如果您运行该程序,您将收到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

恶作剧

我们的程序中可以有多个 Main()方法吗,如下所示?

演示 5

using System;

namespace MultipleMainTest
{
    class Program1
    {
        static void Main(string[] args)
        {
            Console.WriteLine("I am inside Program1.Main(string[] args) now");
            Console.ReadKey();
        }
    }
    class Program2
    {
        static void Main()
        {
            Console.WriteLine("I am inside Program2.Main() now");
            Console.ReadKey();
        }
    }
}

老师说:你会得到以下错误:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了避免这个错误,您可以从您的项目属性中设置入口点(这里我们从 Program2 中选择了 Main())。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,如果您运行该程序,您将获得以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

建议/良好的编程实践

如果可能的话,尽量与重载方法的参数名及其对应的顺序保持一致。

下面是一个good design的例子:

public void ShowMe(int a) {..}

public void ShowMe(int a, int b){...}

[Note that in 2nd line, the position of int a is same as 1st case]

Bad design:

public void ShowMe(int a) {..}

public void ShowMe(int x, int b){...}

[Note that in 2nd line, we start with int x instead of int a]

老师继续说:到目前为止,我们已经用方法重载测试了编译时多态。让我们也用运算符重载来测试这种风格。

运算符重载

每个操作符都有自己的功能;比如+可以把两个整数相加。通过操作符重载技术,我们可以用它来连接两个字符串;也就是说,我们可以对不同类型的操作数执行类似的机制。换句话说,我们可以简单地说操作符重载帮助我们为操作符提供特殊的/额外的含义。

学生问:

先生,那么有人可能会误用这个概念。它们可以重载与运算符重载相矛盾的操作;例如,++运算符也可用于递减。理解正确吗?

老师说:是的,我们需要小心。我们不应该使用++运算符来递减。如果我们这样做,那将是一个极其糟糕的设计。除此之外,我们必须注意 C# 不允许我们重载所有的操作符。MSDN 提供了以下指导方针。

| 经营者 | 过载能力 | | :-- | :-- | | +, -, !,~,++, -,真,假 | 我们可以支配这些一元运算符。 | | +,-,*,/,%,&,|,^,<> | 我们可以支配这些二元运算符。 | | ==, !=,,<=, > = | 比较运算符可以重载(但请参见该表后面的注释)。 | | &&, || | 条件逻辑运算符不能重载,但它们使用&和|进行计算,这可以重载。 | | [ ] | 我们不能重载数组索引操作符,但是我们可以定义索引器。 | | (T)x | 我们不能重载转换操作符,但是我们可以定义新的转换操作符(例如,在显式和隐式的上下文中) | | +=,-=,*=,/=,%=,&=,|=,^=,<<=, > >= | 我们不能重载赋值运算符,而是+=;例如,使用可以重载的+来计算。 | | =, ., ?:, ??、->、= >、f(x)、as、选中、未选中、默认、委托、is、新建、sizeof、typeof | 我们不能支配这些经营者。 |

Note

如果重载,比较运算符必须成对重载;比如= =重载,我们就需要重载!=也。反之亦然,类似于< and >和<= and > =。

老师说:让我们跟着演示。这里我们将一元运算符++应用于一个矩形对象,以增加矩形对象的长度和宽度。

演示 6

using System;
namespace OperatorOverloadingEx
{
    class Rectangle
    {
        public double length, breadth;
        public Rectangle(double length, double breadth)
        {
            this.length = length;
            this.breadth = breadth;
        }
        public double AreaOfRectangle()
        {
            return length * breadth;
        }
        public static Rectangle operator ++ (Rectangle rect)
        {
            rect.length ++;
            rect.breadth++;
            return rect;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Operator Overloading Demo:Overloading ++ operator***\n");
            Rectangle rect = new Rectangle(5, 7);
            Console.WriteLine("Length={0} Unit Breadth={1} Unit", rect.length,rect.breadth);
            Console.WriteLine("Area of Rectangle={0} Sq. Unit",rect.AreaOfRectangle());
            rect++;
            Console.WriteLine("Modified Length={0} Unit Breadth={1} Unit", rect.length, rect.breadth);
            Console.WriteLine("Area of new Rectangle={0} Sq. Unit", rect.AreaOfRectangle());
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在让我们重载二元运算符+。

演示 7

using System;
namespace OperatorOverloadingEx2
{
    class ComplexNumber
    {
        public double real,imaganinary;
        public ComplexNumber()
        {
            this.real = 0;
            this.imaganinary = 0;
        }
        public ComplexNumber(double real, double imaginary )
        {
            this.real = real;
            this.imaganinary = imaginary;
        }
        //Overloading a binary operator +
        public static ComplexNumber operator +(ComplexNumber cnumber1, ComplexNumber cnumber2)
        {
            ComplexNumber temp = new ComplexNumber();
            temp.real = cnumber1.real + cnumber2.real;
            temp.imaganinary = cnumber1.imaganinary + cnumber2.imaganinary;
            return temp;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Operator Overloading Demo 2:Overloading binary operator + operator***\n");
            ComplexNumber cNumber1 = new ComplexNumber(2.1, 3.2);
            Console.WriteLine("Complex Number1: {0}+{1}i", cNumber1.real,cNumber1.imaganinary);
            ComplexNumber cNumber2 = new ComplexNumber(1.1, 2.1);
            Console.WriteLine("Complex Number2: {0}+{1}i", cNumber2.real, cNumber2.imaganinary);
            //Using the + operator on Complex numbers
            ComplexNumber cNumber3 = cNumber1 + cNumber2;
            Console.WriteLine("After applying + operator we have got: {0}+{1}i", cNumber3.real, cNumber3.imaganinary);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

学生问:

先生,在运算符重载的例子中,您使用了关键字“static”。这是故意的吗?

老师说:是的。我们必须记住一些关键的限制。

  • 运算符函数必须标记为 public 和 static。

否则,您可能会遇到这种错误:

  • 关键字 operator 后跟运算符符号。
  • 函数参数在这里是操作数,并且返回作为表达式结果的操作符函数的类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Points to Remember

  • 运算符函数必须标记为 public 和 static。
  • 关键字 operator 后跟运算符符号。

方法覆盖

老师继续说:有时我们想重新定义或修改我们的父类的行为。在这种情况下,方法重写就成了问题。考虑下面的程序和输出。然后在分析部分仔细检查每一点。

演示 8

using System;

namespace OverridingEx1
{
    class ParentClass
    {
        public virtual void ShowMe()
        {
            Console.WriteLine("Inside Parent.ShowMe");
        }
        public void DoNotChangeMe()
        {
            Console.WriteLine("Inside Parent.DoNotChangeMe");
        }
    }
    class ChildClass :ParentClass
    {

        public override void ShowMe()
        {
            Console.WriteLine("Inside Child.ShowMe");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Method Overriding Demo***\n\n");
            ChildClass childOb = new ChildClass();
            childOb.ShowMe();//Calling Child version
            childOb.DoNotChangeMe();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

在前面的程序中,我们看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如果您在前面的程序中省略了单词 virtual 和 override,您将收到以下警告消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如果您使用虚拟关键字但省略 override 关键字,您将再次收到以下警告消息(您可以运行该程序):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 顾名思义,ChildClass 是一个派生类,其父类是 ParentClass。
  • ParentClass 和 ChildClass 中都定义了具有相同签名和返回类型的名为 ShowMe()的方法。
  • 在 Main()方法中,我们创建了一个子类对象 childOb。当我们通过这个对象调用方法 DoNotChangeMe()时,它可以调用方法(遵循继承属性)。没有魔法。
  • 但是当我们通过这个对象调用方法 ShowMe()时,它调用的是 ChildClass 中定义的 ShowMe()版本;也就是说,父方法版本被覆盖。因此,这种情况称为方法重写。
  • 现在请仔细注意:我们是如何在 ChildClass 中重新定义 ShowMe()方法的。我们使用了两个特殊的关键字——虚拟和覆盖。使用关键字 virtual,我们的意思是该方法可以在子类/派生类中重新定义。而 override 关键字是在确认我们是在有意的重定义父类的方法。
  • 如果您在前面的程序中省略了单词 virtual,您将收到以下编译错误:

我们将很快在这个上下文中讨论关键字“new”。

虚方法和重写方法的返回类型、签名和访问说明符必须相同。例如,在前面的示例中,如果在子类的 ShowMe()中将可访问性从 public 更改为 protected,如下所示:

protected override void ShowMe()
    {
        Console.WriteLine("Inside Child.ShowMe");
    }

您将收到编译错误:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,在方法重载中,返回类型并不重要。但这很重要。这是正确的吗?

老师说:是的。我们必须记住,虚方法和重写方法的签名、返回类型和可访问性应该匹配。

学生问:

先生,下面的程序会收到任何编译错误吗?

演示 9

class ParentClass
    {
        public virtual int ShowMe(int i)
        {
            Console.WriteLine("I am in Parent class");
            return i;
        }
    }
    class ChildClass : ParentClass
    {
        public override void ShowMe(int i)
        {
            Console.WriteLine("I am in Child class");
         }
    }

老师说:是的。您将得到以下错误:

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,为了克服这一点,正如编译器所建议的,您可以将方法(在子类中)的返回类型更改为 int,并在方法体中进行一些必要的更改,如下所示:

public override int ShowMe(int i)
        {
            Console.WriteLine("I am in Child class");
            Console.WriteLine("Incrementing i by 5");
            return i +5;//Must return  an int
        }

或者,您可以使用具有 void 返回类型的方法,如下所示(但这次它将被视为方法重载):

public void ShowMe()
        {
            Console.WriteLine("In Child.ShowMe()");
        }

如果在程序中使用这两个重新定义的方法,实际上是在实现方法重载和方法重写。浏览下面的例子。

演示 10

using System;

namespace OverridingEx2
{
    class ParentClass
    {
        public virtual int ShowMe(int i)
        {
            Console.WriteLine("I am in Parent class");
            return i;
        }
    }
    class ChildClass : ParentClass
    {
        public override int ShowMe(int i)
        {
            Console.WriteLine("I am in Child class");
            Console.WriteLine("Incrementing i by 5");
            return i + 5;//Must return  an int
        }
        public void ShowMe()
        {
            Console.WriteLine("In Child.ShowMe()");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Overloading with Overriding Demo***\n");
            ChildClass childOb = new ChildClass();
            Console.WriteLine(childOb.ShowMe(5));//10
            childOb.ShowMe();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师说:据说面向对象的程序员要经过三个重要阶段。在第一阶段,他们熟悉非面向对象的构造/结构(例如,他们使用决策语句、循环构造等。).在第二阶段,他们开始创建类和对象,并使用继承机制。最后在第三阶段,他们使用多态来实现延迟绑定,使他们的程序更加灵活。所以让我们来看看如何在 C# 程序中实现多态。

多态实验

老师继续说:多态通常与一个具有多种形式/结构的方法名称相关联。为了更好的理解它,我们需要先明确核心概念。所以,看看程序及其相应的输出。

演示 11

using System;

namespace BaseRefToChildObjectEx1
{
          class Vehicle
            {
            public void ShowMe()
            {
                Console.WriteLine("Inside Vehicle.ShowMe");
            }
        }
        class Bus : Vehicle
        {
            public void ShowMe()
            {
                Console.WriteLine("Inside Bus.ShowMe");
            }
            public void BusSpecificMethod()
            {
                Console.WriteLine("Inside Bus.ShowMe");
            }
        }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Base Class reference to Child Class Object Demo***\n\n");
            Vehicle obVehicle = new Bus();
            obVehicle.ShowMe();//Inside Vehicle.ShowMe
            // obVehicle.BusSpecificMethod();//Error
            //Bus obBus = new Vehicle();//Error
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

请注意前面程序中的两行重要代码:

Vehicle obVehicle = new Bus();
obVehicle.ShowMe();

这里我们通过一个父类引用(Vehicle 引用)指向一个派生类对象(Bus 对象),然后我们调用 ShowMe()方法。这种调用方式是允许的,我们不会收到任何编译问题;也就是说,基类引用可以指向派生类对象。

但是我们不能使用这两条线:

  1. obVehicle.BusSpecificMethod();//Error(因为这里的表观类型是车辆而不是公共汽车)。要消除这个错误,你需要向下转换,如下:

    ((Bus)obVehicle).BusSpecificMethod();
    
    
  2. Bus obBus = new Vehicle();//Error

如前所述,要消除此错误,需要进行向下转换,如下所示:

Bus obBus = (Bus)new Vehicle();

Points to Remember

通过父类引用,我们可以引用子类对象,但反之则不然。对象引用可以隐式向上转换为基类引用,并显式向下转换为派生类引用。我们将在一些关键比较的分析章节(第八章)中详细了解向上转换和向下转换操作。

现在我们将使用关键字 virtual 和 override 稍微修改一下程序,如下所示。请注意,我们用 virtual 标记了父类(Vehicle)方法,用 override 标记了子类(Bus)方法。

演示 12

using System;

namespace PloymorphismEx1
{
       class Vehicle
        {
            public virtual void ShowMe()
            {
                Console.WriteLine("Inside Vehicle.ShowMe");
            }
        }
        class Bus : Vehicle
        {
            public override void ShowMe()
            {
                Console.WriteLine("Inside Bus.ShowMe");
            }
            public void BusSpecificMethod()
            {
                Console.WriteLine("Inside Bus.ShowMe");
            }
        }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Polymorphism Example-1 ***\n\n");
            Vehicle obVehicle = new Bus();
            obVehicle.ShowMe();//Inside Bus.ShowMe
            // obVehicle.BusSpecificMethod();//Error
            //Bus obBus = new Vehicle();//Error
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

注意输出。这次调用的是子类方法(不是父类方法!).这是因为我们已经将 Vehicle 类中的 ShowMe()方法标记为 virtual。现在,编译器将不再看到调用该方法的明显类型(即,该调用无法在编译时绑定中解析)。当我们通过基类引用指向子类对象的方法时,编译器使用基类引用所引用的对象类型来调用正确的对象的方法。在这种情况下,编译器可以从 Bus 类中选择 ShowMe(),因为 Bus 对象被基类(Vehicle)引用所引用。

因此,通过将基类中的方法标记为虚拟的,我们打算实现多态。现在我们可以有意地在子类中重新定义(覆盖)该方法。在子类中,通过用关键字 override 标记一个方法,我们有意地重新定义了相应的虚方法。

Points to Remember

  • 在 C# 中,默认情况下,所有方法都是非虚拟的。但是,在 Java 中,它们默认是虚拟的。因此,在 C# 中,我们需要标记关键字 override 来避免任何无意识的覆盖。
  • C# 还使用 new 关键字将一个方法标记为非重写的,我们将很快讨论这一点。

学生问:

先生,你是说,“父类引用可以指向子对象,但反过来就不正确了。”我们为什么支持这种设计?

老师说:我们必须同意这些事实:我们可以说所有的公共汽车都是交通工具,但反过来不一定正确,因为还有其他交通工具,如火车、轮船,它们不一定是公共汽车。

同样,在编程术语中,所有派生类都是基类,但反之则不然。例如,假设我们有一个名为 Rectangle 的类,它是从另一个名为 Shape 的类派生而来的。那么我们可以说所有的矩形都是形状,但反过来就不正确了。

你必须记住,我们需要对继承层次进行“是-a”测试,“是-a”的方向总是直截了当的。

学生问:

先生,你是说调用将在运行时被解析为以下代码?

Vehicle obVehicle = new Bus();
 obVehicle.ShowMe();

但是我们可以清楚地看到,总线对象是由父类引用指向的,编译器可以在早期绑定(或编译时绑定)期间将 ShowMe()绑定到总线对象。为什么它不必要地拖延了进程?

老师说:看着前面的代码,你可能会这样想。但是让我们假设我们还有一个子类 Taxi,它也是从父类 Vehicle 继承而来的。在运行时,基于某些情况,我们需要从 Bus 或 Taxi 调用 ShowMe()方法。考虑如下情况:我们正在生成一个 0 到 10 之间的随机数。然后我们检查这个数字是偶数还是奇数。如果是偶数,我们使用 Bus 对象,否则我们使用 Taxi 对象调用相应的 ShowMe()方法。

考虑下面的代码。

演示 13

using System;

namespace PolymorphismEx3
{
    class Vehicle
    {
        public virtual void ShowMe()
        {
            Console.WriteLine("Inside Vehicle.ShowMe");
        }
    }
    class Bus : Vehicle
    {
        public override void ShowMe()
        {
            Console.WriteLine("Inside Bus.ShowMe");
        }
    }
    class Taxi : Vehicle
    {
        public override void ShowMe()
        {
            Console.WriteLine("Inside Taxi.ShowMe");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Polymorphism Example-3 ***\n");
            Vehicle obVehicle;
            int count = 0;
            Random r = new Random();
            while( count <5)
            {
                int tick = r.Next(0, 10);
                if(tick%2==0)
                {
                    obVehicle = new Bus();

                }
                else
                {
                    obVehicle = new Taxi();
                }
                obVehicle.ShowMe();//Output will be determined during runtime
                count++;
            }

            Console.ReadKey();
        }
    }
}

输出

请注意,输出可能会有所不同。

这是第一次运行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是第二次运行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

等等。

说明

现在,您应该意识到,对于这种编码,为什么编译器需要将决策延迟到运行时,以及我们是如何实现多态的。

学生问:

在某些情况下,我们可能想要设置限制。父类中的方法不应被其子类的方法重写。我们如何实现这一目标?

老师说:在很多面试中,你可能会面临这个问题。我们必须记住,我们可以通过使用“static”、“private”或“sealed”关键字来防止重写。但是这里我们只讨论了“密封”的使用。

考虑下面的代码。这里编译器本身阻止了继承过程。

演示 14

sealed class ParentClass
    {
        public void ShowClassName()
        {
            Console.WriteLine("Inside Parent.ShowClassName");
        }
    }
    class ChildClass : ParentClass //Error
    {
        //Some code
    }

我们将收到以下错误:“ChildClass”:不能从密封类型“ParentClass”派生。

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师继续说:密封的关键字可能不仅仅与类相关联。我们也可以用方法来使用它。通过下面的程序可以更好的理解它。

示范 15

class ParentClass
    {
        public virtual void ShowClassName()
        {
            Console.WriteLine("Inside Parent.ShowClassName");
        }
    }
    class ChildClass : ParentClass
    {
        sealed public override void ShowClassName()
        {
            Console.WriteLine("Inside ChildClass.ShowClassName");
        }
    }
    class GrandChildClass : ChildClass
    {
        public override void ShowClassName()
        {
            Console.WriteLine("Inside GrandChildClass.ShowClassName");
        }
    }

这里我们正在试验多级继承,其中(顾名思义)ChildClass 从 ParentClass 派生,GrandChildClass 从 ChildClass 派生。但是在 ChildClass 中,我们使用了用覆盖方法 ShowClassName()密封的关键字。因此,这表明我们不能在它的任何派生类中进一步重写该方法。

但是孙子一般都很调皮。因此,它试图违反由其父类强加的规则(注意,ChildClass 是 GrandChildClass 的父类)。因此,编译器立即提出了它的关注,说您不能违反您父母的规则,并显示以下错误消息:

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师继续说:现在考虑私有构造函数的情况。如果一个类只有私有构造函数,它就不能成为子类。这个概念可以用来创建一个单例设计模式,通过使用 new 关键字,我们可以防止在系统中创建不必要的对象;例如,下面的程序会给你一个编译错误。

示范 16

class ParentClass
    {
        private ParentClass() { }
        public void ShowClassName()
        {
            Console.WriteLine("Inside Parent.ShowClassName");
        }
    }
    class ChildClass : ParentClass //Error
    {
        //Some code
    }

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

恶作剧

你能预测产量吗?有没有编译错误?

示范 17

using System;

namespace QuizOnSealedEx1
{
    class QuizOnSealed
    {
        public virtual void TestMe()
        {
            Console.WriteLine("I am in Class-1");
        }

    }
    class Class1: QuizOnSealed
    {
        sealed public override void TestMe()
        {
            Console.WriteLine("I am in Class-1");
        }
    }
    class Class2: QuizOnSealed
    {
        public override void TestMe()
        {
            Console.WriteLine("I am in Classs-2");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Quiz on sealed keyword usage***\n");
            Class2 obClass2 = new Class2();
            obClass2.TestMe();
            Console.ReadKey();
        }
    }
}

输出

程序将成功编译并运行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

我们在这里没有遇到任何问题,因为 Class2 不是 Class1 的子类。它也是从同一个父类 QuizOnSealed 派生的,并且可以自由地覆盖 TestMe()方法。

Points to Remember

密封类不能是基类。它们阻止了衍生过程。这就是为什么它们不能是抽象的。MSDN 指出,通过一些运行时优化,我们可以更快地调用密封类成员。

学生问:

先生,到目前为止,您已经对方法和类使用了关键字 sealed。可以应用到成员变量上吗?

老师说:不。我们可以在这些上下文中使用 readonly 或 const。我们可以像声明变量一样声明常量,但关键是声明后不能更改。另一方面,我们可以在声明期间或通过构造函数给 readonly 字段赋值。要声明一个常量变量,我们需要在声明前加上关键字 const。常数是隐式静态的。这两者之间的比较包含在 C# 中一些关键比较的分析一章中(第八章)。

恶作剧

代码会编译吗?

class A
{
    sealed int a = 5;
}

回答

不可以。在 C# 中,这是不允许的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这种情况下,您可以使用 readonly。

恶作剧

代码会编译吗?

class A
    {
        sealed A()
        { }
    }

回答

不可以。在 C# 中,这是不允许的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师继续说:关键字 sealed 是用来防止重写的,但是根据语言规范,构造函数根本不能被重写。如果我们想防止构造函数被它的派生类调用,我们可以将它标记为私有。(构造函数不会被子类继承;如果需要,我们需要显式调用基类构造函数)。

学生问:

先生,为了防止遗传,哪一个过程需要优先考虑:情况 1 还是情况 2?

案例 1:

class A1
    {
      private A1() { }
      }

案例 2:

sealed
class A2
    {
        //some code..
    }

老师说:首先,我们需要知道我们的要求。我们不应该预先概括任何决定。在第一种情况下,我们可以添加其他东西,然后我们可以很容易地从中派生出一个新的类。但是在第二种情况下,我们不能派生一个子类。为了更好地理解它,让我们向案例 1 添加一些代码,并遵循这个案例研究。

示范 18

using System;

namespace SealedClassVsA_ClassWithPrivateCons
{
    class A1
    {
      public int x;
      private A1() { }
      public A1(int x) { this.x = x; }
    }
    sealed class A2
    {
        //some code..
    }
    class B1 : A1
    {
       public  int y;
        public B1(int x,int y):base(x)
        {
            this.y = y;
        }
    }
    //class B2 : A2 { }//Cannot derive from sealed type 'A2'

    class Program
    {
        static void Main(string[] args)
        {
           Console.WriteLine("***Case study: sealed class vs private constructor***\n");
           B1 obB1 = new B1(2, 3);
           Console.WriteLine("\t x={0}",obB1.x);
           Console.WriteLine("\t y={0}",obB1.y);                           Console.Read();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

我们可以看到,我们可以扩展案例 1 中的类,但是请注意注释行:

//class B2 : A2 { }//Cannot derive from sealed type 'A2'

如果取消注释,将会出现以下编译错误:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要记住的关键是,如果你使用私有构造函数只是为了防止继承,那么你使用的方法是错误的。私有构造函数通常用在只包含静态成员的类中。当你学习设计模式时,你会发现我们可以使用私有构造函数来停止额外的实例化。在这些情况下,我们的意图是不同的。

学生问:

先生,给我们一些提示,这样我们就可以很容易地区分方法重载和方法重写。

老师说:以下几点可以帮助你复习知识:

在方法重载中,所有的方法都可以驻留在同一个类中(注意这里的单词‘may ’,因为我们可以有这样的例子,方法重载的概念跨越了两个类——父类和子类)。

在方法重写中,涉及父类和子类的继承层次,这意味着在方法重写的情况下,至少涉及父类及其子类(即,最少两个类)。

考虑下面的程序和输出。

示范 19

using System;

namespace OverloadingWithMultipleClasses
{
    class Parent
    {
        public void ShowMe()
        {
            Console.WriteLine("Parent.ShowMe1.No parameter");
        }
        public void ShowMe(int a)
        {
            Console.WriteLine("Parent.ShowMe1\. One integer parameter");
        }
    }
    class Child:Parent
    {
        //An overloaded method in child/derived class
        public void ShowMe(int a,int b)
        {
            Console.WriteLine("Child.ShowMe1\. Two integer parameter");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Overloading across multiple classes***\n");
            Child childOb = new Child();
            //Calling all the 3 overloaded methods
            childOb.ShowMe();
            childOb.ShowMe(1);
            childOb.ShowMe(1,2);
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Note

甚至当你要使用这些重载方法时,Visual Studio 也会给你提示。可以看到子类对象可以访问 1+2;也就是说,在本例中总共有三个重载方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在方法重载中,签名是不同的。在方法重写中,方法签名是相同/兼容的。(虽然你现在不需要考虑兼容这个词。稍后你可能会学到 Java 中的协变返回类型,在那里“兼容”这个词对你来说是有意义的。但在 C# 中,可以忽略“兼容”二字)。

我们可以通过方法重载实现编译时(静态)多态,但我们可以通过方法重写实现运行时(动态)多态。对于静态绑定/早期绑定/重载,编译器在编译时收集它的知识,所以一般来说,它执行得更快。

Points to Remember

所有 C# 方法默认都是非虚的(但在 Java 中,正好相反)。这里我们使用关键字 override 来有意地覆盖或重定义一个方法(被标记为虚拟的)。除了这两个关键字之外,关键字 new 也可以将一个方法标记为非重写的。

老师说:让我们用一个简单的程序来演示“new”关键字在重写上下文中的用法,从而结束这次讨论。考虑下面的程序和输出。

演示 20

using System;

namespace OverridingEx3
{
    class ParentClass
    {
        public virtual void ShowMe()
        {
            Console.WriteLine("Inside Parent.ShowMe");
        }
    }
    class ChildClass : ParentClass
    {
        public new void ShowMe()
        {
            Console.WriteLine("Inside Child.ShowMe");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Use of 'new' in the context of method Overriding ***\n");
            ParentClass parentOb = new ParentClass();
            parentOb.ShowMe();//Calling Parent version
            ChildClass childOb = new ChildClass();
            childOb.ShowMe();//Calling Child version
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

如果您没有在子类的 ShowMe()方法中使用关键字 new,您将看到一条警告消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你熟悉 Java,你可能会发现这个特性很有趣,因为它在那里是不被允许的。想法很简单:C# 引入了标记非重写方法的概念,我们不希望多态地使用它。

老师继续说:为了理解 override 关键字的区别,考虑下面的例子。这里一个子类使用 override,另一个使用 new。现在比较多态行为。

演示 21

using System;

namespace OverridingEx4
{
    class ParentClass
    {
        public virtual void ShowMe()
        {
            Console.WriteLine("Inside Parent.ShowMe");
        }
    }
    class ChildClass1 : ParentClass
    {
        public override void ShowMe()
        {
            Console.WriteLine("Inside Child.ShowMe");
        }
    }
    class ChildClass2 : ParentClass
    {
        public new void ShowMe()
        {
            Console.WriteLine("Inside Child.ShowMe");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Use of 'new' in the context of method Overriding.Example-2 ***\n");
            ParentClass parentOb;
            parentOb= new ParentClass();
            parentOb.ShowMe();
            parentOb = new ChildClass1();
            parentOb.ShowMe();//Inside Child.ShowMe
            parentOb = new ChildClass2();
            parentOb.ShowMe();//Inside Parent.ShowMe

            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

我们可以在输出的最后一行看到使用 new 关键字的影响。我们可以看到“新”成员不是多态的(不打印子。ShowMe)。

抽象类

我们经常期望别人来完成我们未完成的工作。一个真实的例子是购买和改造房产。很常见的是,爷爷奶奶买了一套房产,然后父母在那套房产上盖了一个小房子,后来一个孙子把房子做大了或者重新装修了老房子。基本的想法是一样的:我们可能希望有人继续并首先完成未完成的工作。我们给他们自由,完工后,他们可以根据自己的需要进行改造。抽象类的概念最适合编程世界中的这类场景。

这些是不完整的类,我们不能从这种类型的类中实例化对象。这些类的子类必须首先完成它们,然后它们可以重新定义一些方法(通过重写)。

一般来说,如果一个类包含至少一个不完整/抽象的方法,那么这个类本身就是一个抽象类。术语抽象方法意味着该方法有声明(或签名)但没有实现。换句话说,您可以将抽象成员视为没有默认实现的虚拟成员。

Points to Remember

包含至少一个抽象方法的类必须标记为抽象类。

子类必须完成未完成的任务;也就是说,他们需要提供那些实现,但是如果他们没有提供,他们将再次被标记上 abstract 关键字。

因此,当一个基类/父类想要定义一个将被它的子类共享的通用形式时,这种技术非常有用。它只是将填写细节的责任传递给它的子类。让我们从一个简单的演示开始。

演示 22

using System;

namespace AbstractClassEx1
{
    abstract class MyAbstractClass
    {
        public abstract void ShowMe();
    }
    class MyConcreteClass : MyAbstractClass
    {
        public override void ShowMe()
        {
            Console.WriteLine("I am from a concrete class.");
            Console.WriteLine("My ShowMe() method body is complete.");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Abstract class Example-1 ***\n");
            //Error:Cannot create an instance of the abstract class
            // MyAbstractClass abstractOb=new MyAbstractClass();
            MyConcreteClass concreteOb = new MyConcreteClass();
            concreteOb.ShowMe();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

老师继续说:抽象类也可以包含具体的方法。子类可能会也可能不会重写这些方法。

示范 23

using System;

namespace AbstractClassEx2
{
    abstract class MyAbstractClass
    {
        protected int myInt = 25;
        public abstract void ShowMe();
        public virtual void CompleteMethod1()
        {
                      Console.WriteLine("MyAbstractClass.CompleteMethod1()");
        }
        public void CompleteMethod2()
        {
            Console.WriteLine("MyAbstractClass.CompleteMethod2()");
        }

    }
    class MyConcreteClass : MyAbstractClass
    {
        public override void ShowMe()
        {
            Console.WriteLine("I am from a concrete class.");
            Console.WriteLine("My ShowMe() method body is complete.");
            Console.WriteLine("value of myInt is {0}",myInt);
        }
        public override void CompleteMethod1()
        {
            Console.WriteLine("MyConcreteClass.CompleteMethod1()");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Abstract class Example-2 ***\n");
            //Error:Cannot create an instance of the abstract class
            // MyAbstractClass abstractOb=new MyAbstractClass();
            MyConcreteClass concreteOb = new MyConcreteClass();
            concreteOb.ShowMe();
            concreteOb.CompleteMethod1();
            concreteOb.CompleteMethod2();
            Console.WriteLine("\n\n*** Invoking methods through parent
            class reference now ***\n");
            MyAbstractClass absRef = concreteOb;
            absRef.ShowMe();
            absRef.CompleteMethod1();
            absRef.CompleteMethod2();
            Console.ReadKey();
        }
    }
}

输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明

前面的例子演示了我们可以使用抽象类引用来指向子类对象,然后我们可以调用相关的方法。稍后我们将了解到,我们可以从这种方法中获得巨大的好处。

学生问:

我们如何在这里实现运行时多态的概念?

老师说:我们在前面的例子中用过。请注意以下代码部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

抽象类可以包含字段吗?

老师说:是的。在前一个例子中,我们使用了这样一个字段;也就是 myInt。

学生问:

在前面的示例中,访问修饰符是 public。它是强制性的吗?

老师说:不。我们也可以用其他类型的。稍后你会注意到这是接口的关键区别之一。

学生问:

假设一个类中有十多个方法,其中只有一个是抽象方法。我们需要用关键字 abstract 来标记类吗?

老师说:是的。如果一个类包含至少一个抽象方法,那么这个类本身就是抽象的。你可以简单的认识到一个事实,用一个抽象的关键词来表示不完整。因此,如果你的类包含一个不完整的方法,那么这个类就是不完整的,因此它需要用关键字 abstract 来标记。

所以,简单的公式是,只要你的类至少有一个抽象方法,这个类就是一个抽象类。

老师继续说:现在考虑一个相反的情况。假设,你已经将你的类标记为抽象类,但是其中没有抽象方法,就像这样:

abstract class MyAbstractClass
   {
        protected int myInt = 25;
        //public abstract void ShowMe();
        public virtual void CompleteMethod1()
        {
            Console.WriteLine("MyAbstractClass.CompleteMethod1()");
        }
        public void CompleteMethod2()
        {
            Console.WriteLine("MyAbstractClass.CompleteMethod2()");
        }
    }

恶作剧

我们能编译这个程序吗?

回答

是的。它仍然可以编译,但是你必须记住你不能为这个类创建一个对象。所以,如果你这样编码:

MyAbstractClass absRef = new MyAbstractClass();//Error

编译器会提出它的问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

那么,先生,我们如何从抽象类中创建一个对象呢?

老师说:我们不能从抽象类中创建对象。

学生问:

先生,在我看来,一个抽象类如果不被扩展,实际上是没有任何用处的。这是正确的吗?

老师说:是的。

学生问:

如果一个类扩展了一个抽象类,它必须实现所有的抽象方法。这是正确的吗?

老师说:简单的公式是,如果你想创建一个类的对象,这个类需要被完成;也就是说,它不应该包含任何抽象方法。因此,如果子类不能提供所有抽象方法的实现(即主体),它应该用关键字 abstract 再次标记自己,如下例所示。

abstract class MyAbstractClass
    {
        public abstract void InCompleteMethod1();
        public abstract void InCompleteMethod2();
    }
    abstract class ChildClass : MyAbstractClass
    {
        public override void InCompleteMethod1()
        {
                Console.WriteLine("Making complete of InCompleteMethod1()");
        }
    }

在这种情况下,如果您忘记使用关键字 abstract,编译器将引发一个错误,指出 ChildClass 没有实现 InCompleteMethod2()。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

我们可以说一个具体的类是一个不抽象的类。这是正确的吗?

是的。

学生问:

有时候我们会对关键词的顺序感到困惑;例如,在前面的案例中,我们使用:

    public override void InCompleteMethod1(){...}

老师说:这个方法必须有一个返回类型,它应该在你的方法名之前。所以,如果你能记住这个概念,你就绝对不会写出 C# 中不正确的“public void override ”之类的东西。

学生问:

我们可以用抽象和密封来标记一个方法吗?

老师说:不。这就像如果你说你想探索 C# 但你不会浏览任何材料。类似地,通过声明 abstract,您希望在派生类之间共享一些公共信息,并且您表明重写对于它们是必要的;也就是说,继承链需要增长,但同时,通过声明 sealed,您希望在派生过程中加上结束标记,这样继承链就不会增长。因此,在这种情况下,您试图同时实现两个相反的约束。

恶作剧

你能预测产量吗?

using System;

namespace ExperimentWithConstructorEx1
{
    class MyTestClass
    {
        //Constructors cannot be abstract or sealed

        abstract MyTestClass()//Error
        {
            Console.WriteLine("abstract constructor");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Quiz : Experiment with a constructor***\n");
            MyTestClass ob = new MyTestClass();
            Console.ReadKey();
        }
    }
}

输出

编译错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,为什么构造函数不能是抽象的?

老师说:我们通常在一个类中使用关键字 abstract,表示它是不完整的,子类将负责使他完整。我们必须记住,构造函数不能被重写(也就是说,它们是密封的)。此外,如果你分析构造函数的实际目的(即初始化对象),你必须同意,因为我们不能从抽象类创建对象,这种设计非常适合这里。

恶作剧

你能预测产量吗?

using System;

namespace ExperimentWithAccessModifiersEx1
{
    abstract class IncompleteClass
    {
        public abstract  void ShowMe();
    }
    class CompleteClass : IncompleteClass
    {
        protected override void ShowMe()
        {
                Console.WriteLine("I am complete.");
                Console.WriteLine("I supplied the method body for showMe()");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Quiz : Experiment with access
            specifiers***\n");
            IncompleteClass myRef = new CompleteClass();
            myRef.ShowMe();
            Console.ReadKey();
        }
    }
}

输出

编译器错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:我们需要在 CompleteClass 中使用 public 访问修饰符,而不是 protected 访问修饰符。然后,您可以获得以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学生问:

先生,为什么我们不在两个职业中使用 protected?

老师说:从概念上讲,你可以这样做,但是你会在 Main()方法中遇到编译时错误。这是因为受保护方法的访问仅限于该类(它在其中定义)及其派生类实例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

摘要

本章涵盖了

  • 方法重载
  • 运算符重载
  • 方法覆盖
  • 抽象类
  • 如何用抽象类实现运行时多态
  • 方法签名
  • 如何识别方法是否重载
  • 如何霸王构造函数
  • 如何霸王主 _)法
  • 我们如何在程序中使用多个 Main()
  • 如何实现编译时多态和运行时多态
  • 为什么需要延迟绑定
  • 虚拟、覆盖、密封和抽象关键字的使用
  • 如何用不同的技术防止继承
  • 在应用中使用 sealed 关键字与在应用中使用私有构造函数的比较
  • 方法重载和方法重写的简单比较
  • 为什么构造函数不能是抽象的
  • 23+完整的程序演示和输出,详细涵盖这些概念
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值