02010801 类和继承01-继承、构造函数初始化语句

02010801 类和继承01-继承、构造函数初始化语句

1. 类继承

  • 通过继承可以定义新类,新类纳入一个已经声明的类并进行扩展。
    • 已存在的类称为基类(base class),新类称为派生类(derived class)。
  • 派生类成员的组成如下。
    • 本身声明中的成员。
    • 基类的成员。
// 声明一个派生类
class MyDerivedClass : MyClass // ": MyClass"是基类规格说明。
{
    ...
}

说明:
1. ": MyClass"是基类规格说明。
2. 派生类扩展它的基类,派生类包含了基类的成员,还有它本身声明中的新增功能。
3. 派生类不能删除它所继承的任何成员。

2. 所有类都派生自object类

  • 除了特殊的类object,所有的类都是派生类,即使它们没有基类规格说明。(没有基类规格说明的类隐式的直接派生自object类)。
  • 类object是唯一的非派生类,因为它是继承结构的基础。
  • 类继承的重要内容。
    • 一个类声明的基类规格说明只能有一个单独的类,这被称为单继承。
    • 虽然累只能直接继承一个基类,但派生的层次没有限制。
      • 也就是说作为基类的类可以派生自另外一个类,而这个类又派生自另外一个类...,直至最终到达object。

3. 屏蔽基类的成员

  • 虽然派生类不能删除它继承的任何成员,但可以用于基类成员名称相同的成员来屏蔽基类成员。这是类继承的主要功能之一,非常实用。
    • 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
    • 通过在派生类中声明新的带有相同签名的函数成员,可以屏蔽继承的函数成员。(注意:签名由名称和参数列表组成,不包括返回类型)
    • 要让编译器器知道你故意屏蔽了继承的成员,可以使用new修饰符。否则,程序可以成功编译,但编译器会告诉你隐藏了一个继承的成员。
    • 也可以屏蔽静态成员。
class MyBaseClass // 基类
{
    public string Field;
}

class MyClass : MyBaseClass // 派生类
{
    new public string Field; // 用同样的名称屏蔽基类成员,注意使用new关键字。
}

4. 基类访问

  • 如果派生类必须访问被隐藏的继承成员,可以使用基类访问表达式。
// 基类访问表达式
Console.WriteLine($"{base.Filed1}")
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Demo01
{
    class MyBaseClass
    {
        public string Field = "Base Field.";
    }

    class MyClass : MyBaseClass
    {
        new public string Field = "Derived Field";

        public void PrintField()
        {
            Console.WriteLine($"{Field}");
            Console.WriteLine($"{base.Field}");
        }
    }
    class Program
    {
        static void Main()
        {
            MyClass mc = new MyClass();
            mc.PrintField();

            Console.ReadLine();
        }
    }
}

控制台输出:
Derived Field
Base Field.

说明:如果在程序中经常使用这个特性(即访问隐藏的继承成员),说明类设计的并不合理,需要重新评估类的设计。

5. 使用基类的引用

  • 派生类的实例由基类的实例和派生类新增的成员组成。派生类的引用指向整个类对象,包括基类部分。
  • 如果有一个派生类对象的引用,就可以获取该对象基类部分的引用(使用类型转换运算符把该引用转换为基类类型)。
    • 类型转换运算符放置在对象引用的前面,由()括起来要被转换成的类名组成。
    • 将派生类对象强制转换为基类对象的作用是产生的变量只能访问基类的成员(在被覆写方法中除外)。
  • 使用对象的基类部分的引用来访问对象
图片链接丢失
派生类的引用可以看到完整的MyDerivedClass对象,而mybc只能看到对象的MyBaseClass部分
MyDerivedClass derived = new MyDerivedClass(); // @1 创建一个对象
MyBaseClass mybc = (MyBaseClass)derived; // @2 转换引用

说明:
1. 在@1处声明并初始化了变量derived,它包含一个MyDerivedClass类型对象的引用。
2. 在@2处声明了一个基类类型MyBaseClass的变量,并把derived中的引用转换为该类型,给出对象的基类部分的引用。
2.1 基类部分的引用被存储在变量mybc中,在赋值运算符的左边。
2.2 基类部分的引用“看不到”派生类对象的其余部分,因为它通过基类类型的引用“看”这个对象。即基类引用无法访问派生类成员。

6. 虚方法和覆写方法

  • 虚方法可以使基类的引用访问“升至”派生类内。可以使用基类引用调用派生类的方法,只需要满足以下条件。
    • 派生类的方法和基类的方法有相同的签名和返回类型。
    • 基类的方法使用virtual标注。
    • 派生类的方法使用override标注。
// 基类方法的virtual修饰符,派生类方法的override修饰符的使用
class MyBaseClass
{
    virtual public void Print()
    {
        ...
    }
}

class MyDerivedClass
{
    override public void Print()
    {
        ...
    }
}
...
MyDerivedClass derived = new MyDerivedClass(); // @1 创建一个对象
MyBaseClass mybc = (MyBaseClass)derived; // @2 转换引用

注意,当使用基类引用mybc调用Print方法时,方法调用被传递到派生类并执行,原因如下:
1 基类的方法被标记为virtual。
2 在派生类中有匹配的override方法。
  • 关于virtual和override修饰符的重要信息如下:
    • 覆写和被覆写的方法必须有相同的可访问性,即相同的访问修饰符。
    • 不能覆写static方法或非虚方法。
    • 方法、属性和索引器,以及另一种成员类型(即事件,后续讲解),都可以被声明为virtual和override。

7. 覆写标记为override的方法

// 情况1:使用override声明SecondDerivde中的方法。
class MyBaseClass
{
    virtual public void Print()
    {
        Console.WriteLine("This is the base class.");
    }
}

class MyDerivedClass : MyBaseClass
{
    override public void Print()
    {
        Console.WriteLine("This is the derived class.");
    }
}

class SecondDerived : MyDerivedClass
{
	override public void Print()
    {
        Console.WriteLine("This is seconde derived class")
	}
}
...
SecondDerived secondDerived = new SecondDerived();
MyBaseClass mybc = (MyBaseClass)secondDerived;
secondDerived.Print(); // 输出:This is seconde derived class.
mybc.Pirnt(); // 输出:This is seconde derived class.

说明:当使用对象基类部分的引用调用一个被覆写的方法时,方法的调用被沿派生层次上溯执行,一直到override的方法的最高派生版本。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 情况2:使用new声明Print。
class MyBaseClass
{
    virtual public void Print()
    {
        Console.WriteLine("This is the base class.");
    }
}

class MyDerivedClass : MyBaseClass
{
    override public void Print()
    {
        Console.WriteLine("This is the derived class.");
    }
}

class SecondDerivde : MyDerivedClass
{
	new public void Print()
    {
        Console.WriteLine("This is seconde derived class")
	}
}
...
SecondDerived secondDerived = new SecondDerived();
MyBaseClass mybc = (MyBaseClass)secondDerived;
secondDerived.Print(); // 输出:This is seconde derived class.
mybc.Pirnt(); // 输出:This is the derived class.

说明:这里override只传递了一级。

8. 构造函数的执行

  • 派生类对象有一部分是基类对象。
    • 要创建对象的基类部分,需要隐式的调用某个构造函数。
    • 继承层次链当中的每个类在执行它自己的构造函数之前执行它的基类构造函数。
class MyBaseClass
{
    MyBaseClass()
    {
        ...
    }
}

class MyDerivdeClass : MyBaseClass
{
    MyDerivedClass()
    {
        ...
    }
}

说明:在调用MyDerivedClass构造函数时,它在执行自己的方法体之前会先调用无参的构造函数MyBaseClass。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
class MyBaseClass
{
    public MyBaseClass()
    {
        ...
    }
}

class MyDerivdeClass : MyBaseClass
{
    int MyField01 = 5; // @1
    int MyField02; // @2
    publci MyDerivedClass() // @3
    {
        ...
    }
}

说明:在调用@3处的构造函数的时候,首先会给字段@1设置为5,字段@2设置为0。

注意:禁止在构造函数中调用虚方法。在执行类的构造函数时,基类的虚方法会调用派生类的覆写方法,但这是在执行派生类的构造函数方法体之前。因此调用会在派生类完全初始化(调用派生类的构造方法时,首先字段完成初始化,然后构造函数方法体执行完成)之前传递到派生类。

9. 构造函数初始化语句

  • 默认情况下,在构造对象时,将调用基类无参构造函数。但构造函数可以重载,所以基类可以有一个以上的构造函数。
  • 如果希望派生类使用一个指定的构造函数而不是无参构造函数,必须在构造函数初始化语句中指定它。
  • 有两种形式的构造函数初始化语句。
    • 第一种 → 使用关键字base并指明使用哪一种基类构造函数。
    • 第二种 → 使用关键字this并指明应该使用当前类的哪一个构造函数。
  • 基类构造函数初始化语句放在冒号后面,跟在类的构造函数声明的参数列表后面。
// 第一种构造函数初始化语句 
                                    构造函数初始化语句
                                            ↓
public MyDerivedClass(int x, string s) : base(s, x)
                                          ↑
                                        关键字
{
    
}

说明:
1. 构造函数初始化语句指明要使用两个参数的基类构造函数,并且第一个参数是int,第二个参数是string。
2. 基类参数列表中的参数必须在类型和顺序方面与已定的基类构造函数的参数列表相匹配。

注意,当声明一个不带构造函数初始化语句的构造函数时,它实际上是带有base()构造函数初始化语句的简写形式。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 第二种构造函数初始化语句
                       构造函数初始化语句
                              ↓
public MyClass(int x) : this(x, "abc")
                          ↑
                        关键字
{
    
}

说明:
1. 构造函数初始化语句使用this关键字让构造过程(实际上是编译器)使用当前类中其它的构造函数。
2. 如上述代码段,表示MyDerivedClass类包含一个带有参数的构造函数,但是这个单参数的构造函数使用了同一个类中具有两个参数的构造函数,第一个参数是int,第二个参数提供了一个默认值"abc"。

注意,这种语法很有用的另一个情况是,一个类有好几个构造函数,并且它们都需要在对象构造的过程开始时执行一些公共的代码。对于这种情况,可以将公共代码提取出来作为一个构造函数,被其它所有的构造函数用作构造函数初始化语句。这样减少了重复的代码,推荐使用。

10. 构造函数的访问修饰符

  • 对于公共构造函数的访问修饰符设定。
    • 如果这个构造函数能够初始化类中对象中所有需要初始化的内容,那么可以将这个公共构造函数设置为public。
    • 如果这个构造函数不能够完全初始化对象一个对象,必须禁止从类的外部调用该公共构造函数,因为这样只能初始化对象的一部分。
// 可以将构造函数设置为private,然后用其它的构造函数使用它创建对象。
class MyClass
{
    readonly int firstVar;
    readonly double secondVar;
    public string UserName;
    public int UserId;
    
    private MyClass()
    {
        first firstVar = 20;
        double secondVar = 30.5;
    }
    
    public MyClass(string firstName) : this()
    {
        UserName = firstName;
        UserId = 1000;
    }
    
    publci MyClass(int userId) : this()
    {
        UserName = "Qinway";
        UserId = userId;
    }
}

结尾

书籍:C#图解教程

著:【美】丹尼尔 · 索利斯;卡尔 · 施罗坦博尔

译:窦衍森;姚琪琳

ISBN:978-7-115-51918-4

版次:第5版

发行:人民邮电出版社

※敬请购买正版书籍,侵删请联系85863947@qq.com※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-08-10 22:13  qinway  阅读(12)  评论(0)    收藏  举报